@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.
- 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 -606
- 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 +1300 -655
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +1278 -635
- 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/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 {
|
|
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:
|
|
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
|
|
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(
|
|
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 = {
|
package/src/execution-mode.ts
CHANGED
|
@@ -6,38 +6,54 @@ export interface ModeInfo {
|
|
|
6
6
|
description: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
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: "
|
|
13
|
-
description: "
|
|
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: "
|
|
21
|
+
description: "Auto-accept file edit operations",
|
|
19
22
|
},
|
|
20
23
|
{
|
|
21
24
|
id: "plan",
|
|
22
25
|
name: "Plan Mode",
|
|
23
|
-
description: "
|
|
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: "
|
|
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
|
-
|
|
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 {
|
|
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,
|
package/src/utils/common.ts
CHANGED