@posthog/agent 2.1.131 → 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.
Files changed (32) 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 +116 -164
  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/tools.js +21 -11
  7. package/dist/adapters/claude/tools.js.map +1 -1
  8. package/dist/agent.js +1251 -606
  9. package/dist/agent.js.map +1 -1
  10. package/dist/posthog-api.js +2 -2
  11. package/dist/posthog-api.js.map +1 -1
  12. package/dist/server/agent-server.js +1300 -655
  13. package/dist/server/agent-server.js.map +1 -1
  14. package/dist/server/bin.cjs +1278 -635
  15. package/dist/server/bin.cjs.map +1 -1
  16. package/package.json +2 -2
  17. package/src/adapters/base-acp-agent.ts +6 -3
  18. package/src/adapters/claude/UPSTREAM.md +63 -0
  19. package/src/adapters/claude/claude-agent.ts +682 -421
  20. package/src/adapters/claude/conversion/sdk-to-acp.ts +249 -85
  21. package/src/adapters/claude/conversion/tool-use-to-acp.ts +174 -149
  22. package/src/adapters/claude/hooks.ts +53 -1
  23. package/src/adapters/claude/permissions/permission-handlers.ts +39 -21
  24. package/src/adapters/claude/session/commands.ts +13 -9
  25. package/src/adapters/claude/session/mcp-config.ts +2 -5
  26. package/src/adapters/claude/session/options.ts +58 -6
  27. package/src/adapters/claude/session/settings.ts +326 -0
  28. package/src/adapters/claude/tools.ts +1 -0
  29. package/src/adapters/claude/types.ts +38 -0
  30. package/src/execution-mode.ts +26 -10
  31. package/src/server/agent-server.test.ts +41 -1
  32. 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
- 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;
@@ -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) ?? 0;
115
+ const inputOffset = (input?.offset as number | undefined) ?? 1;
207
116
  if (inputLimit) {
208
- limit = ` (${inputOffset + 1} - ${inputOffset + inputLimit})`;
209
- } else if (inputOffset) {
210
- limit = ` (from line ${inputOffset + 1})`;
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
- 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
- }
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 "FilesWithMatches":
225
+ case "files_with_matches":
339
226
  label += " -l";
340
227
  break;
341
- case "Count":
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
- label += ` "${input?.pattern ? String(input.pattern) : ""}"`;
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
- ): Pick<ToolCallUpdate, "title" | "content" | "locations"> {
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 { type?: string; text?: string };
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
- return toAcpContentUpdate(
541
- toolResult.content,
542
- "is_error" in toolResult ? toolResult.is_error : false,
543
- );
544
- }
545
- case "Edit":
546
- case "Write": {
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
- "is_error" in toolResult &&
549
- toolResult.is_error &&
550
- toolResult.content &&
551
- toolResult.content.length > 0
526
+ result &&
527
+ typeof result === "object" &&
528
+ "type" in result &&
529
+ (result as { type: string }).type === "bash_code_execution_result"
552
530
  ) {
553
- return toAcpContentUpdate(toolResult.content, true);
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
- 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)", "")}`;
@@ -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 | LoadSessionRequest,
5
+ params: Pick<NewSessionRequest, "mcpServers">,
9
6
  ): Record<string, McpServerConfig> {
10
7
  const mcpServers: Record<string, McpServerConfig> = {};
11
8
  if (!Array.isArray(params.mcpServers)) {