@bivy/bivy 0.0.0 → 0.1.0-staging.2

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.
Files changed (146) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +265 -5
  3. package/bin/acp-shim.mjs +298 -0
  4. package/bin/agent-manifest.json +277 -0
  5. package/bin/bivy.mjs +4100 -0
  6. package/bin/codex-app-server-shim.mjs +447 -0
  7. package/bin/patch-pi-dependencies.mjs +44 -0
  8. package/bin/prune-sessions.mjs +52 -0
  9. package/bin/sessions-list.mjs +27 -0
  10. package/bin/shim-path.mjs +126 -0
  11. package/bin/uninstall-paths.mjs +48 -0
  12. package/dist/approval.js +87 -0
  13. package/dist/attach.js +248 -0
  14. package/dist/auth.js +258 -0
  15. package/dist/bivy-login.js +180 -0
  16. package/dist/browser-open.js +50 -0
  17. package/dist/control-plane-tasks.js +236 -0
  18. package/dist/data-dir.js +25 -0
  19. package/dist/device-registry.js +201 -0
  20. package/dist/e2e.js +70 -0
  21. package/dist/ephemeral-exec.js +109 -0
  22. package/dist/exec.js +209 -0
  23. package/dist/git-auth.js +155 -0
  24. package/dist/github-app-auth.js +107 -0
  25. package/dist/github-app-connect.js +235 -0
  26. package/dist/github-app-manifest.js +82 -0
  27. package/dist/github-app-sync-cli.js +93 -0
  28. package/dist/github-app-vault.js +106 -0
  29. package/dist/github-apps.js +121 -0
  30. package/dist/github-connect-repo.js +74 -0
  31. package/dist/github-device-auth.js +109 -0
  32. package/dist/github-tasks.js +650 -0
  33. package/dist/guard.js +109 -0
  34. package/dist/harness/cache-evict.js +88 -0
  35. package/dist/harness/checkpoint.js +0 -0
  36. package/dist/harness/cow-clone.js +84 -0
  37. package/dist/harness/dep-cache.js +78 -0
  38. package/dist/harness/disk-admission.js +46 -0
  39. package/dist/harness/egress.js +30 -0
  40. package/dist/harness/manager.js +97 -0
  41. package/dist/harness/mcp-config-formats.js +164 -0
  42. package/dist/harness/mcp-config.js +111 -0
  43. package/dist/harness/mcp-inject.js +134 -0
  44. package/dist/harness/mcp-proxy-cli.js +88 -0
  45. package/dist/harness/mcp-proxy.js +150 -0
  46. package/dist/harness/net-proxy.js +120 -0
  47. package/dist/harness/sandbox.js +96 -0
  48. package/dist/history-sync.js +26 -0
  49. package/dist/hosted-endpoints.d.mts +14 -0
  50. package/dist/hosted-endpoints.mjs +35 -0
  51. package/dist/identity.js +153 -0
  52. package/dist/integrations/index.js +4 -0
  53. package/dist/integrations/manager.js +279 -0
  54. package/dist/integrations/oauth.js +78 -0
  55. package/dist/integrations/registry.js +239 -0
  56. package/dist/integrations/store.js +54 -0
  57. package/dist/integrations/types.js +1 -0
  58. package/dist/linear-tasks.js +49 -0
  59. package/dist/metadata.js +226 -0
  60. package/dist/multiplexer.js +79 -0
  61. package/dist/native-pi.js +38 -0
  62. package/dist/node-stats.js +237 -0
  63. package/dist/pairing-crypto.js +105 -0
  64. package/dist/policy/conditions.js +103 -0
  65. package/dist/policy/policy-engine.js +20 -0
  66. package/dist/policy/risk.js +18 -0
  67. package/dist/policy/ruleset.js +113 -0
  68. package/dist/policy/run-policy.js +108 -0
  69. package/dist/policy/session-reroute.js +96 -0
  70. package/dist/pty-runner.py +95 -0
  71. package/dist/question.js +146 -0
  72. package/dist/redact.js +97 -0
  73. package/dist/relay-attach.js +345 -0
  74. package/dist/relay-chunk.js +73 -0
  75. package/dist/relay-cli-crypto.js +70 -0
  76. package/dist/relay-client.js +344 -0
  77. package/dist/relay-setup.js +262 -0
  78. package/dist/repo-workspace.js +208 -0
  79. package/dist/runtime/adoption.js +45 -0
  80. package/dist/runtime/agent-service-bin.js +149 -0
  81. package/dist/runtime/agent-service.js +439 -0
  82. package/dist/runtime/ansi.js +27 -0
  83. package/dist/runtime/anthropic-preflight.js +80 -0
  84. package/dist/runtime/claude-code.js +1364 -0
  85. package/dist/runtime/cli-parsers.js +647 -0
  86. package/dist/runtime/codex-auth.js +168 -0
  87. package/dist/runtime/codex-preflight.js +60 -0
  88. package/dist/runtime/codex-sessions.js +229 -0
  89. package/dist/runtime/control-plane-location.js +74 -0
  90. package/dist/runtime/credential-ingest.js +122 -0
  91. package/dist/runtime/credential-provisioning.js +79 -0
  92. package/dist/runtime/credential-store.js +435 -0
  93. package/dist/runtime/credentials.js +153 -0
  94. package/dist/runtime/host.js +153 -0
  95. package/dist/runtime/index.js +1548 -0
  96. package/dist/runtime/local-model-store.js +194 -0
  97. package/dist/runtime/location-registry.js +28 -0
  98. package/dist/runtime/model-catalog.js +97 -0
  99. package/dist/runtime/model-namer.js +85 -0
  100. package/dist/runtime/native-process-scan.js +102 -0
  101. package/dist/runtime/native-session-discovery.js +103 -0
  102. package/dist/runtime/normalize.js +75 -0
  103. package/dist/runtime/oauth/model-oauth-providers.js +75 -0
  104. package/dist/runtime/oauth/model-oauth.js +324 -0
  105. package/dist/runtime/opencode-preflight.js +55 -0
  106. package/dist/runtime/pi-auth.js +82 -0
  107. package/dist/runtime/pi-oauth.js +52 -0
  108. package/dist/runtime/pi-session-discovery.js +42 -0
  109. package/dist/runtime/pi.js +518 -0
  110. package/dist/runtime/process.js +499 -0
  111. package/dist/runtime/protocol.js +630 -0
  112. package/dist/runtime/remote.js +541 -0
  113. package/dist/runtime/rpc-protocol.js +56 -0
  114. package/dist/runtime/ruleset-store.js +117 -0
  115. package/dist/runtime/session-location.js +50 -0
  116. package/dist/runtime/types.js +17 -0
  117. package/dist/secrets-cli.js +134 -0
  118. package/dist/secrets.js +264 -0
  119. package/dist/server.js +9411 -0
  120. package/dist/session/bivy-session.js +1 -0
  121. package/dist/session/checkpoint-pack.js +133 -0
  122. package/dist/session/event-log.js +340 -0
  123. package/dist/session/fork-dirty.js +73 -0
  124. package/dist/session/fork-prereqs.js +61 -0
  125. package/dist/session/fork.js +57 -0
  126. package/dist/session/native-import.js +56 -0
  127. package/dist/session/reconnect.js +168 -0
  128. package/dist/session/replication-service.js +236 -0
  129. package/dist/session/replication.js +106 -0
  130. package/dist/session/replicator.js +140 -0
  131. package/dist/session/session-new-dedupe.js +42 -0
  132. package/dist/session/sibling-client.js +201 -0
  133. package/dist/session/transcript-merge.js +131 -0
  134. package/dist/session/transcript-normal.js +130 -0
  135. package/dist/session/workspace-context.js +1 -0
  136. package/dist/session-event-coalescer.js +50 -0
  137. package/dist/session-identity.js +34 -0
  138. package/dist/session-ref.js +65 -0
  139. package/dist/stt-cli.js +131 -0
  140. package/dist/stt.js +168 -0
  141. package/dist/terminal.js +409 -0
  142. package/dist/wire-format.js +67 -0
  143. package/dist/worktree-provision.js +118 -0
  144. package/dist/worktree.js +117 -0
  145. package/package.json +40 -6
  146. package/public/qr.js +464 -0
@@ -0,0 +1,647 @@
1
+ // SPDX-License-Identifier: FSL-1.1-ALv2
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ // Universal Agent Harness — Phase 4: structured chat fidelity for CLI agents.
4
+ //
5
+ // Governance (Phases 1–3) already works for ANY agent because it wraps effects,
6
+ // not chat. This adds the *fidelity* layer: turn a CLI agent's structured stdout
7
+ // into the same normalized RuntimeEvents the PWA renders for Pi/Claude (streaming
8
+ // assistant text, tool cards, a proper transcript) instead of one opaque blob.
9
+ //
10
+ // A CliParser is a small, stateful, PURE mapper from stdout lines → events. It
11
+ // needs zero knowledge of the daemon. The two shipped here:
12
+ //
13
+ // * bivyProtocol — the UNIVERSAL path. Any agent that emits bivy-agent-
14
+ // protocol JSONL (natively or via a ~30-line shim) gets full
15
+ // fidelity. This is what makes "any agent" real: you never
16
+ // need a bespoke parser, just this one line format.
17
+ // * claudeStreamJson — parses Claude Code CLI `--output-format stream-json`, as
18
+ // a worked example of adapting a native JSON mode. (Format
19
+ // per Anthropic's docs; validate against your CLI version.)
20
+ //
21
+ // Adding an agent = add a parser to CLI_PARSERS (data), never new daemon code.
22
+ // Unit-tested with fixtures in test/runtime-cli-parsers.test.ts.
23
+ /**
24
+ * Best-effort token usage from an agent's structured output. Coding CLIs report
25
+ * tokens under many key spellings (OpenAI `prompt_tokens`, Anthropic/Codex
26
+ * `input_tokens`, Gemini `promptTokenCount`, Goose `total_tokens`, …), so we scan
27
+ * for all of them rather than hard-coding one shape. Returns undefined when the
28
+ * object carries no recognizable counts, keeping usage honestly absent.
29
+ */
30
+ export function extractTokenUsage(raw) {
31
+ if (!raw || typeof raw !== "object")
32
+ return undefined;
33
+ const o = raw;
34
+ const num = (...keys) => {
35
+ for (const k of keys) {
36
+ const v = o[k];
37
+ if (typeof v === "number" && Number.isFinite(v))
38
+ return v;
39
+ }
40
+ return undefined;
41
+ };
42
+ const input = num("input_tokens", "prompt_tokens", "promptTokenCount", "inputTokens", "input", "prompt");
43
+ const output = num("output_tokens", "completion_tokens", "candidatesTokenCount", "outputTokens", "output", "completion");
44
+ const cacheRead = num("cache_read_input_tokens", "cached_input_tokens", "cachedContentTokenCount", "cachedInputTokens", "cache_read");
45
+ const cacheWrite = num("cache_creation_input_tokens", "cache_write_input_tokens", "cacheWriteInputTokens", "cache_write");
46
+ const total = num("total_tokens", "totalTokenCount", "totalTokens", "total") ?? ((input ?? 0) + (output ?? 0) || undefined);
47
+ const costUsd = num("cost_usd", "total_cost", "costUsd");
48
+ if (input === undefined && output === undefined && total === undefined && costUsd === undefined)
49
+ return undefined;
50
+ const tokens = {};
51
+ if (input !== undefined)
52
+ tokens.input = input;
53
+ if (output !== undefined)
54
+ tokens.output = output;
55
+ if (cacheRead !== undefined)
56
+ tokens.cacheRead = cacheRead;
57
+ if (cacheWrite !== undefined)
58
+ tokens.cacheWrite = cacheWrite;
59
+ if (total !== undefined)
60
+ tokens.total = total;
61
+ const snapshot = {};
62
+ if (Object.keys(tokens).length)
63
+ snapshot.tokens = tokens;
64
+ if (costUsd !== undefined)
65
+ snapshot.costUsd = costUsd;
66
+ return Object.keys(snapshot).length ? snapshot : undefined;
67
+ }
68
+ /** Shared assistant-turn accumulator: text + tool_use/tool_result blocks, and
69
+ * the "did we emit message_start yet" latch. Both parsers build on this so the
70
+ * persisted transcript shape matches the live-streaming shape (see protocol.ts). */
71
+ class TurnAccumulator {
72
+ text = "";
73
+ started = false;
74
+ ended = false;
75
+ reasoning = "";
76
+ usageSnapshot;
77
+ toolUses = [];
78
+ toolResults = [];
79
+ out = [];
80
+ ensureStart(events) {
81
+ if (!this.started) {
82
+ this.started = true;
83
+ events.push({ type: "message_start", message: { role: "assistant", content: "" } });
84
+ }
85
+ }
86
+ /**
87
+ * Surface the agent's reasoning/thinking stream as a live intermediate block —
88
+ * the same `{type:"thinking"}` content shape the daemon renders for Pi/Claude
89
+ * (server.ts persistIntermediateFromEvent), so a CLI agent's chain-of-thought
90
+ * shows in the same collapsible sidecar. Kept out of `text` (the answer) and out
91
+ * of persisted history — it's display-only.
92
+ */
93
+ appendReasoning(text, events) {
94
+ if (!text)
95
+ return;
96
+ this.reasoning += text;
97
+ events.push({ type: "message_update", message: { role: "assistant", content: [{ type: "thinking", thinking: this.reasoning }] } });
98
+ }
99
+ /** Merge a best-effort usage snapshot parsed from the agent's output. */
100
+ addUsage(snapshot) {
101
+ if (snapshot)
102
+ this.usageSnapshot = snapshot;
103
+ }
104
+ appendText(text, events) {
105
+ if (!text)
106
+ return;
107
+ this.ensureStart(events);
108
+ this.text += text;
109
+ events.push({ type: "message_update", message: { role: "assistant", content: this.text } });
110
+ }
111
+ addToolUse(id, name, input, events) {
112
+ this.toolUses.push({ type: "tool_use", id, name, input: input ?? {} });
113
+ events.push({ type: "tool_call", toolName: name, input, toolCallId: id });
114
+ }
115
+ addToolResult(toolUseId, name, content, events) {
116
+ this.toolResults.push({ type: "tool_result", tool_use_id: toolUseId, content: content ?? "" });
117
+ events.push({ type: "tool_result", toolName: name, result: { toolCallId: toolUseId, content } });
118
+ }
119
+ /** Finalize the turn: emit message_end/turn_end/agent_end and record history. */
120
+ finish(events) {
121
+ if (this.ended)
122
+ return;
123
+ this.ended = true;
124
+ const message = { role: "assistant", content: this.text };
125
+ if (this.toolUses.length || this.toolResults.length) {
126
+ const content = [];
127
+ if (this.text)
128
+ content.push({ type: "text", text: this.text });
129
+ content.push(...this.toolUses);
130
+ if (content.length)
131
+ this.out.push({ role: "assistant", content });
132
+ if (this.toolResults.length)
133
+ this.out.push({ role: "user", content: this.toolResults });
134
+ }
135
+ else if (this.text) {
136
+ this.out.push(message);
137
+ }
138
+ events.push({ type: "message_end", message });
139
+ events.push({ type: "turn_end" });
140
+ events.push({ type: "agent_end" });
141
+ }
142
+ history() {
143
+ return this.out;
144
+ }
145
+ }
146
+ /** Parser for the bivy-agent-protocol JSONL event vocabulary (the universal path). */
147
+ export function bivyProtocolParser() {
148
+ const acc = new TurnAccumulator();
149
+ return {
150
+ onLine(line) {
151
+ const events = [];
152
+ const trimmed = line.trim();
153
+ if (!trimmed)
154
+ return events;
155
+ let msg;
156
+ try {
157
+ msg = JSON.parse(trimmed);
158
+ }
159
+ catch {
160
+ return events; // ignore non-JSON noise (banners etc.)
161
+ }
162
+ const type = String(msg.type ?? "");
163
+ if (type === "message.delta") {
164
+ acc.appendText(String(msg.text ?? ""), events);
165
+ }
166
+ else if (type === "session.status") {
167
+ const status = String(msg.status ?? "");
168
+ if (status === "working")
169
+ events.push({ type: "turn_start" });
170
+ }
171
+ else if (type === "tool.call") {
172
+ acc.addToolUse(String(msg.toolCallId ?? msg.id ?? ""), String(msg.name ?? "tool"), msg.input, events);
173
+ }
174
+ else if (type === "tool.result") {
175
+ acc.addToolResult(String(msg.toolCallId ?? msg.tool_use_id ?? msg.id ?? ""), String(msg.name ?? "tool"), msg.result ?? msg.output ?? msg.content ?? msg.text, events);
176
+ }
177
+ else if (type === "session.done") {
178
+ acc.finish(events);
179
+ }
180
+ else if (type === "session.error") {
181
+ events.push({ type: "session.error", error: String(msg.error ?? "Agent error") });
182
+ acc.finish(events);
183
+ }
184
+ return events;
185
+ },
186
+ onClose(code, stderr) {
187
+ const events = [];
188
+ if (!acc.ended) {
189
+ if (code && code !== 0 && stderr.trim())
190
+ events.push({ type: "session.error", error: stderr.trim().slice(-2000) });
191
+ acc.finish(events);
192
+ }
193
+ return events;
194
+ },
195
+ messages: () => acc.history(),
196
+ };
197
+ }
198
+ /** Parser for Claude Code CLI `--output-format stream-json`. */
199
+ export function claudeStreamJsonParser() {
200
+ const acc = new TurnAccumulator();
201
+ return {
202
+ onLine(line) {
203
+ const events = [];
204
+ const trimmed = line.trim();
205
+ if (!trimmed)
206
+ return events;
207
+ let msg;
208
+ try {
209
+ msg = JSON.parse(trimmed);
210
+ }
211
+ catch {
212
+ return events;
213
+ }
214
+ const type = String(msg.type ?? "");
215
+ if (type === "assistant") {
216
+ const content = msg.message?.content;
217
+ if (Array.isArray(content)) {
218
+ for (const block of content) {
219
+ const b = block;
220
+ if (b.type === "text")
221
+ acc.appendText(String(b.text ?? ""), events);
222
+ else if (b.type === "tool_use")
223
+ acc.addToolUse(String(b.id ?? ""), String(b.name ?? "tool"), b.input, events);
224
+ }
225
+ }
226
+ }
227
+ else if (type === "user") {
228
+ const content = msg.message?.content;
229
+ if (Array.isArray(content)) {
230
+ for (const block of content) {
231
+ const b = block;
232
+ if (b.type === "tool_result")
233
+ acc.addToolResult(String(b.tool_use_id ?? ""), "tool", b.content, events);
234
+ }
235
+ }
236
+ }
237
+ else if (type === "result") {
238
+ // Final line. If the assistant text never streamed (non-partial mode),
239
+ // seed it from the result string so history isn't empty.
240
+ if (!acc.text && typeof msg.result === "string")
241
+ acc.appendText(msg.result, events);
242
+ acc.finish(events);
243
+ }
244
+ return events;
245
+ },
246
+ onClose(code, stderr) {
247
+ const events = [];
248
+ if (!acc.ended) {
249
+ if (code && code !== 0 && stderr.trim())
250
+ events.push({ type: "session.error", error: stderr.trim().slice(-2000) });
251
+ acc.finish(events);
252
+ }
253
+ return events;
254
+ },
255
+ messages: () => acc.history(),
256
+ };
257
+ }
258
+ /**
259
+ * Parser for Codex `codex exec --json`. Real event model (captured from
260
+ * codex-cli 0.142): a thread with turns, each turn a stream of typed items.
261
+ * {"type":"thread.started","thread_id":…}
262
+ * {"type":"turn.started"} / {"type":"turn.completed"} / {"type":"turn.failed","error":{message}}
263
+ * {"type":"item.completed","item":{"id":…,"type":"agent_message"|"reasoning"|
264
+ * "command_execution"|"mcp_tool_call"|"file_change"|"error", …}}
265
+ * {"type":"error","message":…} ← transient reconnect noise (non-fatal)
266
+ */
267
+ export function codexJsonParser() {
268
+ const acc = new TurnAccumulator();
269
+ const handleItem = (item, events) => {
270
+ const id = String(item.id ?? "");
271
+ switch (String(item.type ?? "")) {
272
+ case "agent_message":
273
+ acc.appendText(String(item.text ?? ""), events);
274
+ break;
275
+ case "command_execution":
276
+ acc.addToolUse(id, "shell", { command: item.command ?? "" }, events);
277
+ if (item.aggregated_output != null || item.exit_code != null) {
278
+ acc.addToolResult(id, "shell", item.aggregated_output ?? "", events);
279
+ }
280
+ break;
281
+ case "mcp_tool_call":
282
+ acc.addToolUse(id, String(item.tool ?? item.server ?? "mcp"), item.arguments ?? item.input, events);
283
+ if (item.result != null)
284
+ acc.addToolResult(id, String(item.tool ?? "mcp"), item.result, events);
285
+ break;
286
+ case "file_change":
287
+ acc.addToolUse(id, "apply_patch", { changes: item.changes ?? item }, events);
288
+ break;
289
+ case "reasoning": {
290
+ // Codex reasoning items carry either `text` or a `summary` (string or an
291
+ // array of {text}) — surface whichever is present as a thinking stream.
292
+ const text = typeof item.text === "string"
293
+ ? item.text
294
+ : Array.isArray(item.summary)
295
+ ? item.summary.map((s) => (typeof s === "string" ? s : String(s?.text ?? ""))).join("")
296
+ : typeof item.summary === "string"
297
+ ? item.summary
298
+ : "";
299
+ acc.appendReasoning(text, events);
300
+ break;
301
+ }
302
+ case "error":
303
+ events.push({ type: "session.error", error: String(item.message ?? "Codex error") });
304
+ break;
305
+ // "todo_list", "web_search" — omitted from the transcript.
306
+ }
307
+ };
308
+ return {
309
+ onLine(line) {
310
+ const events = [];
311
+ const trimmed = line.trim();
312
+ if (!trimmed || trimmed[0] !== "{")
313
+ return events; // skip tracing/log noise
314
+ let msg;
315
+ try {
316
+ msg = JSON.parse(trimmed);
317
+ }
318
+ catch {
319
+ return events;
320
+ }
321
+ switch (String(msg.type ?? "")) {
322
+ case "turn.started":
323
+ events.push({ type: "turn_start" });
324
+ break;
325
+ case "item.completed":
326
+ if (msg.item && typeof msg.item === "object")
327
+ handleItem(msg.item, events);
328
+ break;
329
+ case "token_count":
330
+ // Codex streams running token counts as their own event.
331
+ acc.addUsage(extractTokenUsage(msg.info ?? msg.usage));
332
+ break;
333
+ case "turn.completed":
334
+ // `usage` (when present) rides the terminal turn.completed event.
335
+ acc.addUsage(extractTokenUsage(msg.usage));
336
+ acc.finish(events);
337
+ break;
338
+ case "turn.failed":
339
+ events.push({ type: "session.error", error: String(msg.error?.message ?? "Codex turn failed") });
340
+ acc.finish(events);
341
+ break;
342
+ // top-level "error" = transient reconnect noise; ignore (turn.failed is fatal).
343
+ }
344
+ return events;
345
+ },
346
+ onClose(code, stderr) {
347
+ const events = [];
348
+ if (!acc.ended) {
349
+ if (code && code !== 0 && stderr.trim())
350
+ events.push({ type: "session.error", error: stderr.trim().slice(-2000) });
351
+ acc.finish(events);
352
+ }
353
+ return events;
354
+ },
355
+ messages: () => acc.history(),
356
+ usage: () => acc.usageSnapshot,
357
+ };
358
+ }
359
+ /**
360
+ * Parser for Goose `goose run --output-format stream-json`. Real shape (captured
361
+ * from goose 1.41): a stream of message envelopes then a completion.
362
+ * {"type":"message","message":{"role":"assistant","content":[
363
+ * {"type":"text","text":…} | {"type":"toolRequest",…} | {"type":"toolResponse",…}]}}
364
+ * {"type":"complete","total_tokens":…}
365
+ */
366
+ export function gooseStreamJsonParser() {
367
+ const acc = new TurnAccumulator();
368
+ return {
369
+ onLine(line) {
370
+ const events = [];
371
+ const trimmed = line.trim();
372
+ if (!trimmed || trimmed[0] !== "{")
373
+ return events; // skip the ascii banner
374
+ let msg;
375
+ try {
376
+ msg = JSON.parse(trimmed);
377
+ }
378
+ catch {
379
+ return events;
380
+ }
381
+ const type = String(msg.type ?? "");
382
+ if (type === "message") {
383
+ const message = (msg.message ?? {});
384
+ // Only surface assistant content; user/tool echoes are already in the transcript.
385
+ const content = Array.isArray(message.content) ? message.content : [];
386
+ for (const raw of content) {
387
+ const block = (raw ?? {});
388
+ const bt = String(block.type ?? "");
389
+ if (bt === "text") {
390
+ if (message.role !== "user")
391
+ acc.appendText(String(block.text ?? ""), events);
392
+ }
393
+ else if (bt === "toolRequest" || bt === "frontendToolRequest") {
394
+ const call = (block.toolCall ?? block.tool_call ?? block);
395
+ acc.addToolUse(String(block.id ?? ""), String(call.name ?? block.name ?? "tool"), call.arguments ?? call.args ?? block.arguments, events);
396
+ }
397
+ else if (bt === "toolResponse") {
398
+ const result = (block.toolResult ?? block.tool_result ?? block.result);
399
+ acc.addToolResult(String(block.id ?? ""), "tool", result, events);
400
+ }
401
+ }
402
+ }
403
+ else if (type === "complete") {
404
+ // `complete` carries the turn's token totals (e.g. total_tokens).
405
+ acc.addUsage(extractTokenUsage(msg));
406
+ acc.finish(events);
407
+ }
408
+ return events;
409
+ },
410
+ onClose(code, stderr) {
411
+ const events = [];
412
+ if (!acc.ended) {
413
+ if (code && code !== 0 && stderr.trim())
414
+ events.push({ type: "session.error", error: stderr.trim().slice(-2000) });
415
+ acc.finish(events);
416
+ }
417
+ return events;
418
+ },
419
+ messages: () => acc.history(),
420
+ usage: () => acc.usageSnapshot,
421
+ };
422
+ }
423
+ /**
424
+ * Parser for Gemini `gemini -o json`. Real shape (captured from gemini-cli 0.49):
425
+ * ONE (pretty-printed) JSON object at the end — not a stream — of the form
426
+ * { "session_id": …, "response": "…assistant text…", "stats": {…}, "error"?: {message} }
427
+ * We accumulate all stdout and parse it once at close.
428
+ */
429
+ export function geminiJsonParser() {
430
+ const acc = new TurnAccumulator();
431
+ let raw = "";
432
+ return {
433
+ onLine(line) {
434
+ raw += `${line}\n`;
435
+ return [];
436
+ },
437
+ onClose(code, stderr) {
438
+ const events = [];
439
+ let obj;
440
+ try {
441
+ obj = JSON.parse(raw.trim());
442
+ }
443
+ catch {
444
+ obj = undefined;
445
+ }
446
+ if (obj && obj.error?.message) {
447
+ events.push({ type: "session.error", error: String(obj.error.message) });
448
+ }
449
+ else if (obj && typeof obj.response === "string") {
450
+ acc.appendText(obj.response, events);
451
+ }
452
+ else if (code && code !== 0 && stderr.trim()) {
453
+ events.push({ type: "session.error", error: stderr.trim().slice(-2000) });
454
+ }
455
+ else if (raw.trim()) {
456
+ // Not the expected JSON envelope — surface the text so nothing is lost.
457
+ acc.appendText(raw.trim(), events);
458
+ }
459
+ // Token counts live under `stats` (shape varies by version — extract defensively).
460
+ if (obj)
461
+ acc.addUsage(extractTokenUsage(obj.stats) ?? extractTokenUsage(obj.stats?.tokens));
462
+ acc.finish(events);
463
+ return events;
464
+ },
465
+ messages: () => acc.history(),
466
+ usage: () => acc.usageSnapshot,
467
+ };
468
+ }
469
+ /**
470
+ * Pull a plain-text chunk out of a heterogeneous streaming-JSON event, covering
471
+ * the shapes the popular agent CLIs emit (Claude/ACP assistant blocks, OpenAI
472
+ * chat deltas, and flat `{text|content|response|…}` fields). Returns "" when the
473
+ * event carries no assistant text (a control frame), so the caller can ignore it.
474
+ */
475
+ function textFromStreamEvent(msg) {
476
+ // Claude / ACP assistant shape: { message: { content: [{type:"text",text}] } }.
477
+ const mc = msg.message?.content;
478
+ if (Array.isArray(mc)) {
479
+ return mc.map((b) => (b && typeof b === "object" && typeof b.text === "string" ? b.text : "")).join("");
480
+ }
481
+ if (typeof mc === "string")
482
+ return mc;
483
+ // OpenAI-style delta: { delta: { content|text } } or { choices:[{delta:{content}}] }.
484
+ const delta = msg.delta;
485
+ if (delta) {
486
+ if (typeof delta.text === "string")
487
+ return delta.text;
488
+ if (typeof delta.content === "string")
489
+ return delta.content;
490
+ }
491
+ const choices = msg.choices;
492
+ if (Array.isArray(choices)) {
493
+ return choices.map((c) => (typeof c?.delta?.content === "string" ? c.delta.content : typeof c?.text === "string" ? c.text : "")).join("");
494
+ }
495
+ // Flat string fields, in priority order.
496
+ for (const k of ["text", "content", "response", "message", "output", "chunk"]) {
497
+ const v = msg[k];
498
+ if (typeof v === "string")
499
+ return v;
500
+ }
501
+ return "";
502
+ }
503
+ // Event `type` values that mean "the turn is finished" across the various CLIs.
504
+ const STREAM_TERMINALS = new Set(["result", "done", "complete", "completed", "turn.completed", "session.done", "message_stop", "response.completed", "final"]);
505
+ /**
506
+ * A TOLERANT line-delimited JSON parser for CLIs whose `--stream-json` /
507
+ * `--format json` streaming vocabularies we haven't pinned exactly (Amp, Cursor,
508
+ * …). It extracts assistant text and token usage from the common shapes, ignores
509
+ * control frames it doesn't recognize, and — crucially — if a run produced NO
510
+ * structured text (e.g. the flag was wrong and the CLI printed plain text, or the
511
+ * schema is entirely unfamiliar), it surfaces the raw output at close so enabling
512
+ * it can never LOSE the reply (worst case it degrades to the dumb-pipe view).
513
+ */
514
+ export function genericStreamJsonParser() {
515
+ const acc = new TurnAccumulator();
516
+ let sawText = false;
517
+ let rawBuffer = "";
518
+ return {
519
+ onLine(line) {
520
+ const events = [];
521
+ const trimmed = line.trim();
522
+ if (!trimmed)
523
+ return events;
524
+ let msg;
525
+ try {
526
+ msg = JSON.parse(trimmed);
527
+ }
528
+ catch {
529
+ rawBuffer += `${line}\n`; // non-JSON banner/line — hold for the fallback
530
+ return events;
531
+ }
532
+ acc.addUsage(extractTokenUsage(msg.usage ?? msg.stats ?? msg));
533
+ const type = String(msg.type ?? "");
534
+ if (msg.error && !type.includes("delta")) {
535
+ const m = msg.error.message ?? msg.error;
536
+ events.push({ type: "session.error", error: String(m) });
537
+ }
538
+ const text = textFromStreamEvent(msg);
539
+ if (text && !STREAM_TERMINALS.has(type)) {
540
+ acc.appendText(text, events);
541
+ sawText = true;
542
+ }
543
+ if (STREAM_TERMINALS.has(type)) {
544
+ if (!sawText && text) {
545
+ acc.appendText(text, events);
546
+ sawText = true;
547
+ }
548
+ acc.finish(events);
549
+ }
550
+ return events;
551
+ },
552
+ onClose(code, stderr) {
553
+ const events = [];
554
+ if (!acc.ended) {
555
+ // Nothing structured came through: fall back to whatever the CLI printed so
556
+ // the turn is never silently empty.
557
+ if (!sawText && rawBuffer.trim())
558
+ acc.appendText(rawBuffer.trim(), events);
559
+ else if (!sawText && code && code !== 0 && stderr.trim())
560
+ events.push({ type: "session.error", error: stderr.trim().slice(-2000) });
561
+ acc.finish(events);
562
+ }
563
+ return events;
564
+ },
565
+ messages: () => acc.history(),
566
+ usage: () => acc.usageSnapshot,
567
+ };
568
+ }
569
+ /**
570
+ * A TOLERANT final-object JSON parser for CLIs that buffer their reply and print a
571
+ * single JSON object at exit (`--format json`). It extracts the assistant text
572
+ * from whichever of the common fields is present (`response`/`result`/`text`/
573
+ * `content`/`message`/`output`) and token usage from anywhere, and falls back to
574
+ * the raw output if the object isn't a shape it recognizes — same never-lose-the-
575
+ * reply guarantee as the streaming parser.
576
+ */
577
+ export function genericJsonParser() {
578
+ const acc = new TurnAccumulator();
579
+ let raw = "";
580
+ return {
581
+ onLine(line) {
582
+ raw += `${line}\n`;
583
+ return [];
584
+ },
585
+ onClose(code, stderr) {
586
+ const events = [];
587
+ let obj;
588
+ try {
589
+ obj = JSON.parse(raw.trim());
590
+ }
591
+ catch {
592
+ obj = undefined;
593
+ }
594
+ const errMsg = obj && obj.error?.message;
595
+ if (errMsg) {
596
+ events.push({ type: "session.error", error: String(errMsg) });
597
+ }
598
+ else if (obj) {
599
+ let text = "";
600
+ for (const k of ["response", "result", "text", "content", "output", "message"]) {
601
+ const v = obj[k];
602
+ if (typeof v === "string") {
603
+ text = v;
604
+ break;
605
+ }
606
+ if (Array.isArray(v)) {
607
+ text = v.map((b) => (typeof b === "string" ? b : String(b?.text ?? ""))).join("");
608
+ if (text)
609
+ break;
610
+ }
611
+ }
612
+ if (text)
613
+ acc.appendText(text, events);
614
+ else if (raw.trim())
615
+ acc.appendText(raw.trim(), events); // unfamiliar shape → raw
616
+ acc.addUsage(extractTokenUsage(obj.usage ?? obj.stats ?? obj) ?? extractTokenUsage(obj.stats?.tokens));
617
+ }
618
+ else if (code && code !== 0 && stderr.trim()) {
619
+ events.push({ type: "session.error", error: stderr.trim().slice(-2000) });
620
+ }
621
+ else if (raw.trim()) {
622
+ acc.appendText(raw.trim(), events);
623
+ }
624
+ acc.finish(events);
625
+ return events;
626
+ },
627
+ messages: () => acc.history(),
628
+ usage: () => acc.usageSnapshot,
629
+ };
630
+ }
631
+ /** Registry: parser id → factory. Adding an agent's fidelity = add a line here. */
632
+ export const CLI_PARSERS = {
633
+ "bivy-protocol": bivyProtocolParser,
634
+ "claude-stream-json": claudeStreamJsonParser,
635
+ "codex-json": codexJsonParser,
636
+ "goose-stream-json": gooseStreamJsonParser,
637
+ "gemini-json": geminiJsonParser,
638
+ // Tolerant, format-agnostic parsers for CLIs whose JSON vocabularies we haven't
639
+ // pinned exactly yet (opt-in per agent via a spec parserId; see CLI_AGENT_SPECS).
640
+ "generic-stream-json": genericStreamJsonParser,
641
+ "generic-json": genericJsonParser,
642
+ };
643
+ export function parserFactoryFor(id) {
644
+ if (!id)
645
+ return undefined;
646
+ return CLI_PARSERS[id];
647
+ }