尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Embed a docs chat agent in your app. You give it your documentation (markdown and URLs) and an OpenAI-compatible host and key. It gives you a secure agent endpoint and a React chat UI that answers from your docs and shows its sources.
npm i sideagent
Docs: https://egig.github.io/sideagent/
sideagent/server)import { createAgent, honoHandler } from "sideagent/server";
const agent = await createAgent({
llm: { baseUrl: "https://api.openai.com/v1", apiKey: process.env.LLM_API_KEY!, model: "gpt-4o-mini" },
knowledge: {
embeddings: { model: "text-embedding-3-small" }, // OpenAI-compatible /embeddings; baseUrl/apiKey default to llm's
sources: [{ id: "guide", title: "Invoicing guide", content: markdownString }], // pass strings: no filesystem on the edge
urls: ["https://docs.example.com/payment-terms"], // fetched by the server only
},
allowedOrigins: ["https://app.example.com"],
getAuth: (req) => ({ userId: "…" }), // or anonymous: true
});
app.on(["POST", "OPTIONS"], "/agent", honoHandler(agent)); // also: nextRoute, expressHandler
console.log(await agent.doctor()); // LLM check + what was indexed
Secure by default: origin checks, threads bound to the user, size/step/time limits, SSRF guards on the pages the server fetches, passages given to the model as untrusted data and kept from the browser. The agent has one tool, search_knowledge, and cannot call your API or change anything.
sideagent)import { AgentChat } from "sideagent";
<AgentChat
endpoint="/agent"
mode="floating" // "floating" | "sidebar"; the user can switch
hooks={{ onTurnEnd: () => track("agent_turn") }}
/>
With an agent hosted on sideagent Cloud, pass its publishable key instead of endpoint. The key names the agent and its host, so the chat derives the URL (https://<host>/v1/agents/<agentId>) and sends the key in the x-sideagent-key header on every request. The key is public: it is safe in the browser and proves nothing on its own.
<AgentChat publishableKey="pk_…" />
Pass endpoint as well to send the requests somewhere else, such as a proxy in front of sideagent Cloud. A malformed key, or neither prop, throws during render. parsePublishableKey and publishableEndpoint are exported from sideagent/keys.
Signed-in users: pass getHeaders, which the chat calls before every request, so a login token that expires is fetched fresh each time. Pass storageKey (the user's ID) so the last conversation the chat remembers in localStorage belongs to that user; on a shared browser another user starts from their own.
<AgentChat
publishableKey="pk_…"
getHeaders={async () => ({ Authorization: `Bearer ${await getToken()}` })}
storageKey={user.id}
/>
getHeaders works with endpoint too. Its headers are merged under headers, which wins on a clash, so it cannot replace the publishable key. If it throws, the request is not sent and the chat shows an error.
Hide the floating launcher with launcher={false} and open the panel from your own button through useAgent().setOpen. Turn off chat history with history={false}: no History button, and the chat does not remember or reopen the last conversation. Add your own buttons to the panel header with headerActions.
Wrap your app in AgentProvider to dock the chat on the right, side by side with the app instead of over it. It takes the same props as AgentChat. The user drags the panel's left edge (or uses the arrow keys on it) to resize; the width is remembered. Outside AgentProvider, sidebar mode overlays the page.
import { AgentProvider } from "sideagent";
<AgentProvider endpoint="/agent" mode="sidebar" defaultOpen>
<App />
</AgentProvider>
AgentProvider fills the viewport height (100dvh); override with the className or style props.
Anything under AgentProvider can call useAgent() to get { open, setOpen, available }, e.g. an "Ask assistant" button in your toolbar.
pnpm install
pnpm demo # docs agent + mock LLM on :8787, UI on http://localhost:5173
The demo has a dark mode toggle and uses AgentProvider. Run pnpm demo:api alone for just the agent.
Set LLM_API_KEY (and optionally LLM_BASE_URL, LLM_MODEL) to use a real model.
pnpm build # tsup -> dist/
pnpm test # vitest
pnpm typecheck
Neutral by default (grayscale, host font, 10px radius). theme="auto" follows the OS and a host .dark / data-theme="dark" / data-bs-theme="dark"; pass "light" or "dark" to force one. Match your design system with CSS variables on :root:
:root {
--sideagent-accent: var(--primary); /* buttons */
--sideagent-accent-fg: var(--primary-foreground);
--sideagent-bg: var(--background);
--sideagent-fg: var(--foreground);
--sideagent-border: var(--border);
--sideagent-surface: var(--muted); /* user bubble, hovers */
--sideagent-muted: var(--muted-foreground);
--sideagent-radius: 10px;
--sideagent-font: inherit;
}
A search shows as a "Sources" card: the titles of the sources the answer came from, linked only for the URLs in your config. Raw arguments stay under "Details". Assistant replies render as Markdown (GFM); raw HTML is dropped.
Icons come from Radix Icons (MIT).
AgentChat has a History button (list, open, delete) and reopens the last conversation after a refresh. Messages live in your LangGraph checkpointer; the list lives in a threadStore (in-memory by default). Both must be persistent in production:
createAgent({ …, checkpointer: new PostgresSaver(…), threadStore: myThreadStore }); // ThreadStore: list / has / touch / delete
History is only ever shown from the user's own index, and search results are rebuilt from the already-filtered copies.
createAgent({
…,
knowledge: {
embeddings: { model: "text-embedding-3-small" }, // OpenAI-compatible /embeddings; baseUrl/apiKey default to llm's
sources: [{ id: "guide", title: "Invoicing guide", content: markdownString }], // pass strings: no filesystem on the edge
urls: ["https://docs.example.com/payment-terms", { url: "https://…", title: "Shipping" }], // fetched by the server only
},
vectorStore, // optional: your own VectorStore (pgvector, Vectorize…). Default is in-memory.
});
The model gets a search_knowledge tool; answers show a "Sources" card (links only for your configured URLs). Passages reach the model as labelled untrusted data and never reach the browser, unless your server opts in per request with exposeKnowledge. URLs come only from config, must be public http(s) (private ranges need allowPrivateNetworks, metadata addresses never), follow only same-host redirects, and are capped by maxSourceBytes. URL sources refresh in the background after ttlMs (default 1h) or on agent.knowledge.refresh(). A source that fails to load is skipped with a warning and shown in doctor().
Search is hybrid: a BM25 keyword ranking is fused with vector search (knowledge.hybrid, default on), and a custom vectorStore opts in with keywordSearch(query, k). A semantic cache reuses hits for a near-identical query embedding (knowledge.cache, default on, cleared whenever knowledge changes).
With the default in-memory store, every start re-embeds all sources (embedding cost). Use a persistent vectorStore for large knowledge.
sideagent/server runs on Workers. Use nodejs_compat (or just nodejs_als) in wrangler.jsonc. LangGraph relies on AsyncLocalStorage, and the Workers build of LangGraph does not register it. sideagent registers it for you when the runtime has it.
{ "compatibility_date": "2025-09-01", "compatibility_flags": ["nodejs_compat"] }
Export conditions. sideagent/server resolves to the real build under workerd and worker, listed before browser (which stays a throwing stub for real browser bundles). Wrangler (esbuild) and @cloudflare/vite-plugin need no alias. Both also set browser, so the order in exports matters.
Workers AI as the LLM. env.AI is a binding, not an HTTP endpoint, so use the adapter from sideagent/workers-ai instead of llm:
import { createAgent } from "sideagent/server";
import { workersAiChatModel, workersAiEmbeddings } from "sideagent/workers-ai";
export default {
async fetch(request: Request, env: Env) {
agent ??= await createAgent({
allowedOrigins: ["https://app.example.com"],
getAuth,
model: workersAiChatModel(env.AI, "@cf/openai/gpt-oss-120b"),
knowledge: { sources },
embeddings: workersAiEmbeddings(env.AI, "@cf/baai/bge-m3"), // replaces knowledge.embeddings
});
return agent.handler(request);
},
};
let agent: Awaited<ReturnType<typeof createAgent>> | undefined;
model and llm are mutually exclusive; so are the embeddings option and knowledge.embeddings. model can be any LangChain chat model that supports bindTools(). doctor() checks an injected model with one invoke, not an HTTP call. The adapter takes the binding structurally (no @cloudflare/workers-types needed) and reads gpt-oss Responses-style output, chat-style choices[0].message and { response }.
A custom checkpointer (D1, Durable Object storage) is a BaseCheckpointSaver with four methods: getTuple, list, put, putWrites. sideagent also calls deleteThread when a user deletes a thread.
npx skills add egig/sideagent, then ask your coding agent to "add an AI assistant to this app". See Quickstart.AGENTS.md: commands, layout and rules for coding agents working on this repo.llms.txt and llms-full.txt: the docs in LLM-friendly form.