@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
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import type {
6
+ CanUseTool,
6
7
  McpServerConfig,
7
8
  Options,
8
9
  SpawnedProcess,
@@ -10,8 +11,14 @@ import type {
10
11
  } from "@anthropic-ai/claude-agent-sdk";
11
12
  import { IS_ROOT } from "../../../utils/common.js";
12
13
  import type { Logger } from "../../../utils/logger.js";
13
- import { createPostToolUseHook, type OnModeChange } from "../hooks.js";
14
+ import {
15
+ createPostToolUseHook,
16
+ createPreToolUseHook,
17
+ type OnModeChange,
18
+ } from "../hooks.js";
14
19
  import type { TwigExecutionMode } from "../tools.js";
20
+ import { DEFAULT_MODEL } from "./models.js";
21
+ import type { SettingsManager } from "./settings.js";
15
22
 
16
23
  export interface ProcessSpawnedInfo {
17
24
  pid: number;
@@ -23,13 +30,16 @@ export interface BuildOptionsParams {
23
30
  cwd: string;
24
31
  mcpServers: Record<string, McpServerConfig>;
25
32
  permissionMode: TwigExecutionMode;
26
- canUseTool: Options["canUseTool"];
33
+ canUseTool: CanUseTool;
27
34
  logger: Logger;
28
35
  systemPrompt?: Options["systemPrompt"];
29
36
  userProvidedOptions?: Options;
30
37
  sessionId: string;
31
38
  isResume: boolean;
39
+ forkSession?: boolean;
32
40
  additionalDirectories?: string[];
41
+ disableBuiltInTools?: boolean;
42
+ settingsManager: SettingsManager;
33
43
  onModeChange?: OnModeChange;
34
44
  onProcessSpawned?: (info: ProcessSpawnedInfo) => void;
35
45
  onProcessExited?: (pid: number) => void;
@@ -95,14 +105,22 @@ function buildEnvironment(): Record<string, string> {
95
105
 
96
106
  function buildHooks(
97
107
  userHooks: Options["hooks"],
98
- onModeChange?: OnModeChange,
108
+ onModeChange: OnModeChange | undefined,
109
+ settingsManager: SettingsManager,
110
+ logger: Logger,
99
111
  ): Options["hooks"] {
100
112
  return {
101
113
  ...userHooks,
102
114
  PostToolUse: [
103
115
  ...(userHooks?.PostToolUse || []),
104
116
  {
105
- hooks: [createPostToolUseHook({ onModeChange })],
117
+ hooks: [createPostToolUseHook({ onModeChange, logger })],
118
+ },
119
+ ],
120
+ PreToolUse: [
121
+ ...(userHooks?.PreToolUse || []),
122
+ {
123
+ hooks: [createPreToolUseHook(settingsManager, logger)],
106
124
  },
107
125
  ],
108
126
  };
@@ -214,12 +232,22 @@ export function buildSessionOptions(params: BuildOptionsParams): Options {
214
232
  permissionMode: params.permissionMode,
215
233
  canUseTool: params.canUseTool,
216
234
  executable: "node",
235
+ tools: { type: "preset", preset: "claude_code" },
236
+ extraArgs: {
237
+ ...params.userProvidedOptions?.extraArgs,
238
+ "replay-user-messages": "",
239
+ },
217
240
  mcpServers: buildMcpServers(
218
241
  params.userProvidedOptions?.mcpServers,
219
242
  params.mcpServers,
220
243
  ),
221
244
  env: buildEnvironment(),
222
- hooks: buildHooks(params.userProvidedOptions?.hooks, params.onModeChange),
245
+ hooks: buildHooks(
246
+ params.userProvidedOptions?.hooks,
247
+ params.onModeChange,
248
+ params.settingsManager,
249
+ params.logger,
250
+ ),
223
251
  abortController: getAbortController(
224
252
  params.userProvidedOptions?.abortController,
225
253
  ),
@@ -238,15 +266,39 @@ export function buildSessionOptions(params: BuildOptionsParams): Options {
238
266
 
239
267
  if (params.isResume) {
240
268
  options.resume = params.sessionId;
241
- options.forkSession = false;
269
+ options.forkSession = params.forkSession ?? false;
242
270
  } else {
243
271
  options.sessionId = params.sessionId;
272
+ options.model = DEFAULT_MODEL;
244
273
  }
245
274
 
246
275
  if (params.additionalDirectories) {
247
276
  options.additionalDirectories = params.additionalDirectories;
248
277
  }
249
278
 
279
+ if (params.disableBuiltInTools) {
280
+ const builtInTools = [
281
+ "Read",
282
+ "Write",
283
+ "Edit",
284
+ "Bash",
285
+ "Glob",
286
+ "Grep",
287
+ "Task",
288
+ "TodoWrite",
289
+ "ExitPlanMode",
290
+ "WebSearch",
291
+ "WebFetch",
292
+ "SlashCommand",
293
+ "Skill",
294
+ "NotebookEdit",
295
+ ];
296
+ options.disallowedTools = [
297
+ ...(options.disallowedTools ?? []),
298
+ ...builtInTools,
299
+ ];
300
+ }
301
+
250
302
  clearStatsigCache();
251
303
  return options;
252
304
  }
@@ -0,0 +1,326 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { minimatch } from "minimatch";
5
+
6
+ const ACP_TOOL_NAME_PREFIX = "mcp__acp__";
7
+
8
+ const acpToolNames = {
9
+ read: `${ACP_TOOL_NAME_PREFIX}Read`,
10
+ edit: `${ACP_TOOL_NAME_PREFIX}Edit`,
11
+ write: `${ACP_TOOL_NAME_PREFIX}Write`,
12
+ bash: `${ACP_TOOL_NAME_PREFIX}Bash`,
13
+ };
14
+
15
+ const SHELL_OPERATORS = ["&&", "||", ";", "|", "$(", "`", "\n"];
16
+
17
+ function containsShellOperator(str: string): boolean {
18
+ return SHELL_OPERATORS.some((op) => str.includes(op));
19
+ }
20
+
21
+ const FILE_EDITING_TOOLS = [acpToolNames.edit, acpToolNames.write];
22
+
23
+ const FILE_READING_TOOLS = [acpToolNames.read];
24
+
25
+ const TOOL_ARG_ACCESSORS: Record<
26
+ string,
27
+ (input: Record<string, unknown>) => string | undefined
28
+ > = {
29
+ [acpToolNames.read]: (input) => input?.file_path as string | undefined,
30
+ [acpToolNames.edit]: (input) => input?.file_path as string | undefined,
31
+ [acpToolNames.write]: (input) => input?.file_path as string | undefined,
32
+ [acpToolNames.bash]: (input) => input?.command as string | undefined,
33
+ };
34
+
35
+ interface ParsedRule {
36
+ toolName: string;
37
+ argument?: string;
38
+ isWildcard?: boolean;
39
+ }
40
+
41
+ function parseRule(rule: string): ParsedRule {
42
+ const match = rule.match(/^(\w+)(?:\((.+)\))?$/);
43
+ if (!match) {
44
+ return { toolName: rule };
45
+ }
46
+ const toolName = match[1] ?? rule;
47
+ const argument = match[2];
48
+ if (argument?.endsWith(":*")) {
49
+ return {
50
+ toolName,
51
+ argument: argument.slice(0, -2),
52
+ isWildcard: true,
53
+ };
54
+ }
55
+ return { toolName, argument };
56
+ }
57
+
58
+ function normalizePath(filePath: string, cwd: string): string {
59
+ let resolved = filePath;
60
+ if (resolved.startsWith("~/")) {
61
+ resolved = path.join(os.homedir(), resolved.slice(2));
62
+ } else if (resolved.startsWith("./")) {
63
+ resolved = path.join(cwd, resolved.slice(2));
64
+ } else if (!path.isAbsolute(resolved)) {
65
+ resolved = path.join(cwd, resolved);
66
+ }
67
+ return path.normalize(resolved).replace(/\\/g, "/");
68
+ }
69
+
70
+ function matchesGlob(pattern: string, filePath: string, cwd: string): boolean {
71
+ const normalizedPattern = normalizePath(pattern, cwd);
72
+ const normalizedPath = normalizePath(filePath, cwd);
73
+ return minimatch(normalizedPath, normalizedPattern, {
74
+ dot: true,
75
+ matchBase: false,
76
+ nocase: process.platform === "win32",
77
+ });
78
+ }
79
+
80
+ function matchesRule(
81
+ rule: ParsedRule,
82
+ toolName: string,
83
+ toolInput: unknown,
84
+ cwd: string,
85
+ ): boolean {
86
+ const ruleAppliesToTool =
87
+ (rule.toolName === "Bash" && toolName === acpToolNames.bash) ||
88
+ (rule.toolName === "Edit" && FILE_EDITING_TOOLS.includes(toolName)) ||
89
+ (rule.toolName === "Read" && FILE_READING_TOOLS.includes(toolName));
90
+
91
+ if (!ruleAppliesToTool) {
92
+ return false;
93
+ }
94
+
95
+ if (!rule.argument) {
96
+ return true;
97
+ }
98
+
99
+ const argAccessor = TOOL_ARG_ACCESSORS[toolName];
100
+ if (!argAccessor) {
101
+ return true;
102
+ }
103
+
104
+ const actualArg = argAccessor(toolInput as Record<string, unknown>);
105
+ if (!actualArg) {
106
+ return false;
107
+ }
108
+
109
+ if (toolName === acpToolNames.bash) {
110
+ if (rule.isWildcard) {
111
+ if (!actualArg.startsWith(rule.argument)) {
112
+ return false;
113
+ }
114
+ const remainder = actualArg.slice(rule.argument.length);
115
+ if (containsShellOperator(remainder)) {
116
+ return false;
117
+ }
118
+ return true;
119
+ }
120
+ return actualArg === rule.argument;
121
+ }
122
+
123
+ return matchesGlob(rule.argument, actualArg, cwd);
124
+ }
125
+
126
+ async function loadSettingsFile(
127
+ filePath: string | undefined,
128
+ ): Promise<ClaudeCodeSettings> {
129
+ if (!filePath) {
130
+ return {};
131
+ }
132
+ try {
133
+ const content = await fs.promises.readFile(filePath, "utf-8");
134
+ return JSON.parse(content) as ClaudeCodeSettings;
135
+ } catch {
136
+ return {};
137
+ }
138
+ }
139
+
140
+ export interface PermissionSettings {
141
+ allow?: string[];
142
+ deny?: string[];
143
+ ask?: string[];
144
+ additionalDirectories?: string[];
145
+ defaultMode?: string;
146
+ }
147
+
148
+ export interface ClaudeCodeSettings {
149
+ permissions?: PermissionSettings;
150
+ env?: Record<string, string>;
151
+ model?: string;
152
+ }
153
+
154
+ export type PermissionDecision = "allow" | "deny" | "ask";
155
+
156
+ export interface PermissionCheckResult {
157
+ decision: PermissionDecision;
158
+ rule?: string;
159
+ source?: "allow" | "deny" | "ask";
160
+ }
161
+
162
+ export function getManagedSettingsPath(): string {
163
+ switch (process.platform) {
164
+ case "darwin":
165
+ return "/Library/Application Support/ClaudeCode/managed-settings.json";
166
+ case "linux":
167
+ return "/etc/claude-code/managed-settings.json";
168
+ case "win32":
169
+ return "C:\\Program Files\\ClaudeCode\\managed-settings.json";
170
+ default:
171
+ return "/etc/claude-code/managed-settings.json";
172
+ }
173
+ }
174
+ export class SettingsManager {
175
+ private cwd: string;
176
+ private userSettings: ClaudeCodeSettings = {};
177
+ private projectSettings: ClaudeCodeSettings = {};
178
+ private localSettings: ClaudeCodeSettings = {};
179
+ private enterpriseSettings: ClaudeCodeSettings = {};
180
+ private mergedSettings: ClaudeCodeSettings = {};
181
+ private initialized = false;
182
+
183
+ constructor(cwd: string) {
184
+ this.cwd = cwd;
185
+ }
186
+
187
+ async initialize(): Promise<void> {
188
+ if (this.initialized) {
189
+ return;
190
+ }
191
+ await this.loadAllSettings();
192
+ this.initialized = true;
193
+ }
194
+
195
+ private getUserSettingsPath(): string {
196
+ const configDir =
197
+ process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
198
+ return path.join(configDir, "settings.json");
199
+ }
200
+
201
+ private getProjectSettingsPath(): string {
202
+ return path.join(this.cwd, ".claude", "settings.json");
203
+ }
204
+
205
+ private getLocalSettingsPath(): string {
206
+ return path.join(this.cwd, ".claude", "settings.local.json");
207
+ }
208
+
209
+ private async loadAllSettings(): Promise<void> {
210
+ const [userSettings, projectSettings, localSettings, enterpriseSettings] =
211
+ await Promise.all([
212
+ loadSettingsFile(this.getUserSettingsPath()),
213
+ loadSettingsFile(this.getProjectSettingsPath()),
214
+ loadSettingsFile(this.getLocalSettingsPath()),
215
+ loadSettingsFile(getManagedSettingsPath()),
216
+ ]);
217
+ this.userSettings = userSettings;
218
+ this.projectSettings = projectSettings;
219
+ this.localSettings = localSettings;
220
+ this.enterpriseSettings = enterpriseSettings;
221
+ this.mergeAllSettings();
222
+ }
223
+
224
+ private mergeAllSettings(): void {
225
+ const allSettings = [
226
+ this.userSettings,
227
+ this.projectSettings,
228
+ this.localSettings,
229
+ this.enterpriseSettings,
230
+ ];
231
+
232
+ const permissions: PermissionSettings = {
233
+ allow: [],
234
+ deny: [],
235
+ ask: [],
236
+ };
237
+ const merged: ClaudeCodeSettings = { permissions };
238
+
239
+ for (const settings of allSettings) {
240
+ if (settings.permissions) {
241
+ if (settings.permissions.allow) {
242
+ permissions.allow?.push(...settings.permissions.allow);
243
+ }
244
+ if (settings.permissions.deny) {
245
+ permissions.deny?.push(...settings.permissions.deny);
246
+ }
247
+ if (settings.permissions.ask) {
248
+ permissions.ask?.push(...settings.permissions.ask);
249
+ }
250
+ if (settings.permissions.additionalDirectories) {
251
+ permissions.additionalDirectories = [
252
+ ...(permissions.additionalDirectories || []),
253
+ ...settings.permissions.additionalDirectories,
254
+ ];
255
+ }
256
+ if (settings.permissions.defaultMode) {
257
+ permissions.defaultMode = settings.permissions.defaultMode;
258
+ }
259
+ }
260
+ if (settings.env) {
261
+ merged.env = { ...merged.env, ...settings.env };
262
+ }
263
+ if (settings.model) {
264
+ merged.model = settings.model;
265
+ }
266
+ }
267
+
268
+ this.mergedSettings = merged;
269
+ }
270
+
271
+ checkPermission(toolName: string, toolInput: unknown): PermissionCheckResult {
272
+ if (!toolName.startsWith(ACP_TOOL_NAME_PREFIX)) {
273
+ return { decision: "ask" };
274
+ }
275
+
276
+ const permissions = this.mergedSettings.permissions;
277
+ if (!permissions) {
278
+ return { decision: "ask" };
279
+ }
280
+
281
+ for (const rule of permissions.deny || []) {
282
+ const parsed = parseRule(rule);
283
+ if (matchesRule(parsed, toolName, toolInput, this.cwd)) {
284
+ return { decision: "deny", rule, source: "deny" };
285
+ }
286
+ }
287
+
288
+ for (const rule of permissions.allow || []) {
289
+ const parsed = parseRule(rule);
290
+ if (matchesRule(parsed, toolName, toolInput, this.cwd)) {
291
+ return { decision: "allow", rule, source: "allow" };
292
+ }
293
+ }
294
+
295
+ for (const rule of permissions.ask || []) {
296
+ const parsed = parseRule(rule);
297
+ if (matchesRule(parsed, toolName, toolInput, this.cwd)) {
298
+ return { decision: "ask", rule, source: "ask" };
299
+ }
300
+ }
301
+
302
+ return { decision: "ask" };
303
+ }
304
+
305
+ getSettings(): ClaudeCodeSettings {
306
+ return this.mergedSettings;
307
+ }
308
+
309
+ getCwd(): string {
310
+ return this.cwd;
311
+ }
312
+
313
+ async setCwd(cwd: string): Promise<void> {
314
+ if (this.cwd === cwd) {
315
+ return;
316
+ }
317
+ this.dispose();
318
+ this.cwd = cwd;
319
+ this.initialized = false;
320
+ await this.initialize();
321
+ }
322
+
323
+ dispose(): void {
324
+ this.initialized = false;
325
+ }
326
+ }
@@ -39,6 +39,7 @@ const AUTO_ALLOWED_TOOLS: Record<string, Set<string>> = {
39
39
  default: new Set(BASE_ALLOWED_TOOLS),
40
40
  acceptEdits: new Set([...BASE_ALLOWED_TOOLS, ...WRITE_TOOLS]),
41
41
  plan: new Set(BASE_ALLOWED_TOOLS),
42
+ // dontAsk: new Set(BASE_ALLOWED_TOOLS),
42
43
  };
43
44
 
44
45
  export function isToolAllowedForMode(
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ SessionConfigOption,
2
3
  TerminalHandle,
3
4
  TerminalOutputResponse,
4
5
  } from "@agentclientprotocol/sdk";
@@ -9,8 +10,16 @@ import type {
9
10
  } from "@anthropic-ai/claude-agent-sdk";
10
11
  import type { Pushable } from "../../utils/streams.js";
11
12
  import type { BaseSession } from "../base-acp-agent.js";
13
+ import type { SettingsManager } from "./session/settings.js";
12
14
  import type { TwigExecutionMode } from "./tools.js";
13
15
 
16
+ export type AccumulatedUsage = {
17
+ inputTokens: number;
18
+ outputTokens: number;
19
+ cachedReadTokens: number;
20
+ cachedWriteTokens: number;
21
+ };
22
+
14
23
  export type BackgroundTerminal =
15
24
  | {
16
25
  handle: TerminalHandle;
@@ -22,15 +31,26 @@ export type BackgroundTerminal =
22
31
  pendingOutput: TerminalOutputResponse;
23
32
  };
24
33
 
34
+ export type PendingMessage = {
35
+ resolve: (cancelled: boolean) => void;
36
+ order: number;
37
+ };
38
+
25
39
  export type Session = BaseSession & {
26
40
  query: Query;
27
41
  input: Pushable<SDKUserMessage>;
42
+ settingsManager: SettingsManager;
28
43
  permissionMode: TwigExecutionMode;
29
44
  modelId?: string;
30
45
  cwd: string;
31
46
  taskRunId?: string;
32
47
  lastPlanFilePath?: string;
33
48
  lastPlanContent?: string;
49
+ configOptions: SessionConfigOption[];
50
+ accumulatedUsage: AccumulatedUsage;
51
+ promptRunning: boolean;
52
+ pendingMessages: Map<string, PendingMessage>;
53
+ nextPendingOrder: number;
34
54
  };
35
55
 
36
56
  export type ToolUseCache = {
@@ -42,12 +62,30 @@ export type ToolUseCache = {
42
62
  };
43
63
  };
44
64
 
65
+ export type TerminalInfo = {
66
+ terminal_id: string;
67
+ };
68
+
69
+ export type TerminalOutput = {
70
+ terminal_id: string;
71
+ data: string;
72
+ };
73
+
74
+ export type TerminalExit = {
75
+ terminal_id: string;
76
+ exit_code: number | null;
77
+ signal: string | null;
78
+ };
79
+
45
80
  export type ToolUpdateMeta = {
46
81
  claudeCode?: {
47
82
  toolName: string;
48
83
  toolResponse?: unknown;
49
84
  parentToolCallId?: string;
50
85
  };
86
+ terminal_info?: TerminalInfo;
87
+ terminal_output?: TerminalOutput;
88
+ terminal_exit?: TerminalExit;
51
89
  };
52
90
 
53
91
  export type NewSessionMeta = {
@@ -6,38 +6,54 @@ export interface ModeInfo {
6
6
  description: string;
7
7
  }
8
8
 
9
- const MODES: ModeInfo[] = [
9
+ // Helper constant that can easily be toggled for env/feature flag/etc
10
+ const ALLOW_BYPASS = !IS_ROOT;
11
+
12
+ const availableModes: ModeInfo[] = [
10
13
  {
11
14
  id: "default",
12
- name: "Always Ask",
13
- description: "Prompts for permission on first use of each tool",
15
+ name: "Default",
16
+ description: "Standard behavior, prompts for dangerous operations",
14
17
  },
15
18
  {
16
19
  id: "acceptEdits",
17
20
  name: "Accept Edits",
18
- description: "Automatically accepts file edit permissions for the session",
21
+ description: "Auto-accept file edit operations",
19
22
  },
20
23
  {
21
24
  id: "plan",
22
25
  name: "Plan Mode",
23
- description: "Claude can analyze but not modify files or execute commands",
26
+ description: "Planning mode, no actual tool execution",
24
27
  },
25
- {
28
+ // {
29
+ // id: "dontAsk",
30
+ // name: "Don't Ask",
31
+ // description: "Don't prompt for permissions, deny if not pre-approved",
32
+ // },
33
+ ];
34
+
35
+ if (ALLOW_BYPASS) {
36
+ availableModes.push({
26
37
  id: "bypassPermissions",
27
38
  name: "Bypass Permissions",
28
- description: "Skips all permission prompts",
29
- },
30
- ];
39
+ description: "Bypass all permission checks",
40
+ });
41
+ }
31
42
 
43
+ // Expose execution mode IDs in type-safe order for type checks
32
44
  export const TWIG_EXECUTION_MODES = [
33
45
  "default",
34
46
  "acceptEdits",
35
47
  "plan",
48
+ // "dontAsk",
36
49
  "bypassPermissions",
37
50
  ] as const;
38
51
 
39
52
  export type TwigExecutionMode = (typeof TWIG_EXECUTION_MODES)[number];
40
53
 
41
54
  export function getAvailableModes(): ModeInfo[] {
42
- return IS_ROOT ? MODES.filter((m) => m.id !== "bypassPermissions") : MODES;
55
+ // When IS_ROOT, do not allow bypassPermissions
56
+ return IS_ROOT
57
+ ? availableModes.filter((m) => m.id !== "bypassPermissions")
58
+ : availableModes;
43
59
  }
@@ -1,6 +1,14 @@
1
1
  import jwt from "jsonwebtoken";
2
2
  import { type SetupServerApi, setupServer } from "msw/node";
3
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
+ import {
4
+ afterAll,
5
+ afterEach,
6
+ beforeAll,
7
+ beforeEach,
8
+ describe,
9
+ expect,
10
+ it,
11
+ } from "vitest";
4
12
  import { createTestRepo, type TestRepo } from "../test/fixtures/api.js";
5
13
  import { createPostHogHandlers } from "../test/mocks/msw-handlers.js";
6
14
  import type { TaskRun } from "../types.js";
@@ -14,6 +22,38 @@ interface TestableServer {
14
22
  buildCloudSystemPrompt(prUrl?: string | null): string;
15
23
  }
16
24
 
25
+ // The Claude Agent SDK has an internal readMessages() loop that rejects with
26
+ // "Query closed before response received" during cleanup. The SDK starts this
27
+ // promise in the constructor without a .catch() handler, so the rejection is
28
+ // unhandled. We suppress it here to prevent vitest from failing the suite.
29
+ type Listener = (...args: unknown[]) => void;
30
+ const originalListeners: Listener[] = [];
31
+
32
+ beforeAll(() => {
33
+ originalListeners.push(
34
+ ...process.rawListeners("unhandledRejection").map((l) => l as Listener),
35
+ );
36
+ process.removeAllListeners("unhandledRejection");
37
+ process.on("unhandledRejection", (reason: unknown) => {
38
+ if (
39
+ reason instanceof Error &&
40
+ reason.message === "Query closed before response received"
41
+ ) {
42
+ return;
43
+ }
44
+ for (const listener of originalListeners) {
45
+ listener(reason);
46
+ }
47
+ });
48
+ });
49
+
50
+ afterAll(() => {
51
+ process.removeAllListeners("unhandledRejection");
52
+ for (const listener of originalListeners) {
53
+ process.on("unhandledRejection", listener);
54
+ }
55
+ });
56
+
17
57
  function createTestJwt(
18
58
  payload: JwtPayload,
19
59
  privateKey: string,
@@ -28,7 +28,7 @@ export function unreachable(value: never, logger: Logger): void {
28
28
  try {
29
29
  valueAsString = JSON.stringify(value);
30
30
  } catch {
31
- valueAsString = value;
31
+ valueAsString = String(value);
32
32
  }
33
33
  logger.error(`Unexpected case: ${valueAsString}`);
34
34
  }