@borgee/agents-host 0.2.74 → 0.2.94

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 (82) hide show
  1. package/README.md +11 -24
  2. package/dist/agents-host.d.ts +6 -4
  3. package/dist/agents-host.js +127 -16
  4. package/dist/background-runs.d.ts +24 -0
  5. package/dist/background-runs.js +184 -0
  6. package/dist/chat/chat-control-plane.d.ts +7 -1
  7. package/dist/chat/sdk-chat-control-plane.d.ts +7 -2
  8. package/dist/chat/sdk-chat-control-plane.js +22 -0
  9. package/dist/context/claude-file-brief.js +3 -3
  10. package/dist/context/injection.d.ts +18 -12
  11. package/dist/context/injection.js +53 -53
  12. package/dist/context/prompt.js +10 -10
  13. package/dist/context/resolved-working-folder.d.ts +3 -0
  14. package/dist/context/resolved-working-folder.js +106 -0
  15. package/dist/context/turn-preparation.js +1 -1
  16. package/dist/managed-daemon.js +30 -30
  17. package/dist/plugin-sdk.js +315 -29
  18. package/dist/plugin-sdk.js.map +3 -3
  19. package/dist/policy/authorization-audit.d.ts +1 -1
  20. package/dist/policy/copilot-permission.d.ts +9 -0
  21. package/dist/policy/copilot-permission.js +120 -1
  22. package/dist/progress-to-activity.d.ts +1 -1
  23. package/dist/progress-to-activity.js +1 -0
  24. package/dist/providers/claude/adapter.d.ts +2 -1
  25. package/dist/providers/claude/adapter.js +36 -1
  26. package/dist/providers/claude/background-run-observer.d.ts +32 -0
  27. package/dist/providers/claude/background-run-observer.js +381 -0
  28. package/dist/providers/claude/cli-client.d.ts +12 -0
  29. package/dist/providers/claude/cli-client.js +453 -72
  30. package/dist/providers/claude/foreground-handoff.d.ts +4 -0
  31. package/dist/providers/claude/foreground-handoff.js +45 -0
  32. package/dist/providers/claude/task-cancellation-protocol.d.ts +14 -0
  33. package/dist/providers/claude/task-cancellation-protocol.js +21 -0
  34. package/dist/providers/codex/adapter.d.ts +1 -1
  35. package/dist/providers/codex/adapter.js +7 -0
  36. package/dist/providers/codex/cli-client.js +8 -7
  37. package/dist/providers/codex/project-doc.js +6 -6
  38. package/dist/providers/copilot/activity-metadata.d.ts +3 -0
  39. package/dist/providers/copilot/activity-metadata.js +19 -0
  40. package/dist/providers/copilot/adapter.d.ts +6 -2
  41. package/dist/providers/copilot/adapter.js +27 -1
  42. package/dist/providers/copilot/cli-client.d.ts +37 -10
  43. package/dist/providers/copilot/cli-client.js +756 -122
  44. package/dist/providers/copilot/sdk-session.d.ts +149 -0
  45. package/dist/providers/copilot/sdk-session.js +981 -0
  46. package/dist/providers/create-provider.js +33 -0
  47. package/dist/providers/provider-adapter.d.ts +24 -1
  48. package/dist/providers/provider-adapter.js +17 -0
  49. package/dist/types.d.ts +40 -11
  50. package/dist/vendor/claude-agent-acp/LICENSE +191 -0
  51. package/dist/vendor/claude-agent-acp/NOTICE +8 -0
  52. package/dist/vendor/claude-agent-acp/dist/acp-agent.d.ts +1017 -0
  53. package/dist/vendor/claude-agent-acp/dist/acp-agent.d.ts.map +1 -0
  54. package/dist/vendor/claude-agent-acp/dist/acp-agent.js +6305 -0
  55. package/dist/vendor/claude-agent-acp/dist/borgee-foreground-handoff-bridge.js +322 -0
  56. package/dist/vendor/claude-agent-acp/dist/borgee-task-cancellation-bridge.js +101 -0
  57. package/dist/vendor/claude-agent-acp/dist/borgee-task-lifecycle-bridge.js +58 -0
  58. package/dist/vendor/claude-agent-acp/dist/elicitation.d.ts +130 -0
  59. package/dist/vendor/claude-agent-acp/dist/elicitation.d.ts.map +1 -0
  60. package/dist/vendor/claude-agent-acp/dist/elicitation.js +304 -0
  61. package/dist/vendor/claude-agent-acp/dist/index.d.ts +3 -0
  62. package/dist/vendor/claude-agent-acp/dist/index.d.ts.map +1 -0
  63. package/dist/vendor/claude-agent-acp/dist/index.js +75 -0
  64. package/dist/vendor/claude-agent-acp/dist/lib.d.ts +6 -0
  65. package/dist/vendor/claude-agent-acp/dist/lib.d.ts.map +1 -0
  66. package/dist/vendor/claude-agent-acp/dist/lib.js +5 -0
  67. package/dist/vendor/claude-agent-acp/dist/settings.d.ts +68 -0
  68. package/dist/vendor/claude-agent-acp/dist/settings.d.ts.map +1 -0
  69. package/dist/vendor/claude-agent-acp/dist/settings.js +185 -0
  70. package/dist/vendor/claude-agent-acp/dist/tools.d.ts +102 -0
  71. package/dist/vendor/claude-agent-acp/dist/tools.d.ts.map +1 -0
  72. package/dist/vendor/claude-agent-acp/dist/tools.js +1000 -0
  73. package/dist/vendor/claude-agent-acp/dist/utils.d.ts +16 -0
  74. package/dist/vendor/claude-agent-acp/dist/utils.d.ts.map +1 -0
  75. package/dist/vendor/claude-agent-acp/dist/utils.js +81 -0
  76. package/dist/vendor/claude-agent-acp/package.json +85 -0
  77. package/package.json +15 -10
  78. package/skills/borgee-agent/references/task-properties.md +3 -3
  79. package/skills/borgee-agent/scripts/borgee-agent.mjs +1 -1
  80. package/skills/borgee-agent/scripts/borgee-agent.py +1 -1
  81. package/dist/context/resolved-workspace.d.ts +0 -3
  82. package/dist/context/resolved-workspace.js +0 -106
@@ -0,0 +1,1000 @@
1
+ // Modified by Borgee to forward Claude hook ownership.
2
+ import path from "node:path";
3
+ /**
4
+ * Convert an absolute file path to a project-relative path for display.
5
+ * Returns the original path if it's outside the project directory or if no cwd is provided.
6
+ */
7
+ export function toDisplayPath(filePath, cwd) {
8
+ if (!cwd)
9
+ return filePath;
10
+ const resolvedCwd = path.resolve(cwd);
11
+ const resolvedFile = path.resolve(filePath);
12
+ if (resolvedFile.startsWith(resolvedCwd + path.sep) || resolvedFile === resolvedCwd) {
13
+ return path.relative(resolvedCwd, resolvedFile);
14
+ }
15
+ return filePath;
16
+ }
17
+ export function toolInfoFromToolUse(toolUse, supportsTerminalOutput = false, cwd) {
18
+ const name = toolUse.name;
19
+ switch (name) {
20
+ case "Agent":
21
+ case "Task": {
22
+ const input = toolUse.input;
23
+ return {
24
+ title: input?.description ? input.description : "Task",
25
+ kind: "think",
26
+ content: input && "prompt" in input
27
+ ? [
28
+ {
29
+ type: "content",
30
+ content: { type: "text", text: input.prompt },
31
+ },
32
+ ]
33
+ : [],
34
+ };
35
+ }
36
+ case "Bash": {
37
+ const input = toolUse.input;
38
+ return {
39
+ title: input?.command ? input.command : "Terminal",
40
+ kind: "execute",
41
+ content: supportsTerminalOutput
42
+ ? [{ type: "terminal", terminalId: toolUse.id }]
43
+ : input && input.description
44
+ ? [
45
+ {
46
+ type: "content",
47
+ content: { type: "text", text: input.description },
48
+ },
49
+ ]
50
+ : [],
51
+ };
52
+ }
53
+ case "Read": {
54
+ const input = toolUse.input;
55
+ let limit = "";
56
+ if (input?.limit && input.limit > 0) {
57
+ limit = " (" + (input.offset ?? 1) + " - " + ((input.offset ?? 1) + input.limit - 1) + ")";
58
+ }
59
+ else if (input?.offset) {
60
+ limit = " (from line " + input.offset + ")";
61
+ }
62
+ const displayPath = input?.file_path ? toDisplayPath(input.file_path, cwd) : "File";
63
+ return {
64
+ title: "Read " + displayPath + limit,
65
+ kind: "read",
66
+ locations: input?.file_path
67
+ ? [
68
+ {
69
+ path: input.file_path,
70
+ line: input.offset ?? 1,
71
+ },
72
+ ]
73
+ : [],
74
+ content: [],
75
+ };
76
+ }
77
+ case "Write": {
78
+ const input = toolUse.input;
79
+ let content = [];
80
+ if (input && input.file_path) {
81
+ content = [
82
+ {
83
+ type: "diff",
84
+ path: input.file_path,
85
+ oldText: null,
86
+ newText: input.content,
87
+ },
88
+ ];
89
+ }
90
+ else if (input && input.content) {
91
+ content = [
92
+ {
93
+ type: "content",
94
+ content: { type: "text", text: input.content },
95
+ },
96
+ ];
97
+ }
98
+ const displayPath = input?.file_path ? toDisplayPath(input.file_path, cwd) : undefined;
99
+ return {
100
+ title: displayPath ? `Write ${displayPath}` : "Write",
101
+ kind: "edit",
102
+ content,
103
+ locations: input?.file_path ? [{ path: input.file_path }] : [],
104
+ };
105
+ }
106
+ case "Edit": {
107
+ const input = toolUse.input;
108
+ let content = [];
109
+ if (input && input.file_path && (input.old_string || input.new_string)) {
110
+ content = [
111
+ {
112
+ type: "diff",
113
+ path: input.file_path,
114
+ oldText: input.old_string || null,
115
+ newText: input.new_string ?? "",
116
+ },
117
+ ];
118
+ }
119
+ const displayPath = input?.file_path ? toDisplayPath(input.file_path, cwd) : undefined;
120
+ return {
121
+ title: displayPath ? `Edit ${displayPath}` : "Edit",
122
+ kind: "edit",
123
+ content,
124
+ locations: input?.file_path ? [{ path: input.file_path }] : [],
125
+ };
126
+ }
127
+ case "Glob": {
128
+ const input = toolUse.input;
129
+ let label = "Find";
130
+ if (input?.path) {
131
+ label += ` \`${input.path}\``;
132
+ }
133
+ if (input?.pattern) {
134
+ label += ` \`${input.pattern}\``;
135
+ }
136
+ return {
137
+ title: label,
138
+ kind: "search",
139
+ content: [],
140
+ locations: input?.path ? [{ path: input.path }] : [],
141
+ };
142
+ }
143
+ case "Grep": {
144
+ const input = toolUse.input;
145
+ let label = "grep";
146
+ if (input?.["-i"]) {
147
+ label += " -i";
148
+ }
149
+ if (input?.["-n"]) {
150
+ label += " -n";
151
+ }
152
+ if (input?.["-A"] !== undefined) {
153
+ label += ` -A ${input["-A"]}`;
154
+ }
155
+ if (input?.["-B"] !== undefined) {
156
+ label += ` -B ${input["-B"]}`;
157
+ }
158
+ if (input?.["-C"] !== undefined) {
159
+ label += ` -C ${input["-C"]}`;
160
+ }
161
+ if (input?.output_mode) {
162
+ switch (input.output_mode) {
163
+ case "files_with_matches":
164
+ label += " -l";
165
+ break;
166
+ case "count":
167
+ label += " -c";
168
+ break;
169
+ case "content":
170
+ default:
171
+ break;
172
+ }
173
+ }
174
+ if (input?.head_limit !== undefined) {
175
+ label += ` | head -${input.head_limit}`;
176
+ }
177
+ if (input?.glob) {
178
+ label += ` --include="${input.glob}"`;
179
+ }
180
+ if (input?.type) {
181
+ label += ` --type=${input.type}`;
182
+ }
183
+ if (input?.multiline) {
184
+ label += " -P";
185
+ }
186
+ if (input?.pattern) {
187
+ label += ` "${input.pattern}"`;
188
+ }
189
+ if (input?.path) {
190
+ label += ` ${input.path}`;
191
+ }
192
+ return {
193
+ title: label,
194
+ kind: "search",
195
+ content: [],
196
+ };
197
+ }
198
+ case "WebFetch": {
199
+ const input = toolUse.input;
200
+ return {
201
+ title: input?.url ? `Fetch ${input.url}` : "Fetch",
202
+ kind: "fetch",
203
+ content: input && input.prompt
204
+ ? [
205
+ {
206
+ type: "content",
207
+ content: { type: "text", text: input.prompt },
208
+ },
209
+ ]
210
+ : [],
211
+ };
212
+ }
213
+ case "WebSearch": {
214
+ const input = toolUse.input;
215
+ let label = input?.query ? `"${input.query}"` : "Web search";
216
+ if (input?.allowed_domains && input.allowed_domains.length > 0) {
217
+ label += ` (allowed: ${input.allowed_domains.join(", ")})`;
218
+ }
219
+ if (input?.blocked_domains && input.blocked_domains.length > 0) {
220
+ label += ` (blocked: ${input.blocked_domains.join(", ")})`;
221
+ }
222
+ return {
223
+ title: label,
224
+ kind: "fetch",
225
+ content: [],
226
+ };
227
+ }
228
+ case "TodoWrite": {
229
+ const input = toolUse.input;
230
+ return {
231
+ title: Array.isArray(input?.todos)
232
+ ? `Update TODOs: ${input.todos.map((todo) => todo.content).join(", ")}`
233
+ : "Update TODOs",
234
+ kind: "think",
235
+ content: [],
236
+ };
237
+ }
238
+ case "ReportFindings": {
239
+ const input = toolUse.input;
240
+ const findings = input?.findings ?? [];
241
+ return {
242
+ title: findings.length === 0
243
+ ? "Report findings: none found"
244
+ : `Report ${findings.length} finding${findings.length === 1 ? "" : "s"}`,
245
+ kind: "think",
246
+ content: findings.map((finding) => ({
247
+ type: "content",
248
+ content: {
249
+ type: "text",
250
+ text: `**${finding.file}${finding.line ? `:${finding.line}` : ""}** — ${finding.summary}`,
251
+ },
252
+ })),
253
+ };
254
+ }
255
+ case "TaskCreate": {
256
+ const input = toolUse.input;
257
+ return {
258
+ title: input?.subject ? `Create task: ${input.subject}` : "Create task",
259
+ kind: "think",
260
+ content: [],
261
+ };
262
+ }
263
+ case "TaskUpdate": {
264
+ const input = toolUse.input;
265
+ return {
266
+ title: input?.subject ? `Update task: ${input.subject}` : "Update task",
267
+ kind: "think",
268
+ content: [],
269
+ };
270
+ }
271
+ case "TaskList": {
272
+ return {
273
+ title: "List tasks",
274
+ kind: "think",
275
+ content: [],
276
+ };
277
+ }
278
+ case "TaskGet": {
279
+ return {
280
+ title: "Get task",
281
+ kind: "think",
282
+ content: [],
283
+ };
284
+ }
285
+ case "ExitPlanMode": {
286
+ const planInput = toolUse.input;
287
+ return {
288
+ title: "Ready to code?",
289
+ kind: "switch_mode",
290
+ content: planInput?.plan
291
+ ? [{ type: "content", content: { type: "text", text: planInput.plan } }]
292
+ : [],
293
+ };
294
+ }
295
+ case "AskUserQuestion": {
296
+ const input = toolUse.input;
297
+ const questions = Array.isArray(input?.questions) ? input.questions : [];
298
+ return {
299
+ title: questions.length === 1 && questions[0]?.question
300
+ ? questions[0].question
301
+ : "Asking for your input",
302
+ kind: "other",
303
+ content: questions
304
+ .filter((q) => typeof q?.question === "string")
305
+ .map((q) => ({
306
+ type: "content",
307
+ content: { type: "text", text: q.question },
308
+ })),
309
+ };
310
+ }
311
+ case "Other": {
312
+ const input = toolUse.input;
313
+ let output;
314
+ try {
315
+ output = JSON.stringify(input, null, 2);
316
+ }
317
+ catch {
318
+ output = typeof input === "string" ? input : "{}";
319
+ }
320
+ return {
321
+ title: name || "Unknown Tool",
322
+ kind: "other",
323
+ content: [
324
+ {
325
+ type: "content",
326
+ content: {
327
+ type: "text",
328
+ text: `\`\`\`json\n${output}\`\`\``,
329
+ },
330
+ },
331
+ ],
332
+ };
333
+ }
334
+ default:
335
+ return {
336
+ title: name || "Unknown Tool",
337
+ kind: "other",
338
+ content: [],
339
+ };
340
+ }
341
+ }
342
+ /**
343
+ * Narrow the untyped message-level `tool_use_result` toward a per-tool Output
344
+ * shape: rejects everything but a plain non-null object (arrays pass a bare
345
+ * `typeof === "object"` check, so they're excluded here). The returned value
346
+ * is only *nominally* typed — it arrives over the wire from arbitrary CLI
347
+ * versions, so each caller must still guard the specific fields it reads
348
+ * before trusting them.
349
+ */
350
+ function structuredResult(toolUseResult) {
351
+ return toolUseResult !== null &&
352
+ typeof toolUseResult === "object" &&
353
+ !Array.isArray(toolUseResult)
354
+ ? toolUseResult
355
+ : undefined;
356
+ }
357
+ /**
358
+ * Strip the model-directed trailer from a raw Agent/Task tool_result text:
359
+ * a `<usage>…</usage>` totals block and/or an
360
+ * `agentId: <id> (use SendMessage …)` continuation line at the end of the
361
+ * text. Both patterns are tail-anchored and independent (older CLIs emit
362
+ * variants with only one of them), so a format change makes them stop
363
+ * matching rather than mangle the report.
364
+ */
365
+ function stripAgentTrailer(text) {
366
+ return stripAgentIdLine(stripUsageBlock(text));
367
+ }
368
+ const USAGE_OPEN = "<usage>";
369
+ const USAGE_CLOSE = "</usage>";
370
+ /** Remove a trailing `<usage>…</usage>` block, plus trailing whitespace and
371
+ * one preceding newline. Matches from the *last* `<usage>` so a report that
372
+ * merely mentions the marker earlier isn't truncated at the mention. */
373
+ function stripUsageBlock(text) {
374
+ const body = text.trimEnd();
375
+ if (!body.endsWith(USAGE_CLOSE)) {
376
+ return text;
377
+ }
378
+ const open = body.lastIndexOf(USAGE_OPEN, body.length - USAGE_CLOSE.length - USAGE_OPEN.length);
379
+ if (open === -1) {
380
+ return text;
381
+ }
382
+ return body.slice(0, open > 0 && body[open - 1] === "\n" ? open - 1 : open);
383
+ }
384
+ /** The continuation line, anchored to a whole line so the regex has a single
385
+ * start position and no ambiguous repetition (`[\w-]+` can't consume the
386
+ * following space, `[^)]*` can't consume the closing paren) — it runs in
387
+ * linear time on any input. */
388
+ const AGENT_ID_LINE = /^agentId: [\w-]+ \([^)]*\)$/;
389
+ /** Remove a final `agentId: <id> (…)` line, plus trailing whitespace and the
390
+ * newline that preceded the line. */
391
+ function stripAgentIdLine(text) {
392
+ const body = text.trimEnd();
393
+ const lineStart = body.lastIndexOf("\n") + 1;
394
+ if (!AGENT_ID_LINE.test(body.slice(lineStart))) {
395
+ return text;
396
+ }
397
+ return body.slice(0, Math.max(lineStart - 1, 0));
398
+ }
399
+ /** Apply {@link stripAgentTrailer} across a raw tool_result `content` (plain
400
+ * string or block array), leaving non-text blocks untouched. */
401
+ function stripAgentTrailerFromContent(content) {
402
+ if (typeof content === "string") {
403
+ return stripAgentTrailer(content);
404
+ }
405
+ if (Array.isArray(content)) {
406
+ return content.map((block) => block !== null &&
407
+ typeof block === "object" &&
408
+ block.type === "text" &&
409
+ typeof block.text === "string"
410
+ ? { ...block, text: stripAgentTrailer(block.text) }
411
+ : block);
412
+ }
413
+ return content;
414
+ }
415
+ export function toolUpdateFromToolResult(toolResult, toolUse, supportsTerminalOutput = false, toolUseResult) {
416
+ if ("is_error" in toolResult &&
417
+ toolResult.is_error &&
418
+ toolResult.content &&
419
+ toolResult.content.length > 0 &&
420
+ !(toolUse?.name === "Bash" && supportsTerminalOutput)) {
421
+ // Only return errors
422
+ return toAcpContentUpdate(toolResult.content, true);
423
+ }
424
+ // Shared raw-text fallback: renders the tool_result content the model saw.
425
+ // The structured cases below fall back to this when `tool_use_result` is
426
+ // absent or fails its shape guard (older CLIs, replayed sessions).
427
+ const rawContentUpdate = () => toAcpContentUpdate(toolResult.content, "is_error" in toolResult ? toolResult.is_error : false);
428
+ switch (toolUse?.name) {
429
+ case "Read": {
430
+ // The raw tool_result text is the model-facing view: line-numbered
431
+ // content plus any appended <system-reminder> blocks (malicious-code
432
+ // checks, memory staleness notes, …) that clients shouldn't see. The
433
+ // structured FileReadOutput carries the clean content — rebuild the
434
+ // line-numbered view from it. Non-text variants (image/notebook/pdf)
435
+ // fall back to the raw content blocks, which already render fine.
436
+ const structuredRead = structuredResult(toolUseResult);
437
+ if (structuredRead?.type === "text" &&
438
+ typeof structuredRead.file?.content === "string" &&
439
+ // An empty file has nothing to line-number; keep the raw view (the
440
+ // model-facing "file is empty" note) rather than a phantom blank line.
441
+ structuredRead.file.content.length > 0) {
442
+ // startLine is typed non-optional but defended anyway; a Read's
443
+ // `offset` input is the same 1-based starting line, so it beats a
444
+ // blind 1 when an emitter omits the field.
445
+ const startLine = structuredRead.file.startLine ??
446
+ toolUse.input?.offset ??
447
+ 1;
448
+ // A trailing newline is a line terminator, not an extra line — don't
449
+ // number a phantom empty line after it.
450
+ let numbered = structuredRead.file.content
451
+ .replace(/\n$/, "")
452
+ .split("\n")
453
+ .map((line, i) => `${startLine + i}\t${line}`)
454
+ .join("\n");
455
+ // The model-facing truncation banner doesn't survive reconstruction
456
+ // from file.content (the SDK flag exists for exactly this case) —
457
+ // re-establish it so a partial first page doesn't read as the whole
458
+ // file.
459
+ if (structuredRead.file.truncatedByTokenCap) {
460
+ const { numLines, totalLines } = structuredRead.file;
461
+ const detail = typeof numLines === "number" && typeof totalLines === "number"
462
+ ? `: showing ${numLines} of ${totalLines} lines`
463
+ : "";
464
+ numbered += `\n[File truncated${detail}]`;
465
+ }
466
+ return {
467
+ content: [
468
+ {
469
+ type: "content",
470
+ content: { type: "text", text: markdownEscape(numbered) },
471
+ },
472
+ ],
473
+ };
474
+ }
475
+ if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
476
+ return {
477
+ content: toolResult.content.map((content) => ({
478
+ type: "content",
479
+ content: content.type === "text"
480
+ ? {
481
+ type: "text",
482
+ text: markdownEscape(content.text),
483
+ }
484
+ : toAcpContentBlock(content, false),
485
+ })),
486
+ };
487
+ }
488
+ else if (typeof toolResult.content === "string" && toolResult.content.length > 0) {
489
+ return {
490
+ content: [
491
+ {
492
+ type: "content",
493
+ content: {
494
+ type: "text",
495
+ text: markdownEscape(toolResult.content),
496
+ },
497
+ },
498
+ ],
499
+ };
500
+ }
501
+ return {};
502
+ }
503
+ case "Bash": {
504
+ const result = toolResult.content;
505
+ // The terminal was announced under the tool_use's own id (see
506
+ // `toolInfoFromToolUse`), so key the output/exit metas off that: it is the
507
+ // id the client actually created a terminal for. `toolResult.tool_use_id`
508
+ // is the same value whenever present — the caller looks the tool_use up by
509
+ // it — so preferring `toolUse.id` only adds a source for the case where the
510
+ // result block carries no id at all. Anything that isn't a non-empty
511
+ // string is no id at all: `""` matches no terminal, and stringifying a
512
+ // present-but-undefined field would invent the literal `"undefined"`.
513
+ const terminalIdOf = (id) => typeof id === "string" && id.length > 0 ? id : undefined;
514
+ const terminalId = terminalIdOf(toolUse?.id) ??
515
+ terminalIdOf("tool_use_id" in toolResult ? toolResult.tool_use_id : undefined);
516
+ const isError = "is_error" in toolResult && toolResult.is_error;
517
+ // Extract output and exit code from either format:
518
+ // 1. The structured BashOutput (message-level tool_use_result): its
519
+ // stdout/stderr exclude the model-directed suffixes the raw text
520
+ // carries (stale-read hints, gh rate-limit hints, the
521
+ // persisted-output wrapper for too-large outputs — the interruption
522
+ // and truncation facts those carried are re-established from the
523
+ // structured flags below). Skipped for image output (the raw content
524
+ // array carries the actual image blocks) and backgrounded commands
525
+ // (the raw text carries the background-task notice; structured
526
+ // stdout may be empty).
527
+ // 2. BetaBashCodeExecutionResultBlock: { type: "bash_code_execution_result", stdout, stderr, return_code }
528
+ // 3. Plain string content from a regular tool_result
529
+ // 4. Array content (e.g. [{ type: "text", text: "..." }] for stdout,
530
+ // or [{ type: "image", source: {...} }] when the local Bash tool
531
+ // produces an image, e.g. piping a base64 data URI)
532
+ let output = "";
533
+ let exitCode = isError ? 1 : 0;
534
+ const structuredBash = structuredResult(toolUseResult);
535
+ if (structuredBash &&
536
+ typeof structuredBash.stdout === "string" &&
537
+ typeof structuredBash.stderr === "string" &&
538
+ !structuredBash.isImage &&
539
+ structuredBash.backgroundTaskId === undefined) {
540
+ output = [structuredBash.stdout, structuredBash.stderr].filter(Boolean).join("\n");
541
+ // Two raw-text notices don't survive the structured stdout/stderr —
542
+ // re-establish them so the client isn't shown a clean-looking result:
543
+ // the CLI appends its abort marker only to the model-facing text, and
544
+ // an aborted command isn't a success, so synthesize a failing exit
545
+ // code when the result wasn't already an error.
546
+ if (structuredBash.interrupted) {
547
+ output = [output, "[Command was aborted before completion]"].filter(Boolean).join("\n");
548
+ exitCode = 1;
549
+ }
550
+ // Structured stdout is clipped (~30k chars) when the full output was
551
+ // persisted to disk; without this note the clip is silent and the
552
+ // path to the full output is lost.
553
+ if (typeof structuredBash.persistedOutputPath === "string") {
554
+ const size = typeof structuredBash.persistedOutputSize === "number"
555
+ ? ` (${structuredBash.persistedOutputSize} bytes total)`
556
+ : "";
557
+ output = [
558
+ output,
559
+ `[Output truncated${size}: full output saved to ${structuredBash.persistedOutputPath}]`,
560
+ ]
561
+ .filter(Boolean)
562
+ .join("\n");
563
+ }
564
+ }
565
+ else if (result &&
566
+ typeof result === "object" &&
567
+ "type" in result &&
568
+ result.type === "bash_code_execution_result") {
569
+ const bashResult = result;
570
+ output = [bashResult.stdout, bashResult.stderr].filter(Boolean).join("\n");
571
+ exitCode = bashResult.return_code;
572
+ }
573
+ else if (typeof result === "string") {
574
+ output = result;
575
+ }
576
+ else if (Array.isArray(result) && result.length > 0) {
577
+ const textOnly = result.every((c) => c && typeof c === "object" && typeof c.text === "string");
578
+ if (textOnly) {
579
+ output = result.map((c) => c.text).join("\n");
580
+ }
581
+ else {
582
+ // Image (or mixed non-text) content. Binary payloads can't be
583
+ // streamed through the terminal-output _meta channel, so bypass
584
+ // it and surface the blocks as ACP content. This handles the
585
+ // local Bash tool's image output, which previously failed the
586
+ // text-only guard and was silently dropped.
587
+ return toAcpContentUpdate(result, isError);
588
+ }
589
+ }
590
+ // Without a terminal id there is nothing the client can reconcile these
591
+ // metas against, and emitting them anyway strands the output: a client that
592
+ // buffers output/exit for terminals it has not been told about (Zed keeps
593
+ // them in `pending_terminal_output`/`pending_terminal_exit`, drained only on
594
+ // a matching create) would hold them forever behind an id that never
595
+ // arrives, showing an empty terminal. Fall through to the code-block
596
+ // rendering below instead.
597
+ if (supportsTerminalOutput && terminalId !== undefined) {
598
+ return {
599
+ content: [{ type: "terminal", terminalId }],
600
+ _meta: {
601
+ terminal_info: {
602
+ terminal_id: terminalId,
603
+ },
604
+ terminal_output: {
605
+ terminal_id: terminalId,
606
+ data: output,
607
+ },
608
+ terminal_exit: {
609
+ terminal_id: terminalId,
610
+ exit_code: exitCode,
611
+ signal: null,
612
+ },
613
+ },
614
+ };
615
+ }
616
+ // Fallback: format output as a code block without terminal _meta
617
+ if (output.trim()) {
618
+ return {
619
+ content: [
620
+ {
621
+ type: "content",
622
+ content: {
623
+ type: "text",
624
+ text: `\`\`\`console\n${output.trimEnd()}\n\`\`\``,
625
+ },
626
+ },
627
+ ],
628
+ };
629
+ }
630
+ return {};
631
+ }
632
+ case "Agent":
633
+ case "Task": {
634
+ // The raw tool_result text ends with a model-directed trailer (an
635
+ // `agentId: … (use SendMessage …)` line plus a `<usage>` totals block)
636
+ // that ACP clients shouldn't see. The message-level `tool_use_result`
637
+ // carries the structured AgentOutput whose `content` is the subagent's
638
+ // report without the trailer — render from it when present (per the SDK
639
+ // 0.3.207 guidance) and fall back to the raw text otherwise (older CLIs,
640
+ // replayed sessions).
641
+ // Narrowed to the full union, not the completed variant — the status
642
+ // check below is what discriminates it, and pre-narrowing would let
643
+ // future field reads typecheck against a variant the runtime value may
644
+ // not be.
645
+ const structured = structuredResult(toolUseResult);
646
+ if (structured?.status === "completed" &&
647
+ Array.isArray(structured.content) &&
648
+ // A completed subagent can end with zero text blocks; an empty
649
+ // structured render would beat the raw fallback for no benefit.
650
+ structured.content.length > 0) {
651
+ return toAcpContentUpdate(structured.content, "is_error" in toolResult ? toolResult.is_error : false);
652
+ }
653
+ // No structured report to render from (replayed sessions —
654
+ // getSessionMessages doesn't expose the transcript's toolUseResult —
655
+ // and older CLIs). The SDK advises rendering from tool_use_result
656
+ // instead of parsing the text, but with no structured value the
657
+ // tail-anchored strip is the only cleanup available; if the trailer
658
+ // format changes it simply stops matching and the full raw text
659
+ // renders, no worse than before.
660
+ return toAcpContentUpdate(stripAgentTrailerFromContent(toolResult.content), "is_error" in toolResult ? toolResult.is_error : false);
661
+ }
662
+ case "Edit": // Edit is handled in hooks
663
+ case "Write": {
664
+ return {};
665
+ }
666
+ case "ExitPlanMode": {
667
+ return { title: "Exited Plan Mode" };
668
+ }
669
+ case "WebSearch": {
670
+ // The raw tool_result text is a model-directed dump ("Web search
671
+ // results for query: …\n\nLinks: [{…json…}]"). The structured
672
+ // WebSearchOutput carries the hits — render them the way server-side
673
+ // web_search_result blocks render ("Title (url)").
674
+ const structuredSearch = structuredResult(toolUseResult);
675
+ if (structuredSearch && Array.isArray(structuredSearch.results)) {
676
+ const lines = structuredSearch.results.flatMap((entry) => typeof entry === "string"
677
+ ? [entry]
678
+ : Array.isArray(entry?.content)
679
+ ? // tool_use_result arrives untyped across CLI version skew —
680
+ // skip off-spec hits rather than rendering
681
+ // "undefined (undefined)" lines.
682
+ entry.content.flatMap((hit) => typeof hit?.title === "string" && typeof hit?.url === "string"
683
+ ? [formatWebSearchHit(hit)]
684
+ : [])
685
+ : []);
686
+ if (lines.length > 0) {
687
+ return {
688
+ content: [
689
+ {
690
+ type: "content",
691
+ content: { type: "text", text: lines.join("\n") },
692
+ },
693
+ ],
694
+ };
695
+ }
696
+ }
697
+ return rawContentUpdate();
698
+ }
699
+ default: {
700
+ return rawContentUpdate();
701
+ }
702
+ }
703
+ }
704
+ /** One display format for a web-search hit, shared by the structured
705
+ * WebSearchOutput render and the server-side `web_search_result` block so
706
+ * the two paths can't drift. */
707
+ function formatWebSearchHit(hit) {
708
+ return `${hit.title} (${hit.url})`;
709
+ }
710
+ function toAcpContentUpdate(content, isError = false) {
711
+ if (Array.isArray(content) && content.length > 0) {
712
+ return {
713
+ content: content.map((c) => ({
714
+ type: "content",
715
+ content: toAcpContentBlock(c, isError),
716
+ })),
717
+ };
718
+ }
719
+ else if (typeof content === "object" && content !== null && "type" in content) {
720
+ return {
721
+ content: [
722
+ {
723
+ type: "content",
724
+ content: toAcpContentBlock(content, isError),
725
+ },
726
+ ],
727
+ };
728
+ }
729
+ else if (typeof content === "string" && content.length > 0) {
730
+ return {
731
+ content: [
732
+ {
733
+ type: "content",
734
+ content: {
735
+ type: "text",
736
+ text: isError ? `\`\`\`\n${content}\n\`\`\`` : content,
737
+ },
738
+ },
739
+ ],
740
+ };
741
+ }
742
+ return {};
743
+ }
744
+ function toAcpContentBlock(content, isError) {
745
+ const wrapText = (text) => ({
746
+ type: "text",
747
+ text: isError ? `\`\`\`\n${text}\n\`\`\`` : text,
748
+ });
749
+ switch (content.type) {
750
+ case "text":
751
+ return {
752
+ type: "text",
753
+ text: isError ? `\`\`\`\n${content.text}\n\`\`\`` : content.text,
754
+ };
755
+ case "image":
756
+ if (content.source.type === "base64") {
757
+ return {
758
+ type: "image",
759
+ data: content.source.data,
760
+ mimeType: content.source.media_type,
761
+ };
762
+ }
763
+ // URL and file-based images can't be converted to ACP format (requires data)
764
+ return wrapText(content.source.type === "url"
765
+ ? `[image: ${content.source.url}]`
766
+ : "[image: file reference]");
767
+ case "tool_reference":
768
+ return wrapText(`Tool: ${content.tool_name}`);
769
+ case "tool_search_tool_search_result":
770
+ return wrapText(`Tools found: ${content.tool_references.map((r) => r.tool_name).join(", ") || "none"}`);
771
+ case "tool_search_tool_result_error":
772
+ return wrapText(`Error: ${content.error_code}${content.error_message ? ` - ${content.error_message}` : ""}`);
773
+ case "web_search_result":
774
+ return wrapText(formatWebSearchHit(content));
775
+ case "web_search_tool_result_error":
776
+ return wrapText(`Error: ${content.error_code}`);
777
+ case "web_fetch_result":
778
+ return wrapText(`Fetched: ${content.url}`);
779
+ case "web_fetch_tool_result_error":
780
+ return wrapText(`Error: ${content.error_code}`);
781
+ case "code_execution_result":
782
+ return wrapText(`Output: ${content.stdout || content.stderr || ""}`);
783
+ case "bash_code_execution_result":
784
+ return wrapText(`Output: ${content.stdout || content.stderr || ""}`);
785
+ case "code_execution_tool_result_error":
786
+ case "bash_code_execution_tool_result_error":
787
+ return wrapText(`Error: ${content.error_code}`);
788
+ case "text_editor_code_execution_view_result":
789
+ return wrapText(content.content);
790
+ case "text_editor_code_execution_create_result":
791
+ return wrapText(content.is_file_update ? "File updated" : "File created");
792
+ case "text_editor_code_execution_str_replace_result":
793
+ return wrapText(content.lines?.join("\n") || "");
794
+ case "text_editor_code_execution_tool_result_error":
795
+ return wrapText(`Error: ${content.error_code}${content.error_message ? ` - ${content.error_message}` : ""}`);
796
+ default:
797
+ return wrapText(JSON.stringify(content));
798
+ }
799
+ }
800
+ export function planEntries(input) {
801
+ return (input?.todos ?? []).map((todo) => ({
802
+ content: todo.content,
803
+ status: todo.status,
804
+ priority: "medium",
805
+ }));
806
+ }
807
+ /**
808
+ * Best-effort parse of a TaskCreate tool_result content into the structured
809
+ * TaskCreateOutput. The SDK delivers tool outputs either as a string or as
810
+ * an array of TextBlockParam-like blocks containing JSON text; try both.
811
+ */
812
+ export function parseTaskCreateOutput(content) {
813
+ const tryParse = (text) => {
814
+ try {
815
+ const parsed = JSON.parse(text);
816
+ if (parsed &&
817
+ typeof parsed === "object" &&
818
+ parsed.task &&
819
+ typeof parsed.task.id === "string") {
820
+ return parsed;
821
+ }
822
+ }
823
+ catch {
824
+ // ignore
825
+ }
826
+ return undefined;
827
+ };
828
+ if (typeof content === "string") {
829
+ return tryParse(content);
830
+ }
831
+ if (Array.isArray(content)) {
832
+ for (const block of content) {
833
+ if (block && typeof block === "object" && "type" in block && block.type === "text") {
834
+ const text = block.text;
835
+ if (typeof text === "string") {
836
+ const parsed = tryParse(text);
837
+ if (parsed)
838
+ return parsed;
839
+ }
840
+ }
841
+ }
842
+ }
843
+ return undefined;
844
+ }
845
+ export function applyTaskCreate(state, input, output) {
846
+ const taskId = output?.task?.id;
847
+ if (!taskId || !input)
848
+ return;
849
+ state.set(taskId, {
850
+ subject: input.subject,
851
+ status: "pending",
852
+ activeForm: input.activeForm,
853
+ description: input.description,
854
+ });
855
+ }
856
+ export function applyTaskUpdate(state, input) {
857
+ if (!input?.taskId)
858
+ return;
859
+ if (input.status === "deleted") {
860
+ state.delete(input.taskId);
861
+ return;
862
+ }
863
+ const existing = state.get(input.taskId);
864
+ // Without a subject from either the existing entry or the update payload,
865
+ // we'd produce a plan entry with empty `content` — drop the update.
866
+ const subject = input.subject ?? existing?.subject;
867
+ if (!subject)
868
+ return;
869
+ state.set(input.taskId, {
870
+ subject,
871
+ status: input.status ?? existing?.status ?? "pending",
872
+ activeForm: input.activeForm ?? existing?.activeForm,
873
+ description: input.description ?? existing?.description,
874
+ });
875
+ }
876
+ export function taskStateToPlanEntries(state) {
877
+ return Array.from(state.values()).map((task) => ({
878
+ content: task.subject,
879
+ status: task.status,
880
+ priority: "medium",
881
+ }));
882
+ }
883
+ export function markdownEscape(text) {
884
+ let escape = "```";
885
+ for (const [m] of text.matchAll(/^```+/gm)) {
886
+ while (m.length >= escape.length) {
887
+ escape += "`";
888
+ }
889
+ }
890
+ return escape + "\n" + text + (text.endsWith("\n") ? "" : "\n") + escape;
891
+ }
892
+ /**
893
+ * Builds diff ToolUpdate content from the structured toolResponse provided by
894
+ * the PostToolUse hook for diff-producing tools (Edit, Write). Unlike parsing
895
+ * the plain unified diff string, this uses the pre-parsed structuredPatch
896
+ * which supports multiple replacement sites (replaceAll) and always includes
897
+ * context lines for better readability.
898
+ */
899
+ export function toolUpdateFromDiffToolResponse(toolResponse) {
900
+ if (!toolResponse || typeof toolResponse !== "object")
901
+ return {};
902
+ const response = toolResponse;
903
+ if (!response.filePath || !Array.isArray(response.structuredPatch))
904
+ return {};
905
+ const content = [];
906
+ const locations = [];
907
+ for (const { lines, newStart } of response.structuredPatch) {
908
+ const oldText = [];
909
+ const newText = [];
910
+ for (const line of lines) {
911
+ if (line.startsWith("-")) {
912
+ oldText.push(line.slice(1));
913
+ }
914
+ else if (line.startsWith("+")) {
915
+ newText.push(line.slice(1));
916
+ }
917
+ else {
918
+ oldText.push(line.slice(1));
919
+ newText.push(line.slice(1));
920
+ }
921
+ }
922
+ if (oldText.length > 0 || newText.length > 0) {
923
+ locations.push({ path: response.filePath, line: newStart });
924
+ content.push({
925
+ type: "diff",
926
+ path: response.filePath,
927
+ oldText: oldText.join("\n") || null,
928
+ newText: newText.join("\n"),
929
+ });
930
+ }
931
+ }
932
+ const result = {};
933
+ if (content.length > 0)
934
+ result.content = content;
935
+ if (locations.length > 0)
936
+ result.locations = locations;
937
+ return result;
938
+ }
939
+ /* A global variable to store callbacks that should be executed when receiving hooks from Claude Code */
940
+ const toolUseCallbacks = {};
941
+ /* Setup callbacks that will be called when receiving hooks from Claude Code */
942
+ export const registerHookCallback = (toolUseID, { onPostToolUseHook, }) => {
943
+ toolUseCallbacks[toolUseID] = {
944
+ onPostToolUseHook,
945
+ };
946
+ };
947
+ /* A callback for Claude Code that is called when receiving a PostToolUse hook */
948
+ export const createPostToolUseHook = (options) => async (input, toolUseID) => {
949
+ if (input.hook_event_name === "PostToolUse") {
950
+ // Handle EnterPlanMode tool - notify client of mode change after successful execution
951
+ if (input.tool_name === "EnterPlanMode" && options?.onEnterPlanMode) {
952
+ await options.onEnterPlanMode();
953
+ }
954
+ if (toolUseID) {
955
+ const onPostToolUseHook = toolUseCallbacks[toolUseID]?.onPostToolUseHook;
956
+ if (onPostToolUseHook) {
957
+ await onPostToolUseHook(toolUseID, input.tool_input, input.tool_response);
958
+ }
959
+ delete toolUseCallbacks[toolUseID]; // Cleanup after execution
960
+ }
961
+ }
962
+ return { continue: true };
963
+ };
964
+ /**
965
+ * Hook callback for `TaskCreated` / `TaskCompleted` events. The SDK fires
966
+ * these for both user-facing TaskCreate tool calls and subagent task
967
+ * creation, giving us `task_id` + `task_subject` without having to parse
968
+ * tool_result payloads.
969
+ *
970
+ * Populating `taskState` from the hook means a later `TaskUpdate` (which
971
+ * typically only carries `taskId` + `status`) finds an existing entry with
972
+ * a real subject, instead of synthesizing a placeholder with empty content.
973
+ */
974
+ export const createTaskHook = (options) => async (input) => {
975
+ const taskId = "task_id" in input && typeof input.task_id === "string" ? input.task_id : undefined;
976
+ if (!taskId)
977
+ return { continue: true };
978
+ if (input.hook_event_name === "TaskCreated") {
979
+ if (!input.task_subject)
980
+ return { continue: true };
981
+ if (options.taskState.has(taskId))
982
+ return { continue: true };
983
+ options.taskState.set(taskId, {
984
+ subject: input.task_subject,
985
+ status: "pending",
986
+ description: input.task_description,
987
+ });
988
+ if (options.onChange)
989
+ await options.onChange(input);
990
+ }
991
+ else if (input.hook_event_name === "TaskCompleted") {
992
+ const existing = options.taskState.get(taskId);
993
+ if (!existing || existing.status === "completed")
994
+ return { continue: true };
995
+ options.taskState.set(taskId, { ...existing, status: "completed" });
996
+ if (options.onChange)
997
+ await options.onChange(input);
998
+ }
999
+ return { continue: true };
1000
+ };