Herdr UI
Herdr의 TUI가 뛰어나며, 창을 통해 추가되는 기능
앱에 채팅 에이전트를 삽입, OpenAPI 명세와 OpenAI 호스트·키 제공하면 보안 엔드포인트와 React 채팅 UI 제공
Embed a chat agent in your app. You give it an OpenAPI spec and an OpenAI-compatible host and key. It gives you a secure agent endpoint and a React chat UI.
개발 도구TypeScript · JavaScriptGitHub @egig온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 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.