@posthog/agent 2.3.735 → 2.3.736
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 +2 -10
- package/dist/adapters/claude/conversion/tool-use-to-acp.js +24 -10
- package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -1
- package/dist/adapters/claude/permissions/permission-options.js +6 -4
- package/dist/adapters/claude/permissions/permission-options.js.map +1 -1
- package/dist/adapters/claude/session/jsonl-hydration.js +1 -0
- package/dist/adapters/claude/session/jsonl-hydration.js.map +1 -1
- package/dist/adapters/claude/session/models.js +20 -1
- package/dist/adapters/claude/session/models.js.map +1 -1
- package/dist/adapters/claude/tools.js +4 -2
- package/dist/adapters/claude/tools.js.map +1 -1
- package/dist/adapters/reasoning-effort.js +3 -1
- package/dist/adapters/reasoning-effort.js.map +1 -1
- package/dist/agent.js +524 -72
- package/dist/agent.js.map +1 -1
- package/dist/posthog-api.js +4 -4
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.js +524 -72
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +524 -72
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +6 -6
- package/src/adapters/claude/UPSTREAM.md +80 -3
- package/src/adapters/claude/claude-agent.ts +201 -18
- package/src/adapters/claude/conversion/sdk-to-acp.ts +182 -16
- package/src/adapters/claude/conversion/task-state.test.ts +338 -0
- package/src/adapters/claude/conversion/task-state.ts +178 -0
- package/src/adapters/claude/conversion/tool-use-to-acp.ts +29 -19
- package/src/adapters/claude/hooks.test.ts +162 -0
- package/src/adapters/claude/hooks.ts +44 -3
- package/src/adapters/claude/permissions/permission-options.ts +7 -2
- package/src/adapters/claude/session/commands.ts +1 -0
- package/src/adapters/claude/session/mcp-config.ts +23 -7
- package/src/adapters/claude/session/model-config.test.ts +60 -0
- package/src/adapters/claude/session/model-config.ts +56 -0
- package/src/adapters/claude/session/models.test.ts +119 -0
- package/src/adapters/claude/session/models.ts +31 -0
- package/src/adapters/claude/session/options.test.ts +1 -0
- package/src/adapters/claude/session/options.ts +29 -1
- package/src/adapters/claude/session/settings.test.ts +102 -1
- package/src/adapters/claude/session/settings.ts +33 -6
- package/src/adapters/claude/tools.ts +4 -2
- package/src/adapters/claude/types.ts +8 -0
- package/src/adapters/codex/codex-agent.ts +2 -2
- package/src/server/agent-server.test.ts +173 -0
- package/src/test/mocks/claude-sdk.ts +4 -0
- package/src/test/native-binary.test.ts +27 -0
- package/dist/claude-cli/cli.js +0 -18412
- package/dist/claude-cli/vendor/audio-capture/arm64-darwin/audio-capture.node +0 -0
- package/dist/claude-cli/vendor/audio-capture/arm64-linux/audio-capture.node +0 -0
- package/dist/claude-cli/vendor/audio-capture/arm64-win32/audio-capture.node +0 -0
- package/dist/claude-cli/vendor/audio-capture/x64-darwin/audio-capture.node +0 -0
- package/dist/claude-cli/vendor/audio-capture/x64-linux/audio-capture.node +0 -0
- package/dist/claude-cli/vendor/audio-capture/x64-win32/audio-capture.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/COPYING +0 -3
- package/dist/claude-cli/vendor/ripgrep/arm64-darwin/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-linux/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-win32/rg.exe +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-darwin/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-linux/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-win32/rg.exe +0 -0
- package/dist/claude-cli/yoga.wasm +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
getEffortOptions,
|
|
4
|
+
resolveModelPreference,
|
|
5
|
+
supports1MContext,
|
|
6
|
+
supportsEffort,
|
|
7
|
+
supportsMcpInjection,
|
|
8
|
+
supportsXhighEffort,
|
|
9
|
+
toSdkModelId,
|
|
10
|
+
} from "./models";
|
|
11
|
+
|
|
12
|
+
describe("toSdkModelId", () => {
|
|
13
|
+
it("maps known gateway IDs to SDK aliases", () => {
|
|
14
|
+
expect(toSdkModelId("claude-opus-4-7")).toBe("opus");
|
|
15
|
+
expect(toSdkModelId("claude-opus-4-8")).toBe("opus");
|
|
16
|
+
expect(toSdkModelId("claude-sonnet-4-6")).toBe("sonnet");
|
|
17
|
+
expect(toSdkModelId("claude-haiku-4-5")).toBe("haiku");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("passes unknown IDs through unchanged", () => {
|
|
21
|
+
expect(toSdkModelId("custom-model")).toBe("custom-model");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("model capability flags", () => {
|
|
26
|
+
it("flags 1M context support", () => {
|
|
27
|
+
expect(supports1MContext("claude-opus-4-7")).toBe(true);
|
|
28
|
+
expect(supports1MContext("claude-sonnet-4-6")).toBe(true);
|
|
29
|
+
expect(supports1MContext("claude-haiku-4-5")).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("flags effort support and xhigh-effort support", () => {
|
|
33
|
+
expect(supportsEffort("claude-opus-4-5")).toBe(true);
|
|
34
|
+
expect(supportsXhighEffort("claude-opus-4-7")).toBe(true);
|
|
35
|
+
expect(supportsXhighEffort("claude-opus-4-5")).toBe(false);
|
|
36
|
+
expect(supportsEffort("claude-haiku-4-5")).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("excludes MCP injection only for Haiku", () => {
|
|
40
|
+
expect(supportsMcpInjection("claude-opus-4-7")).toBe(true);
|
|
41
|
+
expect(supportsMcpInjection("claude-haiku-4-5")).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("getEffortOptions", () => {
|
|
46
|
+
it("returns null for models without effort support", () => {
|
|
47
|
+
expect(getEffortOptions("claude-haiku-4-5")).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns low/medium/high for effort-supporting models", () => {
|
|
51
|
+
const opts = getEffortOptions("claude-opus-4-5");
|
|
52
|
+
expect(opts?.map((o) => o.value)).toEqual(["low", "medium", "high"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("appends xhigh and max for xhigh-supporting models", () => {
|
|
56
|
+
const opts = getEffortOptions("claude-opus-4-7");
|
|
57
|
+
expect(opts?.map((o) => o.value)).toEqual([
|
|
58
|
+
"low",
|
|
59
|
+
"medium",
|
|
60
|
+
"high",
|
|
61
|
+
"xhigh",
|
|
62
|
+
"max",
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("resolveModelPreference", () => {
|
|
68
|
+
const options = [
|
|
69
|
+
{ value: "claude-opus-4-8", name: "Claude Opus 4.8" },
|
|
70
|
+
{ value: "claude-opus-4-7", name: "Claude Opus 4.7" },
|
|
71
|
+
{ value: "claude-opus-4-6", name: "Claude Opus 4.6" },
|
|
72
|
+
{ value: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
|
|
73
|
+
{ value: "claude-haiku-4-5", name: "Claude Haiku 4.5" },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
it("returns null for empty preference", () => {
|
|
77
|
+
expect(resolveModelPreference("", options)).toBeNull();
|
|
78
|
+
expect(resolveModelPreference(" ", options)).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("matches an exact value", () => {
|
|
82
|
+
expect(resolveModelPreference("claude-opus-4-7", options)).toBe(
|
|
83
|
+
"claude-opus-4-7",
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("matches case-insensitively on display name", () => {
|
|
88
|
+
expect(resolveModelPreference("claude haiku 4.5", options)).toBe(
|
|
89
|
+
"claude-haiku-4-5",
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("matches by substring", () => {
|
|
94
|
+
expect(resolveModelPreference("sonnet", options)).toBe("claude-sonnet-4-6");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("matches by token alias", () => {
|
|
98
|
+
expect(resolveModelPreference("opus[1m]", options)).toBe("claude-opus-4-8");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("refuses cross-version alias matches", () => {
|
|
102
|
+
const optionsWithAlias = [
|
|
103
|
+
{ value: "opus", name: "Claude Opus 4.7" },
|
|
104
|
+
{ value: "claude-opus-4-6", name: "Claude Opus 4.6" },
|
|
105
|
+
];
|
|
106
|
+
expect(resolveModelPreference("claude-opus-4-6", optionsWithAlias)).toBe(
|
|
107
|
+
"claude-opus-4-6",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns null when nothing matches", () => {
|
|
112
|
+
expect(resolveModelPreference("gpt-5", options)).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("treats `best` and `default` as wildcards (no tokens contribute)", () => {
|
|
116
|
+
expect(resolveModelPreference("best", options)).toBeNull();
|
|
117
|
+
expect(resolveModelPreference("default", options)).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -4,6 +4,7 @@ const GATEWAY_TO_SDK_MODEL: Record<string, string> = {
|
|
|
4
4
|
"claude-opus-4-5": "opus",
|
|
5
5
|
"claude-opus-4-6": "opus",
|
|
6
6
|
"claude-opus-4-7": "opus",
|
|
7
|
+
"claude-opus-4-8": "opus",
|
|
7
8
|
"claude-sonnet-4-5": "sonnet",
|
|
8
9
|
"claude-sonnet-4-6": "sonnet",
|
|
9
10
|
"claude-haiku-4-5": "haiku",
|
|
@@ -16,6 +17,7 @@ export function toSdkModelId(modelId: string): string {
|
|
|
16
17
|
const MODELS_WITH_1M_CONTEXT = new Set([
|
|
17
18
|
"claude-opus-4-6",
|
|
18
19
|
"claude-opus-4-7",
|
|
20
|
+
"claude-opus-4-8",
|
|
19
21
|
"claude-sonnet-4-6",
|
|
20
22
|
]);
|
|
21
23
|
|
|
@@ -27,12 +29,14 @@ const MODELS_WITH_EFFORT = new Set([
|
|
|
27
29
|
"claude-opus-4-5",
|
|
28
30
|
"claude-opus-4-6",
|
|
29
31
|
"claude-opus-4-7",
|
|
32
|
+
"claude-opus-4-8",
|
|
30
33
|
"claude-sonnet-4-6",
|
|
31
34
|
]);
|
|
32
35
|
|
|
33
36
|
const MODELS_WITH_XHIGH_EFFORT = new Set([
|
|
34
37
|
"claude-opus-4-6",
|
|
35
38
|
"claude-opus-4-7",
|
|
39
|
+
"claude-opus-4-8",
|
|
36
40
|
]);
|
|
37
41
|
|
|
38
42
|
export function supportsEffort(modelId: string): boolean {
|
|
@@ -107,6 +111,31 @@ interface ModelOption {
|
|
|
107
111
|
description?: string;
|
|
108
112
|
}
|
|
109
113
|
|
|
114
|
+
// Captures a model family version such as `4-6` or `4.7` so we can keep
|
|
115
|
+
// `claude-opus-4-6` from being copied onto the SDK's `opus` alias when that
|
|
116
|
+
// alias currently resolves to a different family version (e.g. Opus 4.7).
|
|
117
|
+
const MODEL_FAMILY_VERSION_PATTERN = /\b(\d+)[-.](\d+)\b/;
|
|
118
|
+
|
|
119
|
+
function extractModelFamilyVersion(s: string | undefined): string | null {
|
|
120
|
+
if (!s) return null;
|
|
121
|
+
const match = s.match(MODEL_FAMILY_VERSION_PATTERN);
|
|
122
|
+
return match ? `${match[1]}.${match[2]}` : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function modelVersionsCompatible(
|
|
126
|
+
preference: string,
|
|
127
|
+
candidate: ModelOption,
|
|
128
|
+
): boolean {
|
|
129
|
+
const preferred = extractModelFamilyVersion(preference);
|
|
130
|
+
if (!preferred) return true;
|
|
131
|
+
const candidateVersion =
|
|
132
|
+
extractModelFamilyVersion(candidate.value) ??
|
|
133
|
+
extractModelFamilyVersion(candidate.name) ??
|
|
134
|
+
extractModelFamilyVersion(candidate.description);
|
|
135
|
+
if (!candidateVersion) return true;
|
|
136
|
+
return preferred === candidateVersion;
|
|
137
|
+
}
|
|
138
|
+
|
|
110
139
|
function scoreModelMatch(
|
|
111
140
|
model: ModelOption,
|
|
112
141
|
tokens: string[],
|
|
@@ -142,6 +171,7 @@ export function resolveModelPreference(
|
|
|
142
171
|
|
|
143
172
|
// Substring match
|
|
144
173
|
const includesMatch = options.find((o) => {
|
|
174
|
+
if (!modelVersionsCompatible(trimmed, o)) return false;
|
|
145
175
|
const value = o.value.toLowerCase();
|
|
146
176
|
const display = (o.name ?? "").toLowerCase();
|
|
147
177
|
return (
|
|
@@ -157,6 +187,7 @@ export function resolveModelPreference(
|
|
|
157
187
|
let bestMatch: ModelOption | null = null;
|
|
158
188
|
let bestScore = 0;
|
|
159
189
|
for (const model of options) {
|
|
190
|
+
if (!modelVersionsCompatible(trimmed, model)) continue;
|
|
160
191
|
const score = scoreModelMatch(model, tokens, contextHint);
|
|
161
192
|
if (0 < score && (!bestMatch || bestScore < score)) {
|
|
162
193
|
bestMatch = model;
|
|
@@ -13,12 +13,14 @@ import type {
|
|
|
13
13
|
import type { FileEnrichmentDeps } from "../../../enrichment/file-enricher";
|
|
14
14
|
import { IS_ROOT } from "../../../utils/common";
|
|
15
15
|
import type { Logger } from "../../../utils/logger";
|
|
16
|
+
import type { TaskState } from "../conversion/task-state";
|
|
16
17
|
import {
|
|
17
18
|
createPostToolUseHook,
|
|
18
19
|
createPreToolUseHook,
|
|
19
20
|
createReadEnrichmentHook,
|
|
20
21
|
createSignedCommitGuardHook,
|
|
21
22
|
createSubagentRewriteHook,
|
|
23
|
+
createTaskHook,
|
|
22
24
|
type EnrichedReadCache,
|
|
23
25
|
type OnModeChange,
|
|
24
26
|
} from "../hooks";
|
|
@@ -58,6 +60,11 @@ export interface BuildOptionsParams {
|
|
|
58
60
|
enrichedReadCache?: EnrichedReadCache;
|
|
59
61
|
/** Cloud task session — enables the signed-commit guard. */
|
|
60
62
|
cloudMode?: boolean;
|
|
63
|
+
/** Per-session task state populated by createTaskHook from SDK Task* events. */
|
|
64
|
+
taskState: TaskState;
|
|
65
|
+
/** Called after createTaskHook mutates taskState so callers can emit a plan
|
|
66
|
+
* sessionUpdate to the client. */
|
|
67
|
+
onTaskStateChange?: () => Promise<void>;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
export function buildSystemPrompt(
|
|
@@ -111,6 +118,13 @@ function buildEnvironment(): Record<string, string> {
|
|
|
111
118
|
? `${existingCustomHeaders}\n${bedrockFallbackHeader}`
|
|
112
119
|
: bedrockFallbackHeader;
|
|
113
120
|
|
|
121
|
+
// SDK 0.3.142 made MCP servers connect in the background by default. That
|
|
122
|
+
// default is what we want: a slow or unreachable user MCP server (PostHog
|
|
123
|
+
// MCP, custom stdio servers) would otherwise stall turn 1 by up to ~5s per
|
|
124
|
+
// server. We honor an explicit override from the caller's environment for
|
|
125
|
+
// sessions that genuinely need MCP tools available on turn 1.
|
|
126
|
+
const mcpNonblocking = process.env.MCP_CONNECTION_NONBLOCKING;
|
|
127
|
+
|
|
114
128
|
return {
|
|
115
129
|
...process.env,
|
|
116
130
|
ELECTRON_RUN_AS_NODE: "1",
|
|
@@ -119,6 +133,9 @@ function buildEnvironment(): Record<string, string> {
|
|
|
119
133
|
ENABLE_TOOL_SEARCH: "auto:0",
|
|
120
134
|
// Enable idle state as end-of-turn signal (required for SDK 0.2.114+)
|
|
121
135
|
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
|
|
136
|
+
...(mcpNonblocking !== undefined && {
|
|
137
|
+
MCP_CONNECTION_NONBLOCKING: mcpNonblocking,
|
|
138
|
+
}),
|
|
122
139
|
// Route to AWS Bedrock as a fallback when Anthropic returns 5xx
|
|
123
140
|
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
|
|
124
141
|
};
|
|
@@ -133,6 +150,8 @@ function buildHooks(
|
|
|
133
150
|
enrichedReadCache: EnrichedReadCache | undefined,
|
|
134
151
|
registeredAgents: ReadonlySet<string>,
|
|
135
152
|
cloudMode: boolean,
|
|
153
|
+
taskState: TaskState,
|
|
154
|
+
onTaskStateChange: (() => Promise<void>) | undefined,
|
|
136
155
|
): Options["hooks"] {
|
|
137
156
|
const postToolUseHooks = [createPostToolUseHook({ onModeChange })];
|
|
138
157
|
if (enrichmentDeps && enrichedReadCache) {
|
|
@@ -149,6 +168,8 @@ function buildHooks(
|
|
|
149
168
|
preToolUseHooks.push(createSignedCommitGuardHook(logger));
|
|
150
169
|
}
|
|
151
170
|
|
|
171
|
+
const taskHook = createTaskHook(taskState, onTaskStateChange);
|
|
172
|
+
|
|
152
173
|
return {
|
|
153
174
|
...userHooks,
|
|
154
175
|
PostToolUse: [
|
|
@@ -156,6 +177,8 @@ function buildHooks(
|
|
|
156
177
|
{ hooks: postToolUseHooks },
|
|
157
178
|
],
|
|
158
179
|
PreToolUse: [...(userHooks?.PreToolUse || []), { hooks: preToolUseHooks }],
|
|
180
|
+
TaskCreated: [...(userHooks?.TaskCreated || []), { hooks: [taskHook] }],
|
|
181
|
+
TaskCompleted: [...(userHooks?.TaskCompleted || []), { hooks: [taskHook] }],
|
|
159
182
|
};
|
|
160
183
|
}
|
|
161
184
|
|
|
@@ -195,7 +218,10 @@ Rules:
|
|
|
195
218
|
"WebFetch",
|
|
196
219
|
"WebSearch",
|
|
197
220
|
"NotebookRead",
|
|
198
|
-
"
|
|
221
|
+
"TaskCreate",
|
|
222
|
+
"TaskUpdate",
|
|
223
|
+
"TaskGet",
|
|
224
|
+
"TaskList",
|
|
199
225
|
],
|
|
200
226
|
};
|
|
201
227
|
|
|
@@ -357,6 +383,8 @@ export function buildSessionOptions(params: BuildOptionsParams): Options {
|
|
|
357
383
|
params.enrichedReadCache,
|
|
358
384
|
registeredAgentNames,
|
|
359
385
|
params.cloudMode ?? false,
|
|
386
|
+
params.taskState,
|
|
387
|
+
params.onTaskStateChange,
|
|
360
388
|
),
|
|
361
389
|
outputFormat: params.outputFormat,
|
|
362
390
|
abortController: getAbortController(
|
|
@@ -4,7 +4,7 @@ import * as os from "node:os";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
6
|
import { resolveMainRepoPath } from "./repo-path";
|
|
7
|
-
import { SettingsManager } from "./settings";
|
|
7
|
+
import { mergeAvailableModels, SettingsManager } from "./settings";
|
|
8
8
|
|
|
9
9
|
function runGit(cwd: string, args: string[]): void {
|
|
10
10
|
execFileSync("git", args, { cwd, stdio: ["ignore", "ignore", "pipe"] });
|
|
@@ -207,3 +207,104 @@ describe("resolveMainRepoPath", () => {
|
|
|
207
207
|
}
|
|
208
208
|
});
|
|
209
209
|
});
|
|
210
|
+
|
|
211
|
+
describe("availableModels merge", () => {
|
|
212
|
+
let tmpRoot: string;
|
|
213
|
+
let cwd: string;
|
|
214
|
+
let configDir: string;
|
|
215
|
+
let originalConfigDir: string | undefined;
|
|
216
|
+
|
|
217
|
+
beforeEach(async () => {
|
|
218
|
+
tmpRoot = await fs.promises.realpath(
|
|
219
|
+
await fs.promises.mkdtemp(path.join(os.tmpdir(), "available-models-")),
|
|
220
|
+
);
|
|
221
|
+
cwd = path.join(tmpRoot, "repo");
|
|
222
|
+
configDir = path.join(tmpRoot, "user");
|
|
223
|
+
await fs.promises.mkdir(cwd, { recursive: true });
|
|
224
|
+
await fs.promises.mkdir(configDir, { recursive: true });
|
|
225
|
+
runGit(cwd, ["init", "-b", "main"]);
|
|
226
|
+
runGit(cwd, ["config", "user.email", "test@example.com"]);
|
|
227
|
+
runGit(cwd, ["config", "user.name", "test"]);
|
|
228
|
+
runGit(cwd, ["commit", "--allow-empty", "-m", "init"]);
|
|
229
|
+
|
|
230
|
+
originalConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
|
231
|
+
process.env.CLAUDE_CONFIG_DIR = configDir;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
afterEach(async () => {
|
|
235
|
+
if (originalConfigDir === undefined) {
|
|
236
|
+
delete process.env.CLAUDE_CONFIG_DIR;
|
|
237
|
+
} else {
|
|
238
|
+
process.env.CLAUDE_CONFIG_DIR = originalConfigDir;
|
|
239
|
+
}
|
|
240
|
+
await fs.promises.rm(tmpRoot, { recursive: true, force: true });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
async function writeUserSettings(settings: object): Promise<void> {
|
|
244
|
+
await fs.promises.writeFile(
|
|
245
|
+
path.join(configDir, "settings.json"),
|
|
246
|
+
JSON.stringify(settings),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function writeProjectSettings(settings: object): Promise<void> {
|
|
251
|
+
const projectDir = path.join(cwd, ".claude");
|
|
252
|
+
await fs.promises.mkdir(projectDir, { recursive: true });
|
|
253
|
+
await fs.promises.writeFile(
|
|
254
|
+
path.join(projectDir, "settings.json"),
|
|
255
|
+
JSON.stringify(settings),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
it("merges and dedupes availableModels across user and project layers", async () => {
|
|
260
|
+
await writeUserSettings({ availableModels: ["model-a", "model-b"] });
|
|
261
|
+
await writeProjectSettings({ availableModels: ["model-b", "model-c"] });
|
|
262
|
+
|
|
263
|
+
const manager = new SettingsManager(cwd);
|
|
264
|
+
await manager.initialize();
|
|
265
|
+
|
|
266
|
+
expect(manager.getSettings().availableModels).toEqual([
|
|
267
|
+
"model-a",
|
|
268
|
+
"model-b",
|
|
269
|
+
"model-c",
|
|
270
|
+
]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("passes through a single layer unchanged", async () => {
|
|
274
|
+
await writeProjectSettings({ availableModels: ["only-one"] });
|
|
275
|
+
|
|
276
|
+
const manager = new SettingsManager(cwd);
|
|
277
|
+
await manager.initialize();
|
|
278
|
+
|
|
279
|
+
expect(manager.getSettings().availableModels).toEqual(["only-one"]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("leaves availableModels undefined when no layer defines it", async () => {
|
|
283
|
+
const manager = new SettingsManager(cwd);
|
|
284
|
+
await manager.initialize();
|
|
285
|
+
|
|
286
|
+
expect(manager.getSettings().availableModels).toBeUndefined();
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("mergeAvailableModels", () => {
|
|
291
|
+
it("merges and dedupes non-enterprise layers", () => {
|
|
292
|
+
expect(
|
|
293
|
+
mergeAvailableModels(
|
|
294
|
+
["model-a", "model-b"],
|
|
295
|
+
["model-b", "model-c"],
|
|
296
|
+
"project",
|
|
297
|
+
),
|
|
298
|
+
).toEqual(["model-a", "model-b", "model-c"]);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("lets enterprise settings replace lower-precedence allowlists", () => {
|
|
302
|
+
expect(
|
|
303
|
+
mergeAvailableModels(
|
|
304
|
+
["model-a", "model-b"],
|
|
305
|
+
["managed-a", "managed-a"],
|
|
306
|
+
"enterprise",
|
|
307
|
+
),
|
|
308
|
+
).toEqual(["managed-a"]);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
@@ -196,9 +196,12 @@ export interface ClaudeCodeSettings {
|
|
|
196
196
|
permissions?: PermissionSettings;
|
|
197
197
|
env?: Record<string, string>;
|
|
198
198
|
model?: string;
|
|
199
|
+
availableModels?: string[];
|
|
199
200
|
posthogApprovedExecTools?: string[];
|
|
200
201
|
}
|
|
201
202
|
|
|
203
|
+
type SettingsLayer = "user" | "project" | "local" | "enterprise";
|
|
204
|
+
|
|
202
205
|
export type PermissionDecision = "allow" | "deny" | "ask";
|
|
203
206
|
|
|
204
207
|
export interface PermissionCheckResult {
|
|
@@ -220,6 +223,22 @@ export function getManagedSettingsPath(): string {
|
|
|
220
223
|
}
|
|
221
224
|
}
|
|
222
225
|
|
|
226
|
+
export function mergeAvailableModels(
|
|
227
|
+
existing: string[] | undefined,
|
|
228
|
+
incoming: string[] | undefined,
|
|
229
|
+
layer: SettingsLayer,
|
|
230
|
+
): string[] | undefined {
|
|
231
|
+
if (incoming === undefined) {
|
|
232
|
+
return existing;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (layer === "enterprise") {
|
|
236
|
+
return Array.from(new Set(incoming));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return Array.from(new Set([...(existing ?? []), ...incoming]));
|
|
240
|
+
}
|
|
241
|
+
|
|
223
242
|
export class SettingsManager {
|
|
224
243
|
private cwd: string;
|
|
225
244
|
private repoRoot: string;
|
|
@@ -283,11 +302,14 @@ export class SettingsManager {
|
|
|
283
302
|
}
|
|
284
303
|
|
|
285
304
|
private mergeAllSettings(): void {
|
|
286
|
-
const allSettings
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
this.
|
|
305
|
+
const allSettings: Array<{
|
|
306
|
+
layer: SettingsLayer;
|
|
307
|
+
settings: ClaudeCodeSettings;
|
|
308
|
+
}> = [
|
|
309
|
+
{ layer: "user", settings: this.userSettings },
|
|
310
|
+
{ layer: "project", settings: this.projectSettings },
|
|
311
|
+
{ layer: "local", settings: this.localSettings },
|
|
312
|
+
{ layer: "enterprise", settings: this.enterpriseSettings },
|
|
291
313
|
];
|
|
292
314
|
|
|
293
315
|
const permissions: PermissionSettings = {
|
|
@@ -298,7 +320,7 @@ export class SettingsManager {
|
|
|
298
320
|
const merged: ClaudeCodeSettings = { permissions };
|
|
299
321
|
const posthogApprovedExecTools = new Set<string>();
|
|
300
322
|
|
|
301
|
-
for (const settings of allSettings) {
|
|
323
|
+
for (const { layer, settings } of allSettings) {
|
|
302
324
|
if (settings.permissions) {
|
|
303
325
|
if (settings.permissions.allow) {
|
|
304
326
|
permissions.allow?.push(...settings.permissions.allow);
|
|
@@ -325,6 +347,11 @@ export class SettingsManager {
|
|
|
325
347
|
if (settings.model) {
|
|
326
348
|
merged.model = settings.model;
|
|
327
349
|
}
|
|
350
|
+
merged.availableModels = mergeAvailableModels(
|
|
351
|
+
merged.availableModels,
|
|
352
|
+
settings.availableModels,
|
|
353
|
+
layer,
|
|
354
|
+
);
|
|
328
355
|
if (settings.posthogApprovedExecTools) {
|
|
329
356
|
for (const tool of settings.posthogApprovedExecTools) {
|
|
330
357
|
posthogApprovedExecTools.add(tool);
|
|
@@ -29,8 +29,10 @@ export const WEB_TOOLS: Set<string> = new Set(["WebSearch", "WebFetch"]);
|
|
|
29
29
|
export const AGENT_TOOLS: Set<string> = new Set([
|
|
30
30
|
"Task",
|
|
31
31
|
"Agent",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
32
|
+
"TaskCreate",
|
|
33
|
+
"TaskUpdate",
|
|
34
|
+
"TaskGet",
|
|
35
|
+
"TaskList",
|
|
34
36
|
]);
|
|
35
37
|
|
|
36
38
|
const BASE_ALLOWED_TOOLS = [
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
import type { Pushable } from "../../utils/streams";
|
|
12
12
|
import type { BaseSession } from "../base-acp-agent";
|
|
13
13
|
import type { ContextBreakdownBaseline } from "./context-breakdown";
|
|
14
|
+
import type { TaskState } from "./conversion/task-state";
|
|
14
15
|
import type { McpToolApprovals } from "./mcp/tool-metadata";
|
|
15
16
|
import type { SettingsManager } from "./session/settings";
|
|
16
17
|
import type { CodeExecutionMode } from "./tools";
|
|
@@ -75,6 +76,13 @@ export type Session = BaseSession & {
|
|
|
75
76
|
* "command is genuinely unknown" when the session goes idle without an echo.
|
|
76
77
|
*/
|
|
77
78
|
knownSlashCommands?: Set<string>;
|
|
79
|
+
/**
|
|
80
|
+
* Per-session task list accumulated from Task* tool calls.
|
|
81
|
+
* SDK >=0.3.142 replaced TodoWrite (snapshot) with TaskCreate/TaskUpdate
|
|
82
|
+
* (incremental, keyed by task id). Map iteration preserves insertion order
|
|
83
|
+
* which we use for plan entry ordering.
|
|
84
|
+
*/
|
|
85
|
+
taskState: TaskState;
|
|
78
86
|
};
|
|
79
87
|
|
|
80
88
|
export type ToolUseCache = {
|
|
@@ -466,7 +466,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
|
|
|
466
466
|
|
|
467
467
|
// Carry taskRunId/taskId across load so prompt() still emits cloud
|
|
468
468
|
// notifications (TURN_COMPLETE, USAGE_UPDATE) after a reload. newSession
|
|
469
|
-
// and
|
|
469
|
+
// and resumeSession both do this; loadSession historically did
|
|
470
470
|
// not, which silently broke task-completion tracking on re-attach.
|
|
471
471
|
resetSessionState(this.sessionState, params.sessionId, params.cwd, {
|
|
472
472
|
taskRunId: meta?.taskRunId,
|
|
@@ -489,7 +489,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
|
|
|
489
489
|
return response;
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
-
async
|
|
492
|
+
async resumeSession(
|
|
493
493
|
params: ResumeSessionRequest,
|
|
494
494
|
): Promise<ResumeSessionResponse> {
|
|
495
495
|
const meta = params._meta as NewSessionMeta | undefined;
|