@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.
Files changed (40) hide show
  1. package/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +14 -28
  2. package/dist/adapters/claude/conversion/tool-use-to-acp.js +118 -165
  3. package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -1
  4. package/dist/adapters/claude/permissions/permission-options.js +33 -0
  5. package/dist/adapters/claude/permissions/permission-options.js.map +1 -1
  6. package/dist/adapters/claude/session/jsonl-hydration.d.ts +45 -0
  7. package/dist/adapters/claude/session/jsonl-hydration.js +444 -0
  8. package/dist/adapters/claude/session/jsonl-hydration.js.map +1 -0
  9. package/dist/adapters/claude/tools.js +21 -11
  10. package/dist/adapters/claude/tools.js.map +1 -1
  11. package/dist/agent.d.ts +2 -0
  12. package/dist/agent.js +1261 -608
  13. package/dist/agent.js.map +1 -1
  14. package/dist/posthog-api.js +6 -2
  15. package/dist/posthog-api.js.map +1 -1
  16. package/dist/server/agent-server.js +1307 -657
  17. package/dist/server/agent-server.js.map +1 -1
  18. package/dist/server/bin.cjs +1285 -637
  19. package/dist/server/bin.cjs.map +1 -1
  20. package/package.json +8 -4
  21. package/src/adapters/base-acp-agent.ts +6 -3
  22. package/src/adapters/claude/UPSTREAM.md +63 -0
  23. package/src/adapters/claude/claude-agent.ts +682 -421
  24. package/src/adapters/claude/conversion/sdk-to-acp.ts +249 -85
  25. package/src/adapters/claude/conversion/tool-use-to-acp.ts +176 -150
  26. package/src/adapters/claude/hooks.ts +53 -1
  27. package/src/adapters/claude/permissions/permission-handlers.ts +39 -21
  28. package/src/adapters/claude/session/commands.ts +13 -9
  29. package/src/adapters/claude/session/jsonl-hydration.test.ts +903 -0
  30. package/src/adapters/claude/session/jsonl-hydration.ts +581 -0
  31. package/src/adapters/claude/session/mcp-config.ts +2 -5
  32. package/src/adapters/claude/session/options.ts +58 -6
  33. package/src/adapters/claude/session/settings.ts +326 -0
  34. package/src/adapters/claude/tools.ts +1 -0
  35. package/src/adapters/claude/types.ts +38 -0
  36. package/src/adapters/codex/spawn.ts +1 -1
  37. package/src/agent.ts +4 -0
  38. package/src/execution-mode.ts +26 -10
  39. package/src/server/agent-server.test.ts +41 -1
  40. 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
- cachedFileContent: { [key: string]: string },
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) : "Task",
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) ?? 0;
116
+ const inputOffset = (input?.offset as number | undefined) ?? 1;
207
117
  if (inputLimit) {
208
- limit = ` (${inputOffset + 1} - ${inputOffset + inputLimit})`;
209
- } else if (inputOffset) {
210
- limit = ` (from line ${inputOffset + 1})`;
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
- let oldText = input?.old_string ? String(input.old_string) : null;
238
- let newText = input?.new_string ? String(input.new_string) : "";
239
- let affectedLines: number[] = [];
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 "FilesWithMatches":
226
+ case "files_with_matches":
339
227
  label += " -l";
340
228
  break;
341
- case "Count":
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
- label += ` "${input?.pattern ? String(input.pattern) : ""}"`;
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
- ): Pick<ToolCallUpdate, "title" | "content" | "locations"> {
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 { type?: string; text?: string };
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
- return toAcpContentUpdate(
541
- toolResult.content,
542
- "is_error" in toolResult ? toolResult.is_error : false,
543
- );
544
- }
545
- case "Edit":
546
- case "Write": {
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
- "is_error" in toolResult &&
549
- toolResult.is_error &&
550
- toolResult.content &&
551
- toolResult.content.length > 0
527
+ result &&
528
+ typeof result === "object" &&
529
+ "type" in result &&
530
+ (result as { type: string }).type === "bash_code_execution_result"
552
531
  ) {
553
- return toAcpContentUpdate(toolResult.content, true);
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
- emitConfigOptionsUpdate: () => Promise<void>;
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, fileContentCache } = context;
136
+ const { client, sessionId, toolUseID } = context;
136
137
 
137
- const toolInfo = toolInfoFromToolUse(
138
- { name: context.toolName, input: updatedInput },
139
- fileContentCache,
140
- context.logger,
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.emitConfigOptionsUpdate();
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.emitConfigOptionsUpdate();
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, fileContentCache } = context;
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
- { name: context.toolName, input: toolInput },
259
- fileContentCache,
260
- context.logger,
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 { Query } from "@anthropic-ai/claude-agent-sdk";
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 async function getAvailableSlashCommands(
15
- q: Query,
16
- ): Promise<AvailableCommand[]> {
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 = command.argumentHint
22
- ? { hint: command.argumentHint }
23
- : null;
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)", "")}`;