@posthog/agent 2.1.125 → 2.1.137
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 +116 -164
- 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/tools.js +21 -11
- package/dist/adapters/claude/tools.js.map +1 -1
- package/dist/agent.js +1251 -640
- package/dist/agent.js.map +1 -1
- package/dist/posthog-api.js +2 -2
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.js +1295 -684
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +1278 -669
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +2 -2
- 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 +174 -149
- 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/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/execution-mode.ts +26 -10
- package/src/server/agent-server.test.ts +41 -1
- package/src/session-log-writer.ts +1 -36
- 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,114 +28,13 @@ 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;
|
|
@@ -176,6 +76,15 @@ export function toolInfoFromToolUse(
|
|
|
176
76
|
};
|
|
177
77
|
|
|
178
78
|
case "Bash":
|
|
79
|
+
if (options?.supportsTerminalOutput && options?.toolUseId) {
|
|
80
|
+
return {
|
|
81
|
+
title: input?.description
|
|
82
|
+
? String(input.description)
|
|
83
|
+
: "Execute command",
|
|
84
|
+
kind: "execute",
|
|
85
|
+
content: [{ type: "terminal", terminalId: options.toolUseId }],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
179
88
|
return {
|
|
180
89
|
title: input?.description
|
|
181
90
|
? String(input.description)
|
|
@@ -203,11 +112,11 @@ export function toolInfoFromToolUse(
|
|
|
203
112
|
case "Read": {
|
|
204
113
|
let limit = "";
|
|
205
114
|
const inputLimit = input?.limit as number | undefined;
|
|
206
|
-
const inputOffset = (input?.offset as number | undefined) ??
|
|
115
|
+
const inputOffset = (input?.offset as number | undefined) ?? 1;
|
|
207
116
|
if (inputLimit) {
|
|
208
|
-
limit = ` (${inputOffset
|
|
209
|
-
} else if (inputOffset) {
|
|
210
|
-
limit = ` (from line ${inputOffset
|
|
117
|
+
limit = ` (${inputOffset} - ${inputOffset + inputLimit - 1})`;
|
|
118
|
+
} else if (inputOffset > 1) {
|
|
119
|
+
limit = ` (from line ${inputOffset})`;
|
|
211
120
|
}
|
|
212
121
|
return {
|
|
213
122
|
title: `Read ${input?.file_path ? String(input.file_path) : "File"}${limit}`,
|
|
@@ -234,27 +143,9 @@ export function toolInfoFromToolUse(
|
|
|
234
143
|
|
|
235
144
|
case "Edit": {
|
|
236
145
|
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
|
-
}
|
|
146
|
+
const oldText = input?.old_string ? String(input.old_string) : null;
|
|
147
|
+
const newText = input?.new_string ? String(input.new_string) : "";
|
|
148
|
+
|
|
258
149
|
return {
|
|
259
150
|
title: path ? `Edit \`${path}\`` : "Edit",
|
|
260
151
|
kind: "edit",
|
|
@@ -269,11 +160,7 @@ export function toolInfoFromToolUse(
|
|
|
269
160
|
},
|
|
270
161
|
]
|
|
271
162
|
: [],
|
|
272
|
-
locations: path
|
|
273
|
-
? affectedLines.length > 0
|
|
274
|
-
? affectedLines.map((line) => ({ line, path }))
|
|
275
|
-
: [{ path }]
|
|
276
|
-
: [],
|
|
163
|
+
locations: path ? [{ path }] : [],
|
|
277
164
|
};
|
|
278
165
|
}
|
|
279
166
|
|
|
@@ -335,10 +222,10 @@ export function toolInfoFromToolUse(
|
|
|
335
222
|
|
|
336
223
|
if (input?.output_mode) {
|
|
337
224
|
switch (input.output_mode) {
|
|
338
|
-
case "
|
|
225
|
+
case "files_with_matches":
|
|
339
226
|
label += " -l";
|
|
340
227
|
break;
|
|
341
|
-
case "
|
|
228
|
+
case "count":
|
|
342
229
|
label += " -c";
|
|
343
230
|
break;
|
|
344
231
|
default:
|
|
@@ -362,7 +249,9 @@ export function toolInfoFromToolUse(
|
|
|
362
249
|
label += " -P";
|
|
363
250
|
}
|
|
364
251
|
|
|
365
|
-
|
|
252
|
+
if (input?.pattern) {
|
|
253
|
+
label += ` "${String(input.pattern)}"`;
|
|
254
|
+
}
|
|
366
255
|
|
|
367
256
|
if (input?.path) {
|
|
368
257
|
label += ` ${String(input.path)}`;
|
|
@@ -487,6 +376,70 @@ function mcpToolInfo(
|
|
|
487
376
|
};
|
|
488
377
|
}
|
|
489
378
|
|
|
379
|
+
interface StructuredPatchHunk {
|
|
380
|
+
oldStart: number;
|
|
381
|
+
oldLines: number;
|
|
382
|
+
newStart: number;
|
|
383
|
+
newLines: number;
|
|
384
|
+
lines: string[];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
interface StructuredPatch {
|
|
388
|
+
oldFileName: string;
|
|
389
|
+
newFileName: string;
|
|
390
|
+
hunks: StructuredPatchHunk[];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function toolUpdateFromEditToolResponse(
|
|
394
|
+
toolResponse: unknown,
|
|
395
|
+
): { content: ToolCallContent[]; locations: ToolCallLocation[] } | null {
|
|
396
|
+
if (!toolResponse || typeof toolResponse !== "object") return null;
|
|
397
|
+
const response = toolResponse as Record<string, unknown>;
|
|
398
|
+
|
|
399
|
+
const patches = response.structuredPatch as StructuredPatch[] | undefined;
|
|
400
|
+
if (!Array.isArray(patches) || patches.length === 0) return null;
|
|
401
|
+
|
|
402
|
+
const content: ToolCallContent[] = [];
|
|
403
|
+
const locations: ToolCallLocation[] = [];
|
|
404
|
+
|
|
405
|
+
for (const patch of patches) {
|
|
406
|
+
if (!patch.hunks || patch.hunks.length === 0) continue;
|
|
407
|
+
|
|
408
|
+
const filePath = patch.newFileName || patch.oldFileName;
|
|
409
|
+
|
|
410
|
+
const oldLines: string[] = [];
|
|
411
|
+
const newLines: string[] = [];
|
|
412
|
+
for (const hunk of patch.hunks) {
|
|
413
|
+
for (const line of hunk.lines) {
|
|
414
|
+
if (line.startsWith("-")) {
|
|
415
|
+
oldLines.push(line.slice(1));
|
|
416
|
+
} else if (line.startsWith("+")) {
|
|
417
|
+
newLines.push(line.slice(1));
|
|
418
|
+
} else if (line.startsWith(" ")) {
|
|
419
|
+
oldLines.push(line.slice(1));
|
|
420
|
+
newLines.push(line.slice(1));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
content.push({
|
|
426
|
+
type: "diff",
|
|
427
|
+
path: filePath,
|
|
428
|
+
oldText: oldLines.join("\n"),
|
|
429
|
+
newText: newLines.join("\n"),
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const firstHunk = patch.hunks[0];
|
|
433
|
+
locations.push({
|
|
434
|
+
path: filePath,
|
|
435
|
+
line: firstHunk.newStart,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (content.length === 0) return null;
|
|
440
|
+
return { content, locations };
|
|
441
|
+
}
|
|
442
|
+
|
|
490
443
|
export function toolUpdateFromToolResult(
|
|
491
444
|
toolResult:
|
|
492
445
|
| ToolResultBlockParam
|
|
@@ -499,13 +452,27 @@ export function toolUpdateFromToolResult(
|
|
|
499
452
|
| BetaRequestMCPToolResultBlockParam
|
|
500
453
|
| BetaToolSearchToolResultBlockParam,
|
|
501
454
|
toolUse: Pick<ToolUseBlock, "name" | "input"> | undefined,
|
|
502
|
-
|
|
455
|
+
options?: { supportsTerminalOutput?: boolean; toolUseId?: string },
|
|
456
|
+
): Pick<ToolCallUpdate, "title" | "content" | "locations" | "_meta"> {
|
|
457
|
+
if (
|
|
458
|
+
"is_error" in toolResult &&
|
|
459
|
+
toolResult.is_error &&
|
|
460
|
+
toolResult.content &&
|
|
461
|
+
(toolResult.content as unknown[]).length > 0
|
|
462
|
+
) {
|
|
463
|
+
return toAcpContentUpdate(toolResult.content, true);
|
|
464
|
+
}
|
|
465
|
+
|
|
503
466
|
switch (toolUse?.name) {
|
|
504
467
|
case "Read":
|
|
505
468
|
if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
|
|
506
469
|
return {
|
|
507
470
|
content: toolResult.content.map((item) => {
|
|
508
|
-
const itemObj = item as {
|
|
471
|
+
const itemObj = item as {
|
|
472
|
+
type?: string;
|
|
473
|
+
text?: string;
|
|
474
|
+
source?: { data?: string; media_type?: string };
|
|
475
|
+
};
|
|
509
476
|
if (itemObj.type === "text") {
|
|
510
477
|
return {
|
|
511
478
|
type: "content" as const,
|
|
@@ -516,6 +483,16 @@ export function toolUpdateFromToolResult(
|
|
|
516
483
|
),
|
|
517
484
|
};
|
|
518
485
|
}
|
|
486
|
+
if (itemObj.type === "image" && itemObj.source) {
|
|
487
|
+
return {
|
|
488
|
+
type: "content" as const,
|
|
489
|
+
content: {
|
|
490
|
+
type: "image" as const,
|
|
491
|
+
data: itemObj.source.data ?? "",
|
|
492
|
+
mimeType: itemObj.source.media_type ?? "image/png",
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
519
496
|
return {
|
|
520
497
|
type: "content" as const,
|
|
521
498
|
content: item as { type: "text"; text: string },
|
|
@@ -537,23 +514,71 @@ export function toolUpdateFromToolResult(
|
|
|
537
514
|
return {};
|
|
538
515
|
|
|
539
516
|
case "Bash": {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
"
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
517
|
+
const result = toolResult.content;
|
|
518
|
+
const terminalId =
|
|
519
|
+
"tool_use_id" in toolResult ? String(toolResult.tool_use_id) : "";
|
|
520
|
+
const isError = "is_error" in toolResult && toolResult.is_error;
|
|
521
|
+
|
|
522
|
+
let output = "";
|
|
523
|
+
let exitCode = isError ? 1 : 0;
|
|
524
|
+
|
|
547
525
|
if (
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
526
|
+
result &&
|
|
527
|
+
typeof result === "object" &&
|
|
528
|
+
"type" in result &&
|
|
529
|
+
(result as { type: string }).type === "bash_code_execution_result"
|
|
552
530
|
) {
|
|
553
|
-
|
|
531
|
+
const bashResult = result as {
|
|
532
|
+
stdout?: string;
|
|
533
|
+
stderr?: string;
|
|
534
|
+
return_code: number;
|
|
535
|
+
};
|
|
536
|
+
output = [bashResult.stdout, bashResult.stderr]
|
|
537
|
+
.filter(Boolean)
|
|
538
|
+
.join("\n");
|
|
539
|
+
exitCode = bashResult.return_code;
|
|
540
|
+
} else if (typeof result === "string") {
|
|
541
|
+
output = result;
|
|
542
|
+
} else if (
|
|
543
|
+
Array.isArray(result) &&
|
|
544
|
+
result.length > 0 &&
|
|
545
|
+
"text" in result[0] &&
|
|
546
|
+
typeof result[0].text === "string"
|
|
547
|
+
) {
|
|
548
|
+
output = result.map((c: { text?: string }) => c.text ?? "").join("\n");
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (options?.supportsTerminalOutput) {
|
|
552
|
+
return {
|
|
553
|
+
content: [{ type: "terminal" as const, terminalId }],
|
|
554
|
+
_meta: {
|
|
555
|
+
terminal_info: {
|
|
556
|
+
terminal_id: terminalId,
|
|
557
|
+
},
|
|
558
|
+
terminal_output: {
|
|
559
|
+
terminal_id: terminalId,
|
|
560
|
+
data: output,
|
|
561
|
+
},
|
|
562
|
+
terminal_exit: {
|
|
563
|
+
terminal_id: terminalId,
|
|
564
|
+
exit_code: exitCode,
|
|
565
|
+
signal: null,
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (output.trim()) {
|
|
571
|
+
return {
|
|
572
|
+
content: toolContent()
|
|
573
|
+
.text(`\`\`\`console\n${output.trimEnd()}\n\`\`\``)
|
|
574
|
+
.build(),
|
|
575
|
+
};
|
|
554
576
|
}
|
|
555
577
|
return {};
|
|
556
578
|
}
|
|
579
|
+
case "Edit":
|
|
580
|
+
case "Write":
|
|
581
|
+
return {};
|
|
557
582
|
|
|
558
583
|
case "ExitPlanMode": {
|
|
559
584
|
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)", "")}`;
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
LoadSessionRequest,
|
|
3
|
-
NewSessionRequest,
|
|
4
|
-
} from "@agentclientprotocol/sdk";
|
|
1
|
+
import type { NewSessionRequest } from "@agentclientprotocol/sdk";
|
|
5
2
|
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
|
6
3
|
|
|
7
4
|
export function parseMcpServers(
|
|
8
|
-
params: NewSessionRequest
|
|
5
|
+
params: Pick<NewSessionRequest, "mcpServers">,
|
|
9
6
|
): Record<string, McpServerConfig> {
|
|
10
7
|
const mcpServers: Record<string, McpServerConfig> = {};
|
|
11
8
|
if (!Array.isArray(params.mcpServers)) {
|