@koda-sl/baker-bridge 0.36.0-dev.7ebf7748-dev.7ebf7748 → 0.37.0-dev.c223fbd9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # @koda-sl/baker-bridge
2
2
 
3
- HTTP server wrapping the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) with WebSocket, SSE streaming, and async endpoints. Designed for running Claude Code as a headless agent with real-time bidirectional communication and Convex callback relay.
3
+ HTTP server wrapping the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk).
4
+ Runs inside a sandbox, persists every chat event directly to Convex, and exposes
5
+ HTTP endpoints for dispatch / abort / answer-question. The dashboard subscribes
6
+ to Convex realtime — there is no chat WebSocket.
4
7
 
5
8
  ## Installation
6
9
 
@@ -33,9 +36,10 @@ await server.start();
33
36
 
34
37
  | Variable | Description |
35
38
  |---|---|
36
- | `AUTH_TOKEN` | Bearer token for authenticating requests |
37
- | `BAKER_CONVEX_SITE_URL` | Convex site URL for callback relay (e.g. `https://your-deployment.convex.site`) |
39
+ | `AUTH_TOKEN` | Bearer token for `/message/*` and `/answer-question` |
40
+ | `BAKER_CONVEX_SITE_URL` | Convex site URL for chat persistence (e.g. `https://your-deployment.convex.site`) |
38
41
  | `BAKER_API_KEY` | API key for Convex callbacks (`bk_...`) |
42
+ | `INTERNAL_SECRET_KEY` | AES key for encrypting flow `_data.json` config at rest |
39
43
  | `ANTHROPIC_API_KEY` | Anthropic API key (required by the Agent SDK) |
40
44
 
41
45
  All variables are validated at startup via `@t3-oss/env-core` + `zod`.
@@ -45,72 +49,61 @@ All variables are validated at startup via `@t3-oss/env-core` + `zod`.
45
49
  | Method | Path | Auth | Description |
46
50
  |--------|------|------|-------------|
47
51
  | `GET` | `/health` | None | Health check — returns `{ status, projectDir }` |
48
- | `GET` | `/company` | None | Return COMPANY.md as JSON `{ content, title }` |
49
- | `GET` | `/brand` | None | Return BRAND.md as JSON `{ content, title }` |
50
- | `GET` | `/knowledge/:collection/:entry` | None | Return knowledge entry as JSON `{ content, title }` |
52
+ | `GET` | `/commands` | None | List available slash commands (cached 5m) |
53
+ | `GET` | `/company` | None | Return COMPANY.md as `{ content, title }` |
54
+ | `GET` | `/brand` | None | Return BRAND.md as `{ content, title }` |
55
+ | `GET` | `/knowledge/:collection/:entry` | None | Return knowledge entry as `{ content, title }` |
51
56
  | `GET` | `/documents/:slug` | None | Serve document binary (pdf, pptx, docx, xlsx) |
52
57
  | `GET` | `/flows/:slug` | None | Read flow `_data.json` (decrypted) |
53
58
  | `PUT` | `/flows/:slug` | None | Write flow `_data.json` (encrypts at rest) |
54
- | `GET` | `/ws` | Query param `?token=` | WebSocketpersistent bidirectional chat |
55
- | `POST` | `/message/async` | Bearer token | Fire-and-forget with Convex callback |
56
- | `POST` | `/message/abort` | Bearer token | Abort a running query — kills the CLI subprocess |
57
- | `POST` | `/answer-question` | Bearer token | Resolve a pending `AskUserQuestion` tool call |
59
+ | `POST` | `/message/async` | Bearer token | Dispatch a chat turn fire-and-forget, 202 immediately |
60
+ | `POST` | `/message/abort` | Bearer token | Abort a running query — kills the SDK iterator |
61
+ | `POST` | `/answer-question` | Bearer token | Resolve a pending `AskUserQuestion` |
58
62
 
59
- ## WebSocket Protocol
63
+ ## Chat flow
60
64
 
61
- Connect to `/ws?token=<AUTH_TOKEN>` for real-time bidirectional communication.
62
-
63
- ### Client to Server
64
-
65
- **Chat message:**
66
-
67
- ```json
68
- {
69
- "type": "chat",
70
- "threadId": "thread_abc123",
71
- "content": "Build a landing page for our new product",
72
- "attachments": [
73
- { "url": "https://...", "contentType": "image/png", "filename": "mockup.png" }
74
- ]
75
- }
76
65
  ```
77
-
78
- **Answer a pending question:**
79
-
80
- ```json
81
- {
82
- "type": "answer",
83
- "threadId": "thread_abc123",
84
- "toolUseId": "tool_xyz",
85
- "answers": { "question_key": "user response" }
86
- }
66
+ Convex action ──POST /message/async──▶ Bridge AgentSession
67
+
68
+
69
+ SDK query() iterator
70
+
71
+ ┌───────────────────────────┼───────────────────────────┐
72
+ ▼ ▼ ▼
73
+ /api/chat/stream /api/chat/event /api/chat/complete
74
+ (throttled ~50ms (assistant events, (final cost / status)
75
+ text + tool list, idempotent on uuid)
76
+ replaces preview)
77
+
78
+
79
+ Convex (single source
80
+ of truth — UI reads
81
+ via realtime query)
87
82
  ```
88
83
 
89
- **Abort a running query:**
90
-
91
- ```json
92
- {
93
- "type": "abort",
94
- "threadId": "thread_abc123"
95
- }
96
- ```
97
-
98
- Immediately kills the CLI subprocess via `AbortController`. The SDK's abort signal propagates to all tool handlers, cleaning up pending operations. The session ID is preserved on disk so subsequent messages can resume the conversation.
99
-
100
- ### Server to Client
101
-
102
- The server sends Claude Agent SDK events as they stream:
103
-
104
- - `{ type: "assistant", data: ... }` assistant message chunks
105
- - `{ type: "tool_use", data: ... }` tool invocations
106
- - `{ type: "input_request", toolUseId, questions }` agent needs user input
107
- - `{ type: "result", costUsd, isError, errors? }` turn complete
108
-
109
- All non-streaming events are also relayed to Convex via `POST /api/chat/event`.
110
-
111
- ## Async Endpoint
112
-
113
- For server-to-server dispatch where you don't need real-time streaming. Returns `202 Accepted` immediately and processes in the background. Results are delivered via Convex callbacks.
84
+ - **Live preview** is throttled to ~50ms — the bridge buffers text deltas and
85
+ in-flight tool starts and POSTs them to `/api/chat/stream`. The Convex
86
+ mutation overwrites `thread.streamingState`. The UI reactively re-renders.
87
+ - **Persisted messages** go to `/api/chat/event` with the SDK's `event.uuid`
88
+ for idempotent insert. The mutation also clears `streamingState` inline so
89
+ the UI snaps from preview to persisted text without a flicker.
90
+ - **Stop button** flips the thread to `cancelled` via Convex first (UI is
91
+ freed immediately) and best-effort POSTs `/message/abort` to kill the SDK.
92
+ - **AskUserQuestion** posts `/api/chat/input-request` and blocks until the
93
+ dashboard answers via Convex action bridge `/answer-question`.
94
+
95
+ ## Resilience
96
+
97
+ - All Convex POSTs retry with exponential backoff (1s, 2s, 4s).
98
+ - Persistent failures of `/api/chat/event` and `/api/chat/complete` are
99
+ appended to `~/.baker/relay-errors.jsonl` for post-mortem.
100
+ - The SDK iterator is wrapped in a 5 min watchdog: if no event arrives for
101
+ that long, the run is aborted and the thread is marked errored.
102
+ - The session registry evicts entries idle for >24h to bound memory.
103
+ - A new chat that arrives while a previous one is still running aborts the
104
+ previous run with a 30s timeout cap a hung run cannot block the next message.
105
+
106
+ ## Sending dispatch by hand
114
107
 
115
108
  ```bash
116
109
  curl -X POST http://localhost:3000/message/async \
@@ -119,18 +112,19 @@ curl -X POST http://localhost:3000/message/async \
119
112
  -d '{"threadId": "thread_abc123", "prompt": "Analyze the campaign performance"}'
120
113
  ```
121
114
 
122
- On completion, the bridge calls:
123
- - `POST /api/chat/event` each SDK event (excluding stream events)
124
- - `POST /api/chat/complete` — final callback with `{ threadId, isError, costUsd, errors? }`
125
- - `POST /api/chat/input-request` — sets the thread to `awaiting_input` when the agent calls `AskUserQuestion`
126
- - `POST /api/chat/input-resolved` — returns the thread to `streaming` after the user answers
115
+ The bridge processes in the background and writes to Convex; nothing is
116
+ returned in the dispatch response besides 202.
127
117
 
128
118
  ## Architecture
129
119
 
130
- - **One session per thread** — `getOrCreateSession(threadId)` ensures all messages on the same thread share an `AgentSession`, whether they arrive via WebSocket or the async endpoint.
131
- - **Multi-turn via resume** — the first message creates a fresh `query()`. Subsequent messages resume with the `sessionId` so conversation context carries over.
132
- - **Attachment handling** file URLs are downloaded to `/tmp/attachments` and appended to the prompt so the agent can use its `Read` tool to view them.
133
- - **Convex relay** — all SDK events (except `stream_event`) are posted to the Convex backend with exponential backoff retry (3 attempts).
120
+ - **One session per thread** — `getOrCreateSession(threadId)` ensures every
121
+ message on the same thread shares an `AgentSession`. Multi-turn context is
122
+ preserved across bridge restarts via `~/.baker/sessions/<threadId>.json`.
123
+ - **Run-token cancellation** — each call to `sendAndStream` mints a Symbol; if
124
+ the symbol changes (abort, replace) the run cleans up and exits without
125
+ writing further. No globally-mutable abort flag.
126
+ - **Single-writer to Convex** — there is no parallel WebSocket. Convex is the
127
+ only place the UI reads state from, eliminating dual-write races.
134
128
 
135
129
  ## Development
136
130
 
@@ -151,36 +145,27 @@ pnpm --filter @koda-sl/baker-bridge unlink:local # Remove link, restore registr
151
145
 
152
146
  ### Auto-publish (CI)
153
147
 
154
- Pushing to `main` with changes in `packages/bridge/` triggers the GitHub Actions workflow, which publishes to npm with the `latest` tag by default. To publish a pre-release, use the workflow dispatch with `npm_tag: next`.
148
+ Pushing to `main` with changes in `packages/bridge/` triggers the GitHub Actions
149
+ workflow, which publishes to npm with the `latest` tag by default. To publish a
150
+ pre-release, use the workflow dispatch with `npm_tag: next`.
155
151
 
156
152
  ### Manual publish
157
153
 
158
154
  From the monorepo root:
159
155
 
160
156
  ```bash
161
- # Publish as @latest (production)
162
- ./scripts/publish-package.sh bridge
163
-
164
- # Publish as @next (pre-release)
165
- ./scripts/publish-package.sh bridge next
157
+ ./scripts/publish-package.sh bridge # @latest
158
+ ./scripts/publish-package.sh bridge next # @next pre-release
166
159
  ```
167
160
 
168
- The script checks for duplicate versions, builds, and publishes. Bump the version in `package.json` before publishing.
161
+ Bump `version` in `package.json` (and the `VERSION` const in `src/cli.ts`)
162
+ before publishing.
169
163
 
170
164
  ### Testing a pre-release in sandboxes
171
165
 
172
- When you publish a `@next` version, sandboxes won't use it automatically — they use the version baked into the template. To override:
173
-
174
166
  ```bash
175
- # Point sandboxes to the pre-release version
176
167
  npx convex env set BAKER_BRIDGE_VERSION <version>
177
- ```
178
-
179
- The sandbox startup (`nodeActions.ts`) checks this env var and runs `npm install -g @koda-sl/baker-bridge@<version>` before starting. This is optional — when unset, the default template version is used.
180
-
181
- After testing, always clean up to avoid running pre-release in production:
182
-
183
- ```bash
168
+ # … test …
184
169
  npx convex env remove BAKER_BRIDGE_VERSION
185
170
  ```
186
171
 
package/dist/cli.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  declare const args: string[];
3
3
  declare const command: string;
4
- declare const VERSION = "0.36.0";
4
+ declare const VERSION = "0.37.0";
5
5
  declare function printUsage(): void;
6
6
  declare function main(): Promise<void>;
7
7
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
  const args = process.argv.slice(2);
4
4
  const command = args[0];
5
- const VERSION = "0.36.0";
5
+ const VERSION = "0.37.0";
6
6
  function printUsage() {
7
7
  console.error("Usage: baker-bridge <command>");
8
8
  console.error("");
@@ -1,34 +1,37 @@
1
- import type { SDKMessage, SlashCommand } from "@anthropic-ai/claude-agent-sdk";
1
+ import type { SlashCommand } from "@anthropic-ai/claude-agent-sdk";
2
2
  import type { ChatAttachment } from "./types.ts";
3
- interface StreamCallbacks {
4
- onMessage: (msg: SDKMessage) => void;
5
- onInputRequest: (toolUseId: string, questions: unknown) => void;
6
- onComplete: (costUsd: number, isError: boolean, errors?: string[], model?: string) => void;
7
- onModelSelected?: (model: string) => void;
8
- onRouterSample?: (model: string, messages: Array<{
9
- role: string;
10
- content: string;
11
- }>) => void;
12
- }
13
3
  declare class AgentSession {
14
4
  readonly threadId: string;
15
5
  private sessionId;
16
6
  private pendingQuestions;
17
7
  private handlerCtx;
18
8
  private options;
9
+ /** Identity token for the currently-running stream. abort() nulls this; if a
10
+ * later check sees a different value, the run knows it was abandoned. This
11
+ * replaces a global `aborted` flag whose reset-on-entry was racy. */
12
+ private activeRun;
19
13
  private activeAbort;
20
- private aborted;
14
+ private streamingPromise;
15
+ private replacingStream;
16
+ private currentTurnTools;
17
+ private model;
18
+ lastUsedAt: number;
21
19
  constructor(threadId: string);
20
+ /** Cancel any in-flight run. The next status check inside runStream will trip. */
22
21
  abort(): void;
23
- /** Restore a previously persisted session ID (e.g. loaded from disk after restart). */
24
22
  setSessionId(id: string): void;
25
- private preparePrompt;
26
- private handleStreamError;
27
- sendAndStream(content: string, callbacks: StreamCallbacks, attachments?: ChatAttachment[]): Promise<void>;
28
23
  resolveQuestion(toolUseId: string, answers: Record<string, string>): boolean;
24
+ sendAndStream(content: string, attachments?: ChatAttachment[]): Promise<void>;
25
+ private runStream;
26
+ private handleStreamEvent;
27
+ private handleContentBlockStart;
28
+ private handleMessage;
29
+ private preparePrompt;
30
+ private completeWithCancel;
31
+ private completeWithError;
29
32
  }
30
- export declare function getSlashCommands(): Promise<SlashCommand[]>;
31
- export declare function abortSession(threadId: string): boolean;
32
33
  export declare function getOrCreateSession(threadId: string): Promise<AgentSession>;
34
+ export declare function abortSession(threadId: string): boolean;
35
+ export declare function getSlashCommands(): Promise<SlashCommand[]>;
33
36
  export {};
34
37
  //# sourceMappingURL=agent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/hono/agent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,UAAU,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAMxF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAcjD,UAAU,eAAe;IACvB,SAAS,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,IAAI,CAAC;IACrC,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;IAChE,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3F,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,KAAK,IAAI,CAAC;CAC9F;AAUD,cAAM,YAAY;IAChB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,gBAAgB,CAAsC;IAC9D,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,OAAO,CAAS;gBAEZ,QAAQ,EAAE,MAAM;IAM5B,KAAK,IAAI,IAAI;IAQb,uFAAuF;IACvF,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;YAIhB,aAAa;IA4B3B,OAAO,CAAC,iBAAiB;IAuBnB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,eAAe,EAAE,WAAW,CAAC,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAoD/G,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO;CAS7E;AAyHD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAuBhE;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAOtD;AAED,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAahF"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/hono/agent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAuB,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAgBxF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAuCjD,cAAM,YAAY;IAChB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,gBAAgB,CAAsC;IAC9D,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,OAAO,CAAU;IAEzB;;0EAEsE;IACtE,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,KAAK,CAAqB;IAClC,UAAU,EAAE,MAAM,CAAc;gBAEpB,QAAQ,EAAE,MAAM;IAM5B,kFAAkF;IAClF,KAAK,IAAI,IAAI;IAkBb,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAI9B,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO;IAUtE,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YA8BrE,SAAS;IAgFvB,OAAO,CAAC,iBAAiB;IAiBzB,OAAO,CAAC,uBAAuB;YAiBjB,aAAa;YAiBb,aAAa;YAoBb,kBAAkB;YAYlB,iBAAiB;CAehC;AAqFD,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAahF;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAOtD;AAWD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAuBhE"}