@koda-sl/baker-bridge 0.35.2 → 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,60 +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` | `/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` |
57
62
 
58
- ## WebSocket Protocol
63
+ ## Chat flow
59
64
 
60
- Connect to `/ws?token=<AUTH_TOKEN>` for real-time bidirectional communication.
61
-
62
- ### Client to Server
63
-
64
- **Chat message:**
65
-
66
- ```json
67
- {
68
- "type": "chat",
69
- "threadId": "thread_abc123",
70
- "content": "Build a landing page for our new product",
71
- "attachments": [
72
- { "url": "https://...", "contentType": "image/png", "filename": "mockup.png" }
73
- ]
74
- }
75
65
  ```
76
-
77
- **Answer a pending question:**
78
-
79
- ```json
80
- {
81
- "type": "answer",
82
- "threadId": "thread_abc123",
83
- "toolUseId": "tool_xyz",
84
- "answers": { "question_key": "user response" }
85
- }
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)
86
82
  ```
87
83
 
88
- ### Server to Client
89
-
90
- The server sends Claude Agent SDK events as they stream:
91
-
92
- - `{ type: "assistant", data: ... }` assistant message chunks
93
- - `{ type: "tool_use", data: ... }` tool invocations
94
- - `{ type: "input_request", toolUseId, questions }` agent needs user input
95
- - `{ type: "result", costUsd, isError, errors? }` turn complete
96
-
97
- All non-streaming events are also relayed to Convex via `POST /api/chat/event`.
98
-
99
- ## Async Endpoint
100
-
101
- 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
102
107
 
103
108
  ```bash
104
109
  curl -X POST http://localhost:3000/message/async \
@@ -107,18 +112,19 @@ curl -X POST http://localhost:3000/message/async \
107
112
  -d '{"threadId": "thread_abc123", "prompt": "Analyze the campaign performance"}'
108
113
  ```
109
114
 
110
- On completion, the bridge calls:
111
- - `POST /api/chat/event` each SDK event (excluding stream events)
112
- - `POST /api/chat/complete` — final callback with `{ threadId, isError, costUsd, errors? }`
113
- - `POST /api/chat/input-request` — sets the thread to `awaiting_input` when the agent calls `AskUserQuestion`
114
- - `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.
115
117
 
116
118
  ## Architecture
117
119
 
118
- - **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.
119
- - **Multi-turn via resume** — the first message creates a fresh `query()`. Subsequent messages resume with the `sessionId` so conversation context carries over.
120
- - **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.
121
- - **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.
122
128
 
123
129
  ## Development
124
130
 
@@ -139,56 +145,30 @@ pnpm --filter @koda-sl/baker-bridge unlink:local # Remove link, restore registr
139
145
 
140
146
  ### Auto-publish (CI)
141
147
 
142
- 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`.
143
151
 
144
152
  ### Manual publish
145
153
 
146
154
  From the monorepo root:
147
155
 
148
156
  ```bash
149
- # Publish as @latest (production)
150
- ./scripts/publish-package.sh bridge
151
-
152
- # Publish as @next (pre-release)
153
- ./scripts/publish-package.sh bridge next
157
+ ./scripts/publish-package.sh bridge # @latest
158
+ ./scripts/publish-package.sh bridge next # @next pre-release
154
159
  ```
155
160
 
156
- 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.
157
163
 
158
164
  ### Testing a pre-release in sandboxes
159
165
 
160
- When you publish a `@next` version, sandboxes won't use it automatically — they use the version baked into the template. To override:
161
-
162
166
  ```bash
163
- # Point sandboxes to the pre-release version
164
167
  npx convex env set BAKER_BRIDGE_VERSION <version>
165
- ```
166
-
167
- 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.
168
-
169
- After testing, always clean up to avoid running pre-release in production:
170
-
171
- ```bash
168
+ # … test …
172
169
  npx convex env remove BAKER_BRIDGE_VERSION
173
170
  ```
174
171
 
175
- ## Debugging relay failures
176
-
177
- When `postToConvex` exhausts all retries, the error is appended to `~/.baker/relay-errors.jsonl` on the sandbox filesystem. Each line is a JSON object:
178
-
179
- ```jsonl
180
- {"timestamp":"2026-04-24T15:19:55.123Z","path":"/api/chat/complete","threadId":"abc123","status":502,"responseBody":"Bad Gateway"}
181
- {"timestamp":"2026-04-24T15:20:01.456Z","path":"/api/chat/event","threadId":"abc123","error":"fetch failed"}
182
- ```
183
-
184
- To inspect on a running sandbox:
185
-
186
- ```bash
187
- cat ~/.baker/relay-errors.jsonl
188
- ```
189
-
190
- If a thread is stuck in `"streaming"`, this file tells you whether the bridge failed to reach Convex (and why — auth, network, HTTP status).
191
-
192
172
  ## Requirements
193
173
 
194
174
  - Node.js 18+
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.34.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.34.0";
5
+ const VERSION = "0.37.0";
6
6
  function printUsage() {
7
7
  console.error("Usage: baker-bridge <command>");
8
8
  console.error("");
@@ -1,28 +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;
13
+ private activeAbort;
14
+ private streamingPromise;
15
+ private replacingStream;
16
+ private currentTurnTools;
17
+ private model;
18
+ lastUsedAt: number;
19
19
  constructor(threadId: string);
20
- /** Restore a previously persisted session ID (e.g. loaded from disk after restart). */
20
+ /** Cancel any in-flight run. The next status check inside runStream will trip. */
21
+ abort(): void;
21
22
  setSessionId(id: string): void;
22
- sendAndStream(content: string, callbacks: StreamCallbacks, attachments?: ChatAttachment[]): Promise<void>;
23
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;
24
32
  }
25
- export declare function getSlashCommands(): Promise<SlashCommand[]>;
26
33
  export declare function getOrCreateSession(threadId: string): Promise<AgentSession>;
34
+ export declare function abortSession(threadId: string): boolean;
35
+ export declare function getSlashCommands(): Promise<SlashCommand[]>;
27
36
  export {};
28
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;gBAEb,QAAQ,EAAE,MAAM;IAM5B,uFAAuF;IACvF,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAIxB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,eAAe,EAAE,WAAW,CAAC,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAqD/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,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"}