summaryrefslogtreecommitdiffhomepage
path: root/packages/api/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-19 19:40:21 +0900
committerAdam Malczewski <[email protected]>2026-05-19 19:40:21 +0900
commitf78a91c20f658dd404277919a0b872b352c99bb6 (patch)
tree58cfffb655da4443f4b7a39543b86f988f15239f /packages/api/src
downloaddispatch-f78a91c20f658dd404277919a0b872b352c99bb6.tar.gz
dispatch-f78a91c20f658dd404277919a0b872b352c99bb6.zip
Phase 1: single agent + basic UIHEADmain
- Bun monorepo with @dispatch/core, @dispatch/api, @dispatch/frontend - Agent runtime with Vercel AI SDK, streaming via WebSocket - Tools: read_file, write_file, list_files (scoped to working directory) - Hono API server with POST /chat, GET /status, GET /health, WS /ws - Svelte 5 + DaisyUI frontend with chat UI, theme switcher, copy button - OpenCode Go (Zen) as LLM provider, deepseek-v4-flash-free model - Docker setup (dev + prod) with bin/ scripts and gopass secrets - Biome v2 linting/formatting, Vitest tests (44 passing) - Debug info attached to error messages for diagnostics
Diffstat (limited to 'packages/api/src')
-rw-r--r--packages/api/src/agent-manager.ts86
-rw-r--r--packages/api/src/app.ts46
-rw-r--r--packages/api/src/index.ts37
-rw-r--r--packages/api/src/types.ts2
4 files changed, 171 insertions, 0 deletions
diff --git a/packages/api/src/agent-manager.ts b/packages/api/src/agent-manager.ts
new file mode 100644
index 0000000..f60b8d3
--- /dev/null
+++ b/packages/api/src/agent-manager.ts
@@ -0,0 +1,86 @@
+import {
+ Agent,
+ type AgentEvent,
+ type AgentStatus,
+ createListFilesTool,
+ createReadFileTool,
+ createWriteFileTool,
+} from "@dispatch/core";
+
+const SYSTEM_PROMPT = `You are Dispatch, a helpful AI coding assistant. You have access to the following tools for working with files in the current working directory:
+
+- read_file: Read the contents of a file
+- write_file: Write content to a file (creates parent directories if needed)
+- list_files: List files and directories
+
+When asked to work with files, use these tools. Always confirm what you did after completing an action. Be concise and helpful.`;
+
+export class AgentManager {
+ private agent: Agent | null = null;
+ private status: AgentStatus = "idle";
+ private messageCount = 0;
+ private eventListeners: Set<(event: AgentEvent) => void> = new Set();
+
+ private getOrCreateAgent(): Agent {
+ if (!this.agent) {
+ const apiKey = process.env.OPENCODE_API_KEY ?? "";
+ const model = process.env.DISPATCH_MODEL ?? "deepseek-v4-flash-free";
+ const workingDirectory = process.env.DISPATCH_WORKING_DIR ?? process.cwd();
+
+ const tools = [
+ createReadFileTool(workingDirectory),
+ createWriteFileTool(workingDirectory),
+ createListFilesTool(workingDirectory),
+ ];
+
+ this.agent = new Agent({
+ model,
+ apiKey,
+ baseURL: "https://opencode.ai/zen/v1",
+ systemPrompt: SYSTEM_PROMPT,
+ tools,
+ workingDirectory,
+ });
+ }
+ return this.agent;
+ }
+
+ getStatus(): AgentStatus {
+ return this.status;
+ }
+
+ getMessageCount(): number {
+ return this.messageCount;
+ }
+
+ onEvent(listener: (event: AgentEvent) => void): () => void {
+ this.eventListeners.add(listener);
+ return () => {
+ this.eventListeners.delete(listener);
+ };
+ }
+
+ private emit(event: AgentEvent): void {
+ for (const listener of this.eventListeners) {
+ listener(event);
+ }
+ }
+
+ async processMessage(message: string): Promise<void> {
+ const agent = this.getOrCreateAgent();
+
+ this.messageCount += 1;
+
+ try {
+ for await (const event of agent.run(message)) {
+ this.status = event.type === "status" ? event.status : this.status;
+ this.emit(event);
+ }
+ } catch (err) {
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ this.status = "error";
+ this.emit({ type: "error", error: errorMsg });
+ this.emit({ type: "status", status: "error" });
+ }
+ }
+}
diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts
new file mode 100644
index 0000000..9c31eab
--- /dev/null
+++ b/packages/api/src/app.ts
@@ -0,0 +1,46 @@
+import { Hono } from "hono";
+import { cors } from "hono/cors";
+import { AgentManager } from "./agent-manager.js";
+
+export const agentManager = new AgentManager();
+
+export const app = new Hono();
+
+app.use(
+ "*",
+ cors({
+ origin: "http://localhost:5173",
+ credentials: true,
+ allowHeaders: ["Content-Type", "Authorization"],
+ allowMethods: ["GET", "POST", "OPTIONS"],
+ }),
+);
+
+app.get("/health", (c) => {
+ return c.json({ ok: true });
+});
+
+app.get("/status", (c) => {
+ return c.json({
+ status: agentManager.getStatus(),
+ messageCount: agentManager.getMessageCount(),
+ });
+});
+
+app.post("/chat", async (c) => {
+ const body = await c.req.json<{ message?: unknown }>();
+ const message = body.message;
+
+ if (typeof message !== "string" || message.trim() === "") {
+ return c.json({ error: "message must be a non-empty string" }, 400);
+ }
+
+ if (agentManager.getStatus() === "running") {
+ return c.json({ error: "agent is already running" }, 409);
+ }
+
+ // Non-blocking — let the agent run in the background
+ agentManager.processMessage(message).catch(console.error);
+
+ return c.json({ status: "ok" });
+});
diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts
new file mode 100644
index 0000000..02b04b6
--- /dev/null
+++ b/packages/api/src/index.ts
@@ -0,0 +1,37 @@
+import { createBunWebSocket } from "hono/bun";
+import { agentManager, app } from "./app.js";
+
+const { upgradeWebSocket, websocket } = createBunWebSocket();
+
+app.get(
+ "/ws",
+ upgradeWebSocket((_c) => {
+ return {
+ onOpen(_event, ws) {
+ // Send current status immediately
+ ws.send(JSON.stringify({ type: "status", status: agentManager.getStatus() }));
+
+ const unsubscribe = agentManager.onEvent((event) => {
+ ws.send(JSON.stringify(event));
+ });
+
+ // Store unsubscribe fn on the raw socket for cleanup
+ (ws as unknown as { _unsub?: () => void })._unsub = unsubscribe;
+ },
+ onClose(_event, ws) {
+ const unsub = (ws as unknown as { _unsub?: () => void })._unsub;
+ if (unsub) {
+ unsub();
+ }
+ },
+ };
+ }),
+);
+
+export { app };
+
+export default {
+ port: 3000,
+ fetch: app.fetch,
+ websocket,
+};
diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts
new file mode 100644
index 0000000..a88e41b
--- /dev/null
+++ b/packages/api/src/types.ts
@@ -0,0 +1,2 @@
+// Re-export types from @dispatch/core for convenience
+export type { AgentEvent, AgentStatus } from "@dispatch/core";