@posthog/agent 2.1.131 → 2.1.138
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/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +14 -28
- package/dist/adapters/claude/conversion/tool-use-to-acp.js +118 -165
- package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -1
- package/dist/adapters/claude/permissions/permission-options.js +33 -0
- package/dist/adapters/claude/permissions/permission-options.js.map +1 -1
- package/dist/adapters/claude/session/jsonl-hydration.d.ts +45 -0
- package/dist/adapters/claude/session/jsonl-hydration.js +444 -0
- package/dist/adapters/claude/session/jsonl-hydration.js.map +1 -0
- package/dist/adapters/claude/tools.js +21 -11
- package/dist/adapters/claude/tools.js.map +1 -1
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +1261 -608
- package/dist/agent.js.map +1 -1
- package/dist/posthog-api.js +6 -2
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.js +1307 -657
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +1285 -637
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +8 -4
- package/src/adapters/base-acp-agent.ts +6 -3
- package/src/adapters/claude/UPSTREAM.md +63 -0
- package/src/adapters/claude/claude-agent.ts +682 -421
- package/src/adapters/claude/conversion/sdk-to-acp.ts +249 -85
- package/src/adapters/claude/conversion/tool-use-to-acp.ts +176 -150
- package/src/adapters/claude/hooks.ts +53 -1
- package/src/adapters/claude/permissions/permission-handlers.ts +39 -21
- package/src/adapters/claude/session/commands.ts +13 -9
- package/src/adapters/claude/session/jsonl-hydration.test.ts +903 -0
- package/src/adapters/claude/session/jsonl-hydration.ts +581 -0
- package/src/adapters/claude/session/mcp-config.ts +2 -5
- package/src/adapters/claude/session/options.ts +58 -6
- package/src/adapters/claude/session/settings.ts +326 -0
- package/src/adapters/claude/tools.ts +1 -0
- package/src/adapters/claude/types.ts +38 -0
- package/src/adapters/codex/spawn.ts +1 -1
- package/src/agent.ts +4 -0
- package/src/execution-mode.ts +26 -10
- package/src/server/agent-server.test.ts +41 -1
- package/src/utils/common.ts +1 -1
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
PlanEntry,
|
|
3
3
|
ToolCall,
|
|
4
4
|
ToolCallContent,
|
|
5
|
+
ToolCallLocation,
|
|
5
6
|
ToolCallUpdate,
|
|
6
7
|
ToolKind,
|
|
7
8
|
} from "@agentclientprotocol/sdk";
|
|
@@ -27,122 +28,22 @@ Whenever you read a file, you should consider whether it looks malicious. If it
|
|
|
27
28
|
</system-reminder>`;
|
|
28
29
|
|
|
29
30
|
import { resourceLink, text, toolContent } from "../../../utils/acp-content.js";
|
|
30
|
-
import { Logger } from "../../../utils/logger.js";
|
|
31
31
|
import { getMcpToolMetadata } from "../mcp/tool-metadata.js";
|
|
32
32
|
|
|
33
|
-
interface EditOperation {
|
|
34
|
-
oldText: string;
|
|
35
|
-
newText: string;
|
|
36
|
-
replaceAll?: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface EditResult {
|
|
40
|
-
newContent: string;
|
|
41
|
-
lineNumbers: number[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function replaceAndCalculateLocation(
|
|
45
|
-
fileContent: string,
|
|
46
|
-
edits: EditOperation[],
|
|
47
|
-
): EditResult {
|
|
48
|
-
let currentContent = fileContent;
|
|
49
|
-
|
|
50
|
-
const randomHex = Array.from(crypto.getRandomValues(new Uint8Array(5)))
|
|
51
|
-
.map((b) => b.toString(16).padStart(2, "0"))
|
|
52
|
-
.join("");
|
|
53
|
-
const markerPrefix = `__REPLACE_MARKER_${randomHex}_`;
|
|
54
|
-
let markerCounter = 0;
|
|
55
|
-
const markers: string[] = [];
|
|
56
|
-
|
|
57
|
-
for (const edit of edits) {
|
|
58
|
-
if (edit.oldText === "") {
|
|
59
|
-
throw new Error(
|
|
60
|
-
`The provided \`old_string\` is empty.\n\nNo edits were applied.`,
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (edit.replaceAll) {
|
|
65
|
-
const parts: string[] = [];
|
|
66
|
-
let lastIndex = 0;
|
|
67
|
-
let searchIndex = 0;
|
|
68
|
-
|
|
69
|
-
while (true) {
|
|
70
|
-
const index = currentContent.indexOf(edit.oldText, searchIndex);
|
|
71
|
-
if (index === -1) {
|
|
72
|
-
if (searchIndex === 0) {
|
|
73
|
-
throw new Error(
|
|
74
|
-
`The provided \`old_string\` does not appear in the file: "${edit.oldText}".\n\nNo edits were applied.`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
parts.push(currentContent.substring(lastIndex, index));
|
|
81
|
-
|
|
82
|
-
const marker = `${markerPrefix}${markerCounter++}__`;
|
|
83
|
-
markers.push(marker);
|
|
84
|
-
parts.push(marker + edit.newText);
|
|
85
|
-
|
|
86
|
-
lastIndex = index + edit.oldText.length;
|
|
87
|
-
searchIndex = lastIndex;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
parts.push(currentContent.substring(lastIndex));
|
|
91
|
-
currentContent = parts.join("");
|
|
92
|
-
} else {
|
|
93
|
-
const index = currentContent.indexOf(edit.oldText);
|
|
94
|
-
if (index === -1) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
`The provided \`old_string\` does not appear in the file: "${edit.oldText}".\n\nNo edits were applied.`,
|
|
97
|
-
);
|
|
98
|
-
} else {
|
|
99
|
-
const marker = `${markerPrefix}${markerCounter++}__`;
|
|
100
|
-
markers.push(marker);
|
|
101
|
-
currentContent =
|
|
102
|
-
currentContent.substring(0, index) +
|
|
103
|
-
marker +
|
|
104
|
-
edit.newText +
|
|
105
|
-
currentContent.substring(index + edit.oldText.length);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const lineNumbers: number[] = [];
|
|
111
|
-
for (const marker of markers) {
|
|
112
|
-
const index = currentContent.indexOf(marker);
|
|
113
|
-
if (index !== -1) {
|
|
114
|
-
const lineNumber = Math.max(
|
|
115
|
-
0,
|
|
116
|
-
currentContent.substring(0, index).split(/\r\n|\r|\n/).length - 1,
|
|
117
|
-
);
|
|
118
|
-
lineNumbers.push(lineNumber);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
let finalContent = currentContent;
|
|
123
|
-
for (const marker of markers) {
|
|
124
|
-
finalContent = finalContent.replace(marker, "");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const uniqueLineNumbers = [...new Set(lineNumbers)].sort();
|
|
128
|
-
|
|
129
|
-
return { newContent: finalContent, lineNumbers: uniqueLineNumbers };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
33
|
type ToolInfo = Pick<ToolCall, "title" | "kind" | "content" | "locations">;
|
|
133
34
|
|
|
134
35
|
export function toolInfoFromToolUse(
|
|
135
36
|
toolUse: Pick<ToolUseBlock, "name" | "input">,
|
|
136
|
-
|
|
137
|
-
logger: Logger = new Logger({ debug: false, prefix: "[ClaudeTools]" }),
|
|
37
|
+
options?: { supportsTerminalOutput?: boolean; toolUseId?: string },
|
|
138
38
|
): ToolInfo {
|
|
139
39
|
const name = toolUse.name;
|
|
140
40
|
const input = toolUse.input as Record<string, unknown> | undefined;
|
|
141
41
|
|
|
142
42
|
switch (name) {
|
|
143
43
|
case "Task":
|
|
44
|
+
case "Agent":
|
|
144
45
|
return {
|
|
145
|
-
title: input?.description ? String(input.description) :
|
|
46
|
+
title: input?.description ? String(input.description) : name,
|
|
146
47
|
kind: "think",
|
|
147
48
|
content: input?.prompt
|
|
148
49
|
? toolContent().text(String(input.prompt)).build()
|
|
@@ -176,6 +77,15 @@ export function toolInfoFromToolUse(
|
|
|
176
77
|
};
|
|
177
78
|
|
|
178
79
|
case "Bash":
|
|
80
|
+
if (options?.supportsTerminalOutput && options?.toolUseId) {
|
|
81
|
+
return {
|
|
82
|
+
title: input?.description
|
|
83
|
+
? String(input.description)
|
|
84
|
+
: "Execute command",
|
|
85
|
+
kind: "execute",
|
|
86
|
+
content: [{ type: "terminal", terminalId: options.toolUseId }],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
179
89
|
return {
|
|
180
90
|
title: input?.description
|
|
181
91
|
? String(input.description)
|
|
@@ -203,11 +113,11 @@ export function toolInfoFromToolUse(
|
|
|
203
113
|
case "Read": {
|
|
204
114
|
let limit = "";
|
|
205
115
|
const inputLimit = input?.limit as number | undefined;
|
|
206
|
-
const inputOffset = (input?.offset as number | undefined) ??
|
|
116
|
+
const inputOffset = (input?.offset as number | undefined) ?? 1;
|
|
207
117
|
if (inputLimit) {
|
|
208
|
-
limit = ` (${inputOffset
|
|
209
|
-
} else if (inputOffset) {
|
|
210
|
-
limit = ` (from line ${inputOffset
|
|
118
|
+
limit = ` (${inputOffset} - ${inputOffset + inputLimit - 1})`;
|
|
119
|
+
} else if (inputOffset > 1) {
|
|
120
|
+
limit = ` (from line ${inputOffset})`;
|
|
211
121
|
}
|
|
212
122
|
return {
|
|
213
123
|
title: `Read ${input?.file_path ? String(input.file_path) : "File"}${limit}`,
|
|
@@ -234,27 +144,9 @@ export function toolInfoFromToolUse(
|
|
|
234
144
|
|
|
235
145
|
case "Edit": {
|
|
236
146
|
const path = input?.file_path ? String(input.file_path) : undefined;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (path && oldText) {
|
|
242
|
-
try {
|
|
243
|
-
const oldContent = cachedFileContent[path] || "";
|
|
244
|
-
const newContent = replaceAndCalculateLocation(oldContent, [
|
|
245
|
-
{
|
|
246
|
-
oldText,
|
|
247
|
-
newText,
|
|
248
|
-
replaceAll: false,
|
|
249
|
-
},
|
|
250
|
-
]);
|
|
251
|
-
oldText = oldContent;
|
|
252
|
-
newText = newContent.newContent;
|
|
253
|
-
affectedLines = newContent.lineNumbers;
|
|
254
|
-
} catch (e) {
|
|
255
|
-
logger.error("Failed to edit file", e);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
147
|
+
const oldText = input?.old_string ? String(input.old_string) : null;
|
|
148
|
+
const newText = input?.new_string ? String(input.new_string) : "";
|
|
149
|
+
|
|
258
150
|
return {
|
|
259
151
|
title: path ? `Edit \`${path}\`` : "Edit",
|
|
260
152
|
kind: "edit",
|
|
@@ -269,11 +161,7 @@ export function toolInfoFromToolUse(
|
|
|
269
161
|
},
|
|
270
162
|
]
|
|
271
163
|
: [],
|
|
272
|
-
locations: path
|
|
273
|
-
? affectedLines.length > 0
|
|
274
|
-
? affectedLines.map((line) => ({ line, path }))
|
|
275
|
-
: [{ path }]
|
|
276
|
-
: [],
|
|
164
|
+
locations: path ? [{ path }] : [],
|
|
277
165
|
};
|
|
278
166
|
}
|
|
279
167
|
|
|
@@ -335,10 +223,10 @@ export function toolInfoFromToolUse(
|
|
|
335
223
|
|
|
336
224
|
if (input?.output_mode) {
|
|
337
225
|
switch (input.output_mode) {
|
|
338
|
-
case "
|
|
226
|
+
case "files_with_matches":
|
|
339
227
|
label += " -l";
|
|
340
228
|
break;
|
|
341
|
-
case "
|
|
229
|
+
case "count":
|
|
342
230
|
label += " -c";
|
|
343
231
|
break;
|
|
344
232
|
default:
|
|
@@ -362,7 +250,9 @@ export function toolInfoFromToolUse(
|
|
|
362
250
|
label += " -P";
|
|
363
251
|
}
|
|
364
252
|
|
|
365
|
-
|
|
253
|
+
if (input?.pattern) {
|
|
254
|
+
label += ` "${String(input.pattern)}"`;
|
|
255
|
+
}
|
|
366
256
|
|
|
367
257
|
if (input?.path) {
|
|
368
258
|
label += ` ${String(input.path)}`;
|
|
@@ -487,6 +377,70 @@ function mcpToolInfo(
|
|
|
487
377
|
};
|
|
488
378
|
}
|
|
489
379
|
|
|
380
|
+
interface StructuredPatchHunk {
|
|
381
|
+
oldStart: number;
|
|
382
|
+
oldLines: number;
|
|
383
|
+
newStart: number;
|
|
384
|
+
newLines: number;
|
|
385
|
+
lines: string[];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
interface StructuredPatch {
|
|
389
|
+
oldFileName: string;
|
|
390
|
+
newFileName: string;
|
|
391
|
+
hunks: StructuredPatchHunk[];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function toolUpdateFromEditToolResponse(
|
|
395
|
+
toolResponse: unknown,
|
|
396
|
+
): { content: ToolCallContent[]; locations: ToolCallLocation[] } | null {
|
|
397
|
+
if (!toolResponse || typeof toolResponse !== "object") return null;
|
|
398
|
+
const response = toolResponse as Record<string, unknown>;
|
|
399
|
+
|
|
400
|
+
const patches = response.structuredPatch as StructuredPatch[] | undefined;
|
|
401
|
+
if (!Array.isArray(patches) || patches.length === 0) return null;
|
|
402
|
+
|
|
403
|
+
const content: ToolCallContent[] = [];
|
|
404
|
+
const locations: ToolCallLocation[] = [];
|
|
405
|
+
|
|
406
|
+
for (const patch of patches) {
|
|
407
|
+
if (!patch.hunks || patch.hunks.length === 0) continue;
|
|
408
|
+
|
|
409
|
+
const filePath = patch.newFileName || patch.oldFileName;
|
|
410
|
+
|
|
411
|
+
const oldLines: string[] = [];
|
|
412
|
+
const newLines: string[] = [];
|
|
413
|
+
for (const hunk of patch.hunks) {
|
|
414
|
+
for (const line of hunk.lines) {
|
|
415
|
+
if (line.startsWith("-")) {
|
|
416
|
+
oldLines.push(line.slice(1));
|
|
417
|
+
} else if (line.startsWith("+")) {
|
|
418
|
+
newLines.push(line.slice(1));
|
|
419
|
+
} else if (line.startsWith(" ")) {
|
|
420
|
+
oldLines.push(line.slice(1));
|
|
421
|
+
newLines.push(line.slice(1));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
content.push({
|
|
427
|
+
type: "diff",
|
|
428
|
+
path: filePath,
|
|
429
|
+
oldText: oldLines.join("\n"),
|
|
430
|
+
newText: newLines.join("\n"),
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const firstHunk = patch.hunks[0];
|
|
434
|
+
locations.push({
|
|
435
|
+
path: filePath,
|
|
436
|
+
line: firstHunk.newStart,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (content.length === 0) return null;
|
|
441
|
+
return { content, locations };
|
|
442
|
+
}
|
|
443
|
+
|
|
490
444
|
export function toolUpdateFromToolResult(
|
|
491
445
|
toolResult:
|
|
492
446
|
| ToolResultBlockParam
|
|
@@ -499,13 +453,27 @@ export function toolUpdateFromToolResult(
|
|
|
499
453
|
| BetaRequestMCPToolResultBlockParam
|
|
500
454
|
| BetaToolSearchToolResultBlockParam,
|
|
501
455
|
toolUse: Pick<ToolUseBlock, "name" | "input"> | undefined,
|
|
502
|
-
|
|
456
|
+
options?: { supportsTerminalOutput?: boolean; toolUseId?: string },
|
|
457
|
+
): Pick<ToolCallUpdate, "title" | "content" | "locations" | "_meta"> {
|
|
458
|
+
if (
|
|
459
|
+
"is_error" in toolResult &&
|
|
460
|
+
toolResult.is_error &&
|
|
461
|
+
toolResult.content &&
|
|
462
|
+
(toolResult.content as unknown[]).length > 0
|
|
463
|
+
) {
|
|
464
|
+
return toAcpContentUpdate(toolResult.content, true);
|
|
465
|
+
}
|
|
466
|
+
|
|
503
467
|
switch (toolUse?.name) {
|
|
504
468
|
case "Read":
|
|
505
469
|
if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
|
|
506
470
|
return {
|
|
507
471
|
content: toolResult.content.map((item) => {
|
|
508
|
-
const itemObj = item as {
|
|
472
|
+
const itemObj = item as {
|
|
473
|
+
type?: string;
|
|
474
|
+
text?: string;
|
|
475
|
+
source?: { data?: string; media_type?: string };
|
|
476
|
+
};
|
|
509
477
|
if (itemObj.type === "text") {
|
|
510
478
|
return {
|
|
511
479
|
type: "content" as const,
|
|
@@ -516,6 +484,16 @@ export function toolUpdateFromToolResult(
|
|
|
516
484
|
),
|
|
517
485
|
};
|
|
518
486
|
}
|
|
487
|
+
if (itemObj.type === "image" && itemObj.source) {
|
|
488
|
+
return {
|
|
489
|
+
type: "content" as const,
|
|
490
|
+
content: {
|
|
491
|
+
type: "image" as const,
|
|
492
|
+
data: itemObj.source.data ?? "",
|
|
493
|
+
mimeType: itemObj.source.media_type ?? "image/png",
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
}
|
|
519
497
|
return {
|
|
520
498
|
type: "content" as const,
|
|
521
499
|
content: item as { type: "text"; text: string },
|
|
@@ -537,23 +515,71 @@ export function toolUpdateFromToolResult(
|
|
|
537
515
|
return {};
|
|
538
516
|
|
|
539
517
|
case "Bash": {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
"
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
518
|
+
const result = toolResult.content;
|
|
519
|
+
const terminalId =
|
|
520
|
+
"tool_use_id" in toolResult ? String(toolResult.tool_use_id) : "";
|
|
521
|
+
const isError = "is_error" in toolResult && toolResult.is_error;
|
|
522
|
+
|
|
523
|
+
let output = "";
|
|
524
|
+
let exitCode = isError ? 1 : 0;
|
|
525
|
+
|
|
547
526
|
if (
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
527
|
+
result &&
|
|
528
|
+
typeof result === "object" &&
|
|
529
|
+
"type" in result &&
|
|
530
|
+
(result as { type: string }).type === "bash_code_execution_result"
|
|
552
531
|
) {
|
|
553
|
-
|
|
532
|
+
const bashResult = result as {
|
|
533
|
+
stdout?: string;
|
|
534
|
+
stderr?: string;
|
|
535
|
+
return_code: number;
|
|
536
|
+
};
|
|
537
|
+
output = [bashResult.stdout, bashResult.stderr]
|
|
538
|
+
.filter(Boolean)
|
|
539
|
+
.join("\n");
|
|
540
|
+
exitCode = bashResult.return_code;
|
|
541
|
+
} else if (typeof result === "string") {
|
|
542
|
+
output = result;
|
|
543
|
+
} else if (
|
|
544
|
+
Array.isArray(result) &&
|
|
545
|
+
result.length > 0 &&
|
|
546
|
+
"text" in result[0] &&
|
|
547
|
+
typeof result[0].text === "string"
|
|
548
|
+
) {
|
|
549
|
+
output = result.map((c: { text?: string }) => c.text ?? "").join("\n");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (options?.supportsTerminalOutput) {
|
|
553
|
+
return {
|
|
554
|
+
content: [{ type: "terminal" as const, terminalId }],
|
|
555
|
+
_meta: {
|
|
556
|
+
terminal_info: {
|
|
557
|
+
terminal_id: terminalId,
|
|
558
|
+
},
|
|
559
|
+
terminal_output: {
|
|
560
|
+
terminal_id: terminalId,
|
|
561
|
+
data: output,
|
|
562
|
+
},
|
|
563
|
+
terminal_exit: {
|
|
564
|
+
terminal_id: terminalId,
|
|
565
|
+
exit_code: exitCode,
|
|
566
|
+
signal: null,
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
if (output.trim()) {
|
|
572
|
+
return {
|
|
573
|
+
content: toolContent()
|
|
574
|
+
.text(`\`\`\`console\n${output.trimEnd()}\n\`\`\``)
|
|
575
|
+
.build(),
|
|
576
|
+
};
|
|
554
577
|
}
|
|
555
578
|
return {};
|
|
556
579
|
}
|
|
580
|
+
case "Edit":
|
|
581
|
+
case "Write":
|
|
582
|
+
return {};
|
|
557
583
|
|
|
558
584
|
case "ExitPlanMode": {
|
|
559
585
|
return { title: "Exited Plan Mode" };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { HookCallback, HookInput } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import type { Logger } from "../../utils/logger.js";
|
|
3
|
+
import type { SettingsManager } from "./session/settings.js";
|
|
2
4
|
import type { TwigExecutionMode } from "./tools.js";
|
|
3
5
|
|
|
4
6
|
const toolUseCallbacks: {
|
|
@@ -32,10 +34,11 @@ export type OnModeChange = (mode: TwigExecutionMode) => Promise<void>;
|
|
|
32
34
|
|
|
33
35
|
interface CreatePostToolUseHookParams {
|
|
34
36
|
onModeChange?: OnModeChange;
|
|
37
|
+
logger?: Logger;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
export const createPostToolUseHook =
|
|
38
|
-
({ onModeChange }: CreatePostToolUseHookParams): HookCallback =>
|
|
41
|
+
({ onModeChange, logger }: CreatePostToolUseHookParams): HookCallback =>
|
|
39
42
|
async (
|
|
40
43
|
input: HookInput,
|
|
41
44
|
toolUseID: string | undefined,
|
|
@@ -57,8 +60,57 @@ export const createPostToolUseHook =
|
|
|
57
60
|
input.tool_response,
|
|
58
61
|
);
|
|
59
62
|
delete toolUseCallbacks[toolUseID];
|
|
63
|
+
} else {
|
|
64
|
+
logger?.error(
|
|
65
|
+
`No onPostToolUseHook found for tool use ID: ${toolUseID}`,
|
|
66
|
+
);
|
|
67
|
+
delete toolUseCallbacks[toolUseID];
|
|
60
68
|
}
|
|
61
69
|
}
|
|
62
70
|
}
|
|
63
71
|
return { continue: true };
|
|
64
72
|
};
|
|
73
|
+
|
|
74
|
+
export const createPreToolUseHook =
|
|
75
|
+
(settingsManager: SettingsManager, logger: Logger): HookCallback =>
|
|
76
|
+
async (input: HookInput, _toolUseID: string | undefined) => {
|
|
77
|
+
if (input.hook_event_name !== "PreToolUse") {
|
|
78
|
+
return { continue: true };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const toolName = input.tool_name;
|
|
82
|
+
const toolInput = input.tool_input;
|
|
83
|
+
const permissionCheck = settingsManager.checkPermission(
|
|
84
|
+
toolName,
|
|
85
|
+
toolInput,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (permissionCheck.decision !== "ask") {
|
|
89
|
+
logger.info(
|
|
90
|
+
`[PreToolUseHook] Tool: ${toolName}, Decision: ${permissionCheck.decision}, Rule: ${permissionCheck.rule}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
switch (permissionCheck.decision) {
|
|
95
|
+
case "allow":
|
|
96
|
+
return {
|
|
97
|
+
continue: true,
|
|
98
|
+
hookSpecificOutput: {
|
|
99
|
+
hookEventName: "PreToolUse" as const,
|
|
100
|
+
permissionDecision: "allow" as const,
|
|
101
|
+
permissionDecisionReason: `Allowed by settings rule: ${permissionCheck.rule}`,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
case "deny":
|
|
105
|
+
return {
|
|
106
|
+
continue: true,
|
|
107
|
+
hookSpecificOutput: {
|
|
108
|
+
hookEventName: "PreToolUse" as const,
|
|
109
|
+
permissionDecision: "deny" as const,
|
|
110
|
+
permissionDecisionReason: `Denied by settings rule: ${permissionCheck.rule}`,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
default:
|
|
114
|
+
return { continue: true };
|
|
115
|
+
}
|
|
116
|
+
};
|
|
@@ -43,11 +43,12 @@ interface ToolHandlerContext {
|
|
|
43
43
|
toolInput: Record<string, unknown>;
|
|
44
44
|
toolUseID: string;
|
|
45
45
|
suggestions?: PermissionUpdate[];
|
|
46
|
+
signal?: AbortSignal;
|
|
46
47
|
client: AgentSideConnection;
|
|
47
48
|
sessionId: string;
|
|
48
49
|
fileContentCache: { [key: string]: string };
|
|
49
50
|
logger: Logger;
|
|
50
|
-
|
|
51
|
+
updateConfigOption: (configId: string, value: string) => Promise<void>;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
async function emitToolDenial(
|
|
@@ -132,13 +133,12 @@ async function requestPlanApproval(
|
|
|
132
133
|
context: ToolHandlerContext,
|
|
133
134
|
updatedInput: Record<string, unknown>,
|
|
134
135
|
): Promise<RequestPermissionResponse> {
|
|
135
|
-
const { client, sessionId, toolUseID
|
|
136
|
+
const { client, sessionId, toolUseID } = context;
|
|
136
137
|
|
|
137
|
-
const toolInfo = toolInfoFromToolUse(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
);
|
|
138
|
+
const toolInfo = toolInfoFromToolUse({
|
|
139
|
+
name: context.toolName,
|
|
140
|
+
input: updatedInput,
|
|
141
|
+
});
|
|
142
142
|
|
|
143
143
|
return await client.requestPermission({
|
|
144
144
|
options: buildExitPlanModePermissionOptions(),
|
|
@@ -168,7 +168,14 @@ async function applyPlanApproval(
|
|
|
168
168
|
) {
|
|
169
169
|
session.permissionMode = response.outcome.optionId;
|
|
170
170
|
await session.query.setPermissionMode(response.outcome.optionId);
|
|
171
|
-
await context.
|
|
171
|
+
await context.client.sessionUpdate({
|
|
172
|
+
sessionId: context.sessionId,
|
|
173
|
+
update: {
|
|
174
|
+
sessionUpdate: "current_mode_update",
|
|
175
|
+
currentModeId: response.outcome.optionId,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
await context.updateConfigOption("mode", response.outcome.optionId);
|
|
172
179
|
|
|
173
180
|
return {
|
|
174
181
|
behavior: "allow",
|
|
@@ -196,7 +203,7 @@ async function handleEnterPlanModeTool(
|
|
|
196
203
|
|
|
197
204
|
session.permissionMode = "plan";
|
|
198
205
|
await session.query.setPermissionMode("plan");
|
|
199
|
-
await context.
|
|
206
|
+
await context.updateConfigOption("mode", "plan");
|
|
200
207
|
|
|
201
208
|
return {
|
|
202
209
|
behavior: "allow",
|
|
@@ -221,6 +228,9 @@ async function handleExitPlanModeTool(
|
|
|
221
228
|
}
|
|
222
229
|
|
|
223
230
|
const response = await requestPlanApproval(context, updatedInput);
|
|
231
|
+
if (context.signal?.aborted || response.outcome?.outcome === "cancelled") {
|
|
232
|
+
throw new Error("Tool use aborted");
|
|
233
|
+
}
|
|
224
234
|
return await applyPlanApproval(response, context, updatedInput);
|
|
225
235
|
}
|
|
226
236
|
|
|
@@ -250,15 +260,14 @@ async function handleAskUserQuestionTool(
|
|
|
250
260
|
};
|
|
251
261
|
}
|
|
252
262
|
|
|
253
|
-
const { client, sessionId, toolUseID, toolInput
|
|
263
|
+
const { client, sessionId, toolUseID, toolInput } = context;
|
|
254
264
|
const firstQuestion = questions[0];
|
|
255
265
|
const options = buildQuestionOptions(firstQuestion);
|
|
256
266
|
|
|
257
|
-
const toolInfo = toolInfoFromToolUse(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
);
|
|
267
|
+
const toolInfo = toolInfoFromToolUse({
|
|
268
|
+
name: context.toolName,
|
|
269
|
+
input: toolInput,
|
|
270
|
+
});
|
|
262
271
|
|
|
263
272
|
const response = await client.requestPermission({
|
|
264
273
|
options,
|
|
@@ -275,6 +284,10 @@ async function handleAskUserQuestionTool(
|
|
|
275
284
|
},
|
|
276
285
|
});
|
|
277
286
|
|
|
287
|
+
if (context.signal?.aborted || response.outcome?.outcome === "cancelled") {
|
|
288
|
+
throw new Error("Tool use aborted");
|
|
289
|
+
}
|
|
290
|
+
|
|
278
291
|
if (response.outcome?.outcome !== "selected") {
|
|
279
292
|
const customMessage = (
|
|
280
293
|
response._meta as Record<string, unknown> | undefined
|
|
@@ -317,15 +330,10 @@ async function handleDefaultPermissionFlow(
|
|
|
317
330
|
toolUseID,
|
|
318
331
|
client,
|
|
319
332
|
sessionId,
|
|
320
|
-
fileContentCache,
|
|
321
333
|
suggestions,
|
|
322
334
|
} = context;
|
|
323
335
|
|
|
324
|
-
const toolInfo = toolInfoFromToolUse(
|
|
325
|
-
{ name: toolName, input: toolInput },
|
|
326
|
-
fileContentCache,
|
|
327
|
-
context.logger,
|
|
328
|
-
);
|
|
336
|
+
const toolInfo = toolInfoFromToolUse({ name: toolName, input: toolInput });
|
|
329
337
|
|
|
330
338
|
const options = buildPermissionOptions(
|
|
331
339
|
toolName,
|
|
@@ -347,6 +355,10 @@ async function handleDefaultPermissionFlow(
|
|
|
347
355
|
},
|
|
348
356
|
});
|
|
349
357
|
|
|
358
|
+
if (context.signal?.aborted || response.outcome?.outcome === "cancelled") {
|
|
359
|
+
throw new Error("Tool use aborted");
|
|
360
|
+
}
|
|
361
|
+
|
|
350
362
|
if (
|
|
351
363
|
response.outcome?.outcome === "selected" &&
|
|
352
364
|
(response.outcome.optionId === "allow" ||
|
|
@@ -436,5 +448,11 @@ export async function canUseTool(
|
|
|
436
448
|
return planFileResult;
|
|
437
449
|
}
|
|
438
450
|
|
|
451
|
+
// if (session.permissionMode === "dontAsk") {
|
|
452
|
+
// const message = "Tool not pre-approved. Denied by dontAsk mode.";
|
|
453
|
+
// await emitToolDenial(context, message);
|
|
454
|
+
// return { behavior: "deny", message, interrupt: false };
|
|
455
|
+
// }
|
|
456
|
+
|
|
439
457
|
return handleDefaultPermissionFlow(context);
|
|
440
458
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { AvailableCommand } from "@agentclientprotocol/sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SlashCommand } from "@anthropic-ai/claude-agent-sdk";
|
|
3
3
|
|
|
4
4
|
const UNSUPPORTED_COMMANDS = [
|
|
5
5
|
"context",
|
|
6
6
|
"cost",
|
|
7
|
+
"keybindings-help",
|
|
7
8
|
"login",
|
|
8
9
|
"logout",
|
|
9
10
|
"output-style:new",
|
|
@@ -11,16 +12,19 @@ const UNSUPPORTED_COMMANDS = [
|
|
|
11
12
|
"todos",
|
|
12
13
|
];
|
|
13
14
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
):
|
|
17
|
-
const commands = await q.supportedCommands();
|
|
18
|
-
|
|
15
|
+
export function getAvailableSlashCommands(
|
|
16
|
+
commands: SlashCommand[],
|
|
17
|
+
): AvailableCommand[] {
|
|
19
18
|
return commands
|
|
20
19
|
.map((command) => {
|
|
21
|
-
const input =
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const input =
|
|
21
|
+
command.argumentHint != null
|
|
22
|
+
? {
|
|
23
|
+
hint: Array.isArray(command.argumentHint)
|
|
24
|
+
? command.argumentHint.join(" ")
|
|
25
|
+
: command.argumentHint,
|
|
26
|
+
}
|
|
27
|
+
: null;
|
|
24
28
|
let name = command.name;
|
|
25
29
|
if (command.name.endsWith(" (MCP)")) {
|
|
26
30
|
name = `mcp:${name.replace(" (MCP)", "")}`;
|