@lucascouts/claude-agent-tui 0.1.0
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/LICENSE +191 -0
- package/NOTICE +14 -0
- package/README.md +50 -0
- package/dist/acp-agent.d.ts +594 -0
- package/dist/acp-agent.d.ts.map +1 -0
- package/dist/acp-agent.js +2139 -0
- package/dist/ansi-mirror.d.ts +42 -0
- package/dist/ansi-mirror.d.ts.map +1 -0
- package/dist/ansi-mirror.js +61 -0
- package/dist/besteffort.d.ts +44 -0
- package/dist/besteffort.d.ts.map +1 -0
- package/dist/besteffort.js +100 -0
- package/dist/billing/entrypoint-guard.d.ts +97 -0
- package/dist/billing/entrypoint-guard.d.ts.map +1 -0
- package/dist/billing/entrypoint-guard.js +166 -0
- package/dist/claude-path.d.ts +12 -0
- package/dist/claude-path.d.ts.map +1 -0
- package/dist/claude-path.js +61 -0
- package/dist/diff-enriched-reader.d.ts +41 -0
- package/dist/diff-enriched-reader.d.ts.map +1 -0
- package/dist/diff-enriched-reader.js +106 -0
- package/dist/diff-source.d.ts +104 -0
- package/dist/diff-source.d.ts.map +1 -0
- package/dist/diff-source.js +164 -0
- package/dist/end-of-turn.d.ts +172 -0
- package/dist/end-of-turn.d.ts.map +1 -0
- package/dist/end-of-turn.js +415 -0
- package/dist/engine-lifecycle.d.ts +222 -0
- package/dist/engine-lifecycle.d.ts.map +1 -0
- package/dist/engine-lifecycle.js +236 -0
- package/dist/engine-pty.d.ts +143 -0
- package/dist/engine-pty.d.ts.map +1 -0
- package/dist/engine-pty.js +222 -0
- package/dist/engine-watcher.d.ts +83 -0
- package/dist/engine-watcher.d.ts.map +1 -0
- package/dist/engine-watcher.js +173 -0
- package/dist/engine.d.ts +30 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +34 -0
- package/dist/event-switch.d.ts +164 -0
- package/dist/event-switch.d.ts.map +1 -0
- package/dist/event-switch.js +206 -0
- package/dist/gate/port.d.ts +38 -0
- package/dist/gate/port.d.ts.map +1 -0
- package/dist/gate/port.js +126 -0
- package/dist/gate/settings-writer.d.ts +130 -0
- package/dist/gate/settings-writer.d.ts.map +1 -0
- package/dist/gate/settings-writer.js +349 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +106 -0
- package/dist/jsonl.d.ts +267 -0
- package/dist/jsonl.d.ts.map +1 -0
- package/dist/jsonl.js +527 -0
- package/dist/lib.d.ts +6 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +5 -0
- package/dist/linearize.d.ts +219 -0
- package/dist/linearize.d.ts.map +1 -0
- package/dist/linearize.js +444 -0
- package/dist/live-diff-env.d.ts +7 -0
- package/dist/live-diff-env.d.ts.map +1 -0
- package/dist/live-diff-env.js +18 -0
- package/dist/live-subagent-env.d.ts +7 -0
- package/dist/live-subagent-env.d.ts.map +1 -0
- package/dist/live-subagent-env.js +19 -0
- package/dist/permissions/allow-inject.d.ts +67 -0
- package/dist/permissions/allow-inject.d.ts.map +1 -0
- package/dist/permissions/allow-inject.js +85 -0
- package/dist/permissions/deny.d.ts +60 -0
- package/dist/permissions/deny.d.ts.map +1 -0
- package/dist/permissions/deny.js +81 -0
- package/dist/permissions/gate-wiring.d.ts +112 -0
- package/dist/permissions/gate-wiring.d.ts.map +1 -0
- package/dist/permissions/gate-wiring.js +350 -0
- package/dist/permissions/hook-server.d.ts +72 -0
- package/dist/permissions/hook-server.d.ts.map +1 -0
- package/dist/permissions/hook-server.js +179 -0
- package/dist/permissions/permission-mode.d.ts +67 -0
- package/dist/permissions/permission-mode.d.ts.map +1 -0
- package/dist/permissions/permission-mode.js +100 -0
- package/dist/permissions/request-permission.d.ts +102 -0
- package/dist/permissions/request-permission.d.ts.map +1 -0
- package/dist/permissions/request-permission.js +124 -0
- package/dist/settings.d.ts +68 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +182 -0
- package/dist/stop-reason-map.d.ts +17 -0
- package/dist/stop-reason-map.d.ts.map +1 -0
- package/dist/stop-reason-map.js +33 -0
- package/dist/subagent-source.d.ts +63 -0
- package/dist/subagent-source.d.ts.map +1 -0
- package/dist/subagent-source.js +132 -0
- package/dist/subagent-watcher.d.ts +40 -0
- package/dist/subagent-watcher.d.ts.map +1 -0
- package/dist/subagent-watcher.js +108 -0
- package/dist/tools.d.ts +119 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +729 -0
- package/dist/usage-env.d.ts +7 -0
- package/dist/usage-env.d.ts.map +1 -0
- package/dist/usage-env.js +16 -0
- package/dist/usage.d.ts +54 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +53 -0
- package/dist/utils.d.ts +16 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +83 -0
- package/dist/zed-register.d.ts +26 -0
- package/dist/zed-register.d.ts.map +1 -0
- package/dist/zed-register.js +106 -0
- package/package.json +79 -0
package/dist/tools.js
ADDED
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Convert an absolute file path to a project-relative path for display.
|
|
4
|
+
* Returns the original path if it's outside the project directory or if no cwd is provided.
|
|
5
|
+
*/
|
|
6
|
+
export function toDisplayPath(filePath, cwd) {
|
|
7
|
+
if (!cwd)
|
|
8
|
+
return filePath;
|
|
9
|
+
const resolvedCwd = path.resolve(cwd);
|
|
10
|
+
const resolvedFile = path.resolve(filePath);
|
|
11
|
+
if (resolvedFile.startsWith(resolvedCwd + path.sep) || resolvedFile === resolvedCwd) {
|
|
12
|
+
return path.relative(resolvedCwd, resolvedFile);
|
|
13
|
+
}
|
|
14
|
+
return filePath;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Story 025 / Task 2.2 (R2.1, R2.3) — derive the ACP `toolCallId` from a JSONL
|
|
18
|
+
* `tool_use.id`. Pure, total and side-effect-free, and STABLE across the live and
|
|
19
|
+
* replay paths so a loaded thread re-derives identical ids.
|
|
20
|
+
*
|
|
21
|
+
* Default (`namespaced` omitted/false): returns the raw `tool_use.id` VERBATIM — the
|
|
22
|
+
* §7 reuse contract stories 017-021 assume, and the behaviour the live-Zed probe
|
|
23
|
+
* (Task 2.1, FORK.md) confirms or overturns. When the probe shows the user's Zed
|
|
24
|
+
* requires per-session uniqueness, callers pass `{ namespaced: true }` to get a
|
|
25
|
+
* deterministic `${sessionId}:${toolUseId}` id — identical for the same
|
|
26
|
+
* (toolUseId, sessionId) and distinct across sessions. The raw id stays embedded so
|
|
27
|
+
* the original `tool_use.id` remains recoverable.
|
|
28
|
+
*/
|
|
29
|
+
export function toolCallIdFor(toolUseId, sessionId, options = {}) {
|
|
30
|
+
return options.namespaced ? `${sessionId}:${toolUseId}` : toolUseId;
|
|
31
|
+
}
|
|
32
|
+
export function toolInfoFromToolUse(toolUse, supportsTerminalOutput = false, cwd) {
|
|
33
|
+
const name = toolUse.name;
|
|
34
|
+
switch (name) {
|
|
35
|
+
case "Agent":
|
|
36
|
+
case "Task": {
|
|
37
|
+
const input = toolUse.input;
|
|
38
|
+
return {
|
|
39
|
+
title: input?.description ? input.description : "Task",
|
|
40
|
+
kind: "think",
|
|
41
|
+
content: input && "prompt" in input
|
|
42
|
+
? [
|
|
43
|
+
{
|
|
44
|
+
type: "content",
|
|
45
|
+
content: { type: "text", text: input.prompt },
|
|
46
|
+
},
|
|
47
|
+
]
|
|
48
|
+
: [],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
case "Bash": {
|
|
52
|
+
const input = toolUse.input;
|
|
53
|
+
return {
|
|
54
|
+
title: input?.command ? input.command : "Terminal",
|
|
55
|
+
kind: "execute",
|
|
56
|
+
content: supportsTerminalOutput
|
|
57
|
+
? [{ type: "terminal", terminalId: toolUse.id }]
|
|
58
|
+
: input && input.description
|
|
59
|
+
? [
|
|
60
|
+
{
|
|
61
|
+
type: "content",
|
|
62
|
+
content: { type: "text", text: input.description },
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
: [],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
case "Read": {
|
|
69
|
+
const input = toolUse.input;
|
|
70
|
+
let limit = "";
|
|
71
|
+
if (input?.limit && input.limit > 0) {
|
|
72
|
+
limit = " (" + (input.offset ?? 1) + " - " + ((input.offset ?? 1) + input.limit - 1) + ")";
|
|
73
|
+
}
|
|
74
|
+
else if (input?.offset) {
|
|
75
|
+
limit = " (from line " + input.offset + ")";
|
|
76
|
+
}
|
|
77
|
+
const displayPath = input?.file_path ? toDisplayPath(input.file_path, cwd) : "File";
|
|
78
|
+
return {
|
|
79
|
+
title: "Read " + displayPath + limit,
|
|
80
|
+
kind: "read",
|
|
81
|
+
locations: input?.file_path
|
|
82
|
+
? [
|
|
83
|
+
{
|
|
84
|
+
path: input.file_path,
|
|
85
|
+
line: input.offset ?? 1,
|
|
86
|
+
},
|
|
87
|
+
]
|
|
88
|
+
: [],
|
|
89
|
+
content: [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
case "Write": {
|
|
93
|
+
const input = toolUse.input;
|
|
94
|
+
let content = [];
|
|
95
|
+
if (input && input.file_path) {
|
|
96
|
+
content = [
|
|
97
|
+
{
|
|
98
|
+
type: "diff",
|
|
99
|
+
path: input.file_path,
|
|
100
|
+
oldText: null,
|
|
101
|
+
newText: input.content,
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
else if (input && input.content) {
|
|
106
|
+
content = [
|
|
107
|
+
{
|
|
108
|
+
type: "content",
|
|
109
|
+
content: { type: "text", text: input.content },
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
const displayPath = input?.file_path ? toDisplayPath(input.file_path, cwd) : undefined;
|
|
114
|
+
return {
|
|
115
|
+
title: displayPath ? `Write ${displayPath}` : "Write",
|
|
116
|
+
kind: "edit",
|
|
117
|
+
content,
|
|
118
|
+
locations: input?.file_path ? [{ path: input.file_path }] : [],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
case "Edit": {
|
|
122
|
+
const input = toolUse.input;
|
|
123
|
+
let content = [];
|
|
124
|
+
if (input && input.file_path && (input.old_string || input.new_string)) {
|
|
125
|
+
content = [
|
|
126
|
+
{
|
|
127
|
+
type: "diff",
|
|
128
|
+
path: input.file_path,
|
|
129
|
+
oldText: input.old_string || null,
|
|
130
|
+
newText: input.new_string ?? "",
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
const displayPath = input?.file_path ? toDisplayPath(input.file_path, cwd) : undefined;
|
|
135
|
+
return {
|
|
136
|
+
title: displayPath ? `Edit ${displayPath}` : "Edit",
|
|
137
|
+
kind: "edit",
|
|
138
|
+
content,
|
|
139
|
+
locations: input?.file_path ? [{ path: input.file_path }] : [],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
case "Glob": {
|
|
143
|
+
const input = toolUse.input;
|
|
144
|
+
let label = "Find";
|
|
145
|
+
if (input?.path) {
|
|
146
|
+
label += ` \`${input.path}\``;
|
|
147
|
+
}
|
|
148
|
+
if (input?.pattern) {
|
|
149
|
+
label += ` \`${input.pattern}\``;
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
title: label,
|
|
153
|
+
kind: "search",
|
|
154
|
+
content: [],
|
|
155
|
+
locations: input?.path ? [{ path: input.path }] : [],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
case "Grep": {
|
|
159
|
+
const input = toolUse.input;
|
|
160
|
+
let label = "grep";
|
|
161
|
+
if (input?.["-i"]) {
|
|
162
|
+
label += " -i";
|
|
163
|
+
}
|
|
164
|
+
if (input?.["-n"]) {
|
|
165
|
+
label += " -n";
|
|
166
|
+
}
|
|
167
|
+
if (input?.["-A"] !== undefined) {
|
|
168
|
+
label += ` -A ${input["-A"]}`;
|
|
169
|
+
}
|
|
170
|
+
if (input?.["-B"] !== undefined) {
|
|
171
|
+
label += ` -B ${input["-B"]}`;
|
|
172
|
+
}
|
|
173
|
+
if (input?.["-C"] !== undefined) {
|
|
174
|
+
label += ` -C ${input["-C"]}`;
|
|
175
|
+
}
|
|
176
|
+
if (input?.output_mode) {
|
|
177
|
+
switch (input.output_mode) {
|
|
178
|
+
case "files_with_matches":
|
|
179
|
+
label += " -l";
|
|
180
|
+
break;
|
|
181
|
+
case "count":
|
|
182
|
+
label += " -c";
|
|
183
|
+
break;
|
|
184
|
+
case "content":
|
|
185
|
+
default:
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (input?.head_limit !== undefined) {
|
|
190
|
+
label += ` | head -${input.head_limit}`;
|
|
191
|
+
}
|
|
192
|
+
if (input?.glob) {
|
|
193
|
+
label += ` --include="${input.glob}"`;
|
|
194
|
+
}
|
|
195
|
+
if (input?.type) {
|
|
196
|
+
label += ` --type=${input.type}`;
|
|
197
|
+
}
|
|
198
|
+
if (input?.multiline) {
|
|
199
|
+
label += " -P";
|
|
200
|
+
}
|
|
201
|
+
if (input?.pattern) {
|
|
202
|
+
label += ` "${input.pattern}"`;
|
|
203
|
+
}
|
|
204
|
+
if (input?.path) {
|
|
205
|
+
label += ` ${input.path}`;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
title: label,
|
|
209
|
+
kind: "search",
|
|
210
|
+
content: [],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
case "WebFetch": {
|
|
214
|
+
const input = toolUse.input;
|
|
215
|
+
return {
|
|
216
|
+
title: input?.url ? `Fetch ${input.url}` : "Fetch",
|
|
217
|
+
kind: "fetch",
|
|
218
|
+
content: input && input.prompt
|
|
219
|
+
? [
|
|
220
|
+
{
|
|
221
|
+
type: "content",
|
|
222
|
+
content: { type: "text", text: input.prompt },
|
|
223
|
+
},
|
|
224
|
+
]
|
|
225
|
+
: [],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
case "WebSearch": {
|
|
229
|
+
const input = toolUse.input;
|
|
230
|
+
let label = input?.query ? `"${input.query}"` : "Web search";
|
|
231
|
+
if (input?.allowed_domains && input.allowed_domains.length > 0) {
|
|
232
|
+
label += ` (allowed: ${input.allowed_domains.join(", ")})`;
|
|
233
|
+
}
|
|
234
|
+
if (input?.blocked_domains && input.blocked_domains.length > 0) {
|
|
235
|
+
label += ` (blocked: ${input.blocked_domains.join(", ")})`;
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
title: label,
|
|
239
|
+
kind: "fetch",
|
|
240
|
+
content: [],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
case "TodoWrite": {
|
|
244
|
+
const input = toolUse.input;
|
|
245
|
+
return {
|
|
246
|
+
title: Array.isArray(input?.todos)
|
|
247
|
+
? `Update TODOs: ${input.todos.map((todo) => todo.content).join(", ")}`
|
|
248
|
+
: "Update TODOs",
|
|
249
|
+
kind: "think",
|
|
250
|
+
content: [],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
case "TaskCreate": {
|
|
254
|
+
const input = toolUse.input;
|
|
255
|
+
return {
|
|
256
|
+
title: input?.subject ? `Create task: ${input.subject}` : "Create task",
|
|
257
|
+
kind: "think",
|
|
258
|
+
content: [],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
case "TaskUpdate": {
|
|
262
|
+
const input = toolUse.input;
|
|
263
|
+
return {
|
|
264
|
+
title: input?.subject ? `Update task: ${input.subject}` : "Update task",
|
|
265
|
+
kind: "think",
|
|
266
|
+
content: [],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
case "TaskList": {
|
|
270
|
+
return {
|
|
271
|
+
title: "List tasks",
|
|
272
|
+
kind: "think",
|
|
273
|
+
content: [],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
case "TaskGet": {
|
|
277
|
+
return {
|
|
278
|
+
title: "Get task",
|
|
279
|
+
kind: "think",
|
|
280
|
+
content: [],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
case "ExitPlanMode": {
|
|
284
|
+
const planInput = toolUse.input;
|
|
285
|
+
return {
|
|
286
|
+
title: "Ready to code?",
|
|
287
|
+
kind: "switch_mode",
|
|
288
|
+
content: planInput?.plan
|
|
289
|
+
? [{ type: "content", content: { type: "text", text: planInput.plan } }]
|
|
290
|
+
: [],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
case "Other": {
|
|
294
|
+
const input = toolUse.input;
|
|
295
|
+
let output;
|
|
296
|
+
try {
|
|
297
|
+
output = JSON.stringify(input, null, 2);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
output = typeof input === "string" ? input : "{}";
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
title: name || "Unknown Tool",
|
|
304
|
+
kind: "other",
|
|
305
|
+
content: [
|
|
306
|
+
{
|
|
307
|
+
type: "content",
|
|
308
|
+
content: {
|
|
309
|
+
type: "text",
|
|
310
|
+
text: `\`\`\`json\n${output}\`\`\``,
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
default:
|
|
317
|
+
return {
|
|
318
|
+
title: name || "Unknown Tool",
|
|
319
|
+
kind: "other",
|
|
320
|
+
content: [],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
export function toolUpdateFromToolResult(toolResult, toolUse, supportsTerminalOutput = false) {
|
|
325
|
+
if ("is_error" in toolResult &&
|
|
326
|
+
toolResult.is_error &&
|
|
327
|
+
toolResult.content &&
|
|
328
|
+
toolResult.content.length > 0) {
|
|
329
|
+
// Only return errors
|
|
330
|
+
return toAcpContentUpdate(toolResult.content, true);
|
|
331
|
+
}
|
|
332
|
+
switch (toolUse?.name) {
|
|
333
|
+
case "Read":
|
|
334
|
+
if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
|
|
335
|
+
return {
|
|
336
|
+
content: toolResult.content.map((content) => ({
|
|
337
|
+
type: "content",
|
|
338
|
+
content: content.type === "text"
|
|
339
|
+
? {
|
|
340
|
+
type: "text",
|
|
341
|
+
text: markdownEscape(content.text),
|
|
342
|
+
}
|
|
343
|
+
: toAcpContentBlock(content, false),
|
|
344
|
+
})),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
else if (typeof toolResult.content === "string" && toolResult.content.length > 0) {
|
|
348
|
+
return {
|
|
349
|
+
content: [
|
|
350
|
+
{
|
|
351
|
+
type: "content",
|
|
352
|
+
content: {
|
|
353
|
+
type: "text",
|
|
354
|
+
text: markdownEscape(toolResult.content),
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return {};
|
|
361
|
+
case "Bash": {
|
|
362
|
+
const result = toolResult.content;
|
|
363
|
+
const terminalId = "tool_use_id" in toolResult ? String(toolResult.tool_use_id) : "";
|
|
364
|
+
const isError = "is_error" in toolResult && toolResult.is_error;
|
|
365
|
+
// Extract output and exit code from either format:
|
|
366
|
+
// 1. BetaBashCodeExecutionResultBlock: { type: "bash_code_execution_result", stdout, stderr, return_code }
|
|
367
|
+
// 2. Plain string content from a regular tool_result
|
|
368
|
+
// 3. Array content (e.g. [{ type: "text", text: "..." }])
|
|
369
|
+
let output = "";
|
|
370
|
+
let exitCode = isError ? 1 : 0;
|
|
371
|
+
if (result &&
|
|
372
|
+
typeof result === "object" &&
|
|
373
|
+
"type" in result &&
|
|
374
|
+
result.type === "bash_code_execution_result") {
|
|
375
|
+
const bashResult = result;
|
|
376
|
+
output = [bashResult.stdout, bashResult.stderr].filter(Boolean).join("\n");
|
|
377
|
+
exitCode = bashResult.return_code;
|
|
378
|
+
}
|
|
379
|
+
else if (typeof result === "string") {
|
|
380
|
+
output = result;
|
|
381
|
+
}
|
|
382
|
+
else if (Array.isArray(result) &&
|
|
383
|
+
result.length > 0 &&
|
|
384
|
+
"text" in result[0] &&
|
|
385
|
+
typeof result[0].text === "string") {
|
|
386
|
+
output = result.map((c) => c.text).join("\n");
|
|
387
|
+
}
|
|
388
|
+
if (supportsTerminalOutput) {
|
|
389
|
+
return {
|
|
390
|
+
content: [{ type: "terminal", terminalId }],
|
|
391
|
+
_meta: {
|
|
392
|
+
terminal_info: {
|
|
393
|
+
terminal_id: terminalId,
|
|
394
|
+
},
|
|
395
|
+
terminal_output: {
|
|
396
|
+
terminal_id: terminalId,
|
|
397
|
+
data: output,
|
|
398
|
+
},
|
|
399
|
+
terminal_exit: {
|
|
400
|
+
terminal_id: terminalId,
|
|
401
|
+
exit_code: exitCode,
|
|
402
|
+
signal: null,
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
// Fallback: format output as a code block without terminal _meta
|
|
408
|
+
if (output.trim()) {
|
|
409
|
+
return {
|
|
410
|
+
content: [
|
|
411
|
+
{
|
|
412
|
+
type: "content",
|
|
413
|
+
content: {
|
|
414
|
+
type: "text",
|
|
415
|
+
text: `\`\`\`console\n${output.trimEnd()}\n\`\`\``,
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
return {};
|
|
422
|
+
}
|
|
423
|
+
case "Edit": // Edit is handled in hooks
|
|
424
|
+
case "Write": {
|
|
425
|
+
return {};
|
|
426
|
+
}
|
|
427
|
+
case "ExitPlanMode": {
|
|
428
|
+
return { title: "Exited Plan Mode" };
|
|
429
|
+
}
|
|
430
|
+
default: {
|
|
431
|
+
return toAcpContentUpdate(toolResult.content, "is_error" in toolResult ? toolResult.is_error : false);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function toAcpContentUpdate(content, isError = false) {
|
|
436
|
+
if (Array.isArray(content) && content.length > 0) {
|
|
437
|
+
return {
|
|
438
|
+
content: content.map((c) => ({
|
|
439
|
+
type: "content",
|
|
440
|
+
content: toAcpContentBlock(c, isError),
|
|
441
|
+
})),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
else if (typeof content === "object" && content !== null && "type" in content) {
|
|
445
|
+
return {
|
|
446
|
+
content: [
|
|
447
|
+
{
|
|
448
|
+
type: "content",
|
|
449
|
+
content: toAcpContentBlock(content, isError),
|
|
450
|
+
},
|
|
451
|
+
],
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
else if (typeof content === "string" && content.length > 0) {
|
|
455
|
+
return {
|
|
456
|
+
content: [
|
|
457
|
+
{
|
|
458
|
+
type: "content",
|
|
459
|
+
content: {
|
|
460
|
+
type: "text",
|
|
461
|
+
text: isError ? `\`\`\`\n${content}\n\`\`\`` : content,
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return {};
|
|
468
|
+
}
|
|
469
|
+
function toAcpContentBlock(content, isError) {
|
|
470
|
+
const wrapText = (text) => ({
|
|
471
|
+
type: "text",
|
|
472
|
+
text: isError ? `\`\`\`\n${text}\n\`\`\`` : text,
|
|
473
|
+
});
|
|
474
|
+
switch (content.type) {
|
|
475
|
+
case "text":
|
|
476
|
+
return {
|
|
477
|
+
type: "text",
|
|
478
|
+
text: isError ? `\`\`\`\n${content.text}\n\`\`\`` : content.text,
|
|
479
|
+
};
|
|
480
|
+
case "image":
|
|
481
|
+
if (content.source.type === "base64") {
|
|
482
|
+
return {
|
|
483
|
+
type: "image",
|
|
484
|
+
data: content.source.data,
|
|
485
|
+
mimeType: content.source.media_type,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
// URL and file-based images can't be converted to ACP format (requires data)
|
|
489
|
+
return wrapText(content.source.type === "url"
|
|
490
|
+
? `[image: ${content.source.url}]`
|
|
491
|
+
: "[image: file reference]");
|
|
492
|
+
case "tool_reference":
|
|
493
|
+
return wrapText(`Tool: ${content.tool_name}`);
|
|
494
|
+
case "tool_search_tool_search_result":
|
|
495
|
+
return wrapText(`Tools found: ${content.tool_references.map((r) => r.tool_name).join(", ") || "none"}`);
|
|
496
|
+
case "tool_search_tool_result_error":
|
|
497
|
+
return wrapText(`Error: ${content.error_code}${content.error_message ? ` - ${content.error_message}` : ""}`);
|
|
498
|
+
case "web_search_result":
|
|
499
|
+
return wrapText(`${content.title} (${content.url})`);
|
|
500
|
+
case "web_search_tool_result_error":
|
|
501
|
+
return wrapText(`Error: ${content.error_code}`);
|
|
502
|
+
case "web_fetch_result":
|
|
503
|
+
return wrapText(`Fetched: ${content.url}`);
|
|
504
|
+
case "web_fetch_tool_result_error":
|
|
505
|
+
return wrapText(`Error: ${content.error_code}`);
|
|
506
|
+
case "code_execution_result":
|
|
507
|
+
return wrapText(`Output: ${content.stdout || content.stderr || ""}`);
|
|
508
|
+
case "bash_code_execution_result":
|
|
509
|
+
return wrapText(`Output: ${content.stdout || content.stderr || ""}`);
|
|
510
|
+
case "code_execution_tool_result_error":
|
|
511
|
+
case "bash_code_execution_tool_result_error":
|
|
512
|
+
return wrapText(`Error: ${content.error_code}`);
|
|
513
|
+
case "text_editor_code_execution_view_result":
|
|
514
|
+
return wrapText(content.content);
|
|
515
|
+
case "text_editor_code_execution_create_result":
|
|
516
|
+
return wrapText(content.is_file_update ? "File updated" : "File created");
|
|
517
|
+
case "text_editor_code_execution_str_replace_result":
|
|
518
|
+
return wrapText(content.lines?.join("\n") || "");
|
|
519
|
+
case "text_editor_code_execution_tool_result_error":
|
|
520
|
+
return wrapText(`Error: ${content.error_code}${content.error_message ? ` - ${content.error_message}` : ""}`);
|
|
521
|
+
default:
|
|
522
|
+
return wrapText(JSON.stringify(content));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
export function planEntries(input) {
|
|
526
|
+
return (input?.todos ?? []).map((todo) => ({
|
|
527
|
+
content: todo.content,
|
|
528
|
+
status: todo.status,
|
|
529
|
+
priority: "medium",
|
|
530
|
+
}));
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Best-effort parse of a TaskCreate tool_result content into the structured
|
|
534
|
+
* TaskCreateOutput. The SDK delivers tool outputs either as a string or as
|
|
535
|
+
* an array of TextBlockParam-like blocks containing JSON text; try both.
|
|
536
|
+
*/
|
|
537
|
+
export function parseTaskCreateOutput(content) {
|
|
538
|
+
const tryParse = (text) => {
|
|
539
|
+
try {
|
|
540
|
+
const parsed = JSON.parse(text);
|
|
541
|
+
if (parsed &&
|
|
542
|
+
typeof parsed === "object" &&
|
|
543
|
+
parsed.task &&
|
|
544
|
+
typeof parsed.task.id === "string") {
|
|
545
|
+
return parsed;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
// ignore
|
|
550
|
+
}
|
|
551
|
+
return undefined;
|
|
552
|
+
};
|
|
553
|
+
if (typeof content === "string") {
|
|
554
|
+
return tryParse(content);
|
|
555
|
+
}
|
|
556
|
+
if (Array.isArray(content)) {
|
|
557
|
+
for (const block of content) {
|
|
558
|
+
if (block && typeof block === "object" && "type" in block && block.type === "text") {
|
|
559
|
+
const text = block.text;
|
|
560
|
+
if (typeof text === "string") {
|
|
561
|
+
const parsed = tryParse(text);
|
|
562
|
+
if (parsed)
|
|
563
|
+
return parsed;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
export function applyTaskCreate(state, input, output) {
|
|
571
|
+
const taskId = output?.task?.id;
|
|
572
|
+
if (!taskId || !input)
|
|
573
|
+
return;
|
|
574
|
+
state.set(taskId, {
|
|
575
|
+
subject: input.subject,
|
|
576
|
+
status: "pending",
|
|
577
|
+
activeForm: input.activeForm,
|
|
578
|
+
description: input.description,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
export function applyTaskUpdate(state, input) {
|
|
582
|
+
if (!input?.taskId)
|
|
583
|
+
return;
|
|
584
|
+
if (input.status === "deleted") {
|
|
585
|
+
state.delete(input.taskId);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const existing = state.get(input.taskId);
|
|
589
|
+
// Without a subject from either the existing entry or the update payload,
|
|
590
|
+
// we'd produce a plan entry with empty `content` — drop the update.
|
|
591
|
+
const subject = input.subject ?? existing?.subject;
|
|
592
|
+
if (!subject)
|
|
593
|
+
return;
|
|
594
|
+
state.set(input.taskId, {
|
|
595
|
+
subject,
|
|
596
|
+
status: input.status ?? existing?.status ?? "pending",
|
|
597
|
+
activeForm: input.activeForm ?? existing?.activeForm,
|
|
598
|
+
description: input.description ?? existing?.description,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
export function taskStateToPlanEntries(state) {
|
|
602
|
+
return Array.from(state.values()).map((task) => ({
|
|
603
|
+
content: task.subject,
|
|
604
|
+
status: task.status,
|
|
605
|
+
priority: "medium",
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
608
|
+
export function markdownEscape(text) {
|
|
609
|
+
let escape = "```";
|
|
610
|
+
for (const [m] of text.matchAll(/^```+/gm)) {
|
|
611
|
+
while (m.length >= escape.length) {
|
|
612
|
+
escape += "`";
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return escape + "\n" + text + (text.endsWith("\n") ? "" : "\n") + escape;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Builds diff ToolUpdate content from the structured toolResponse provided by
|
|
619
|
+
* the PostToolUse hook for diff-producing tools (Edit, Write). Unlike parsing
|
|
620
|
+
* the plain unified diff string, this uses the pre-parsed structuredPatch
|
|
621
|
+
* which supports multiple replacement sites (replaceAll) and always includes
|
|
622
|
+
* context lines for better readability.
|
|
623
|
+
*/
|
|
624
|
+
export function toolUpdateFromDiffToolResponse(toolResponse) {
|
|
625
|
+
if (!toolResponse || typeof toolResponse !== "object")
|
|
626
|
+
return {};
|
|
627
|
+
const response = toolResponse;
|
|
628
|
+
if (!response.filePath || !Array.isArray(response.structuredPatch))
|
|
629
|
+
return {};
|
|
630
|
+
const content = [];
|
|
631
|
+
const locations = [];
|
|
632
|
+
for (const { lines, newStart } of response.structuredPatch) {
|
|
633
|
+
const oldText = [];
|
|
634
|
+
const newText = [];
|
|
635
|
+
for (const line of lines) {
|
|
636
|
+
if (line.startsWith("-")) {
|
|
637
|
+
oldText.push(line.slice(1));
|
|
638
|
+
}
|
|
639
|
+
else if (line.startsWith("+")) {
|
|
640
|
+
newText.push(line.slice(1));
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
oldText.push(line.slice(1));
|
|
644
|
+
newText.push(line.slice(1));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (oldText.length > 0 || newText.length > 0) {
|
|
648
|
+
locations.push({ path: response.filePath, line: newStart });
|
|
649
|
+
content.push({
|
|
650
|
+
type: "diff",
|
|
651
|
+
path: response.filePath,
|
|
652
|
+
oldText: oldText.join("\n") || null,
|
|
653
|
+
newText: newText.join("\n"),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const result = {};
|
|
658
|
+
if (content.length > 0)
|
|
659
|
+
result.content = content;
|
|
660
|
+
if (locations.length > 0)
|
|
661
|
+
result.locations = locations;
|
|
662
|
+
return result;
|
|
663
|
+
}
|
|
664
|
+
/* A global variable to store callbacks that should be executed when receiving hooks from Claude Code */
|
|
665
|
+
const toolUseCallbacks = {};
|
|
666
|
+
/* Setup callbacks that will be called when receiving hooks from Claude Code */
|
|
667
|
+
export const registerHookCallback = (toolUseID, { onPostToolUseHook, }) => {
|
|
668
|
+
toolUseCallbacks[toolUseID] = {
|
|
669
|
+
onPostToolUseHook,
|
|
670
|
+
};
|
|
671
|
+
};
|
|
672
|
+
/* A callback for Claude Code that is called when receiving a PostToolUse hook */
|
|
673
|
+
export const createPostToolUseHook = (logger = console, options) => async (input, toolUseID) => {
|
|
674
|
+
if (input.hook_event_name === "PostToolUse") {
|
|
675
|
+
// Handle EnterPlanMode tool - notify client of mode change after successful execution
|
|
676
|
+
if (input.tool_name === "EnterPlanMode" && options?.onEnterPlanMode) {
|
|
677
|
+
await options.onEnterPlanMode();
|
|
678
|
+
}
|
|
679
|
+
if (toolUseID) {
|
|
680
|
+
const onPostToolUseHook = toolUseCallbacks[toolUseID]?.onPostToolUseHook;
|
|
681
|
+
if (onPostToolUseHook) {
|
|
682
|
+
await onPostToolUseHook(toolUseID, input.tool_input, input.tool_response);
|
|
683
|
+
delete toolUseCallbacks[toolUseID]; // Cleanup after execution
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
logger.error(`No onPostToolUseHook found for tool use ID: ${toolUseID}`);
|
|
687
|
+
delete toolUseCallbacks[toolUseID];
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return { continue: true };
|
|
692
|
+
};
|
|
693
|
+
/**
|
|
694
|
+
* Hook callback for `TaskCreated` / `TaskCompleted` events. The SDK fires
|
|
695
|
+
* these for both user-facing TaskCreate tool calls and subagent task
|
|
696
|
+
* creation, giving us `task_id` + `task_subject` without having to parse
|
|
697
|
+
* tool_result payloads.
|
|
698
|
+
*
|
|
699
|
+
* Populating `taskState` from the hook means a later `TaskUpdate` (which
|
|
700
|
+
* typically only carries `taskId` + `status`) finds an existing entry with
|
|
701
|
+
* a real subject, instead of synthesizing a placeholder with empty content.
|
|
702
|
+
*/
|
|
703
|
+
export const createTaskHook = (options) => async (input) => {
|
|
704
|
+
const taskId = "task_id" in input && typeof input.task_id === "string" ? input.task_id : undefined;
|
|
705
|
+
if (!taskId)
|
|
706
|
+
return { continue: true };
|
|
707
|
+
if (input.hook_event_name === "TaskCreated") {
|
|
708
|
+
if (!input.task_subject)
|
|
709
|
+
return { continue: true };
|
|
710
|
+
if (options.taskState.has(taskId))
|
|
711
|
+
return { continue: true };
|
|
712
|
+
options.taskState.set(taskId, {
|
|
713
|
+
subject: input.task_subject,
|
|
714
|
+
status: "pending",
|
|
715
|
+
description: input.task_description,
|
|
716
|
+
});
|
|
717
|
+
if (options.onChange)
|
|
718
|
+
await options.onChange();
|
|
719
|
+
}
|
|
720
|
+
else if (input.hook_event_name === "TaskCompleted") {
|
|
721
|
+
const existing = options.taskState.get(taskId);
|
|
722
|
+
if (!existing || existing.status === "completed")
|
|
723
|
+
return { continue: true };
|
|
724
|
+
options.taskState.set(taskId, { ...existing, status: "completed" });
|
|
725
|
+
if (options.onChange)
|
|
726
|
+
await options.onChange();
|
|
727
|
+
}
|
|
728
|
+
return { continue: true };
|
|
729
|
+
};
|