diff --git a/packages/server/src/server/agent/providers/claude-agent.env.test.ts b/packages/server/src/server/agent/providers/claude-agent.env.test.ts index c3a1c9a45..26b0e7f83 100644 --- a/packages/server/src/server/agent/providers/claude-agent.env.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.env.test.ts @@ -66,6 +66,7 @@ describe("Claude SDK env", () => { const client = new ClaudeAgentClient({ logger: createTestLogger(), queryFactory, + resolveBinary: async () => "/test/claude/bin", }); const session = await client.createSession( { @@ -123,6 +124,7 @@ describe("Claude SDK env", () => { const client = new ClaudeAgentClient({ logger: createTestLogger(), queryFactory, + resolveBinary: async () => "/test/claude/bin", }); const session = await client.resumeSession( { diff --git a/packages/server/src/server/agent/providers/claude-agent.interrupt-restart-regression.test.ts b/packages/server/src/server/agent/providers/claude-agent.interrupt-restart-regression.test.ts index 4ad86767a..bcf4c2e91 100644 --- a/packages/server/src/server/agent/providers/claude-agent.interrupt-restart-regression.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.interrupt-restart-regression.test.ts @@ -261,7 +261,7 @@ test("interrupt only calls query.interrupt and leaves the query open", async () return scriptedQuery; }); - const client = new ClaudeAgentClient({ logger }); + const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" }); const session = await client.createSession({ provider: "claude", cwd: process.cwd(), @@ -311,7 +311,7 @@ test("reuses the existing query after interrupt before starting the next prompt" return scriptedQuery; }); - const client = new ClaudeAgentClient({ logger }); + const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" }); const session = await client.createSession({ provider: "claude", cwd: process.cwd(), @@ -361,7 +361,7 @@ test("emits an assistant system notice when Claude changes session id mid-turn", return queryRef; }); - const client = new ClaudeAgentClient({ logger }); + const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" }); const session = await client.createSession({ provider: "claude", cwd: process.cwd(), @@ -455,7 +455,7 @@ test("recovers when the query pump sees a single interrupt abort before the next return scriptedQuery; }); - const client = new ClaudeAgentClient({ logger }); + const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" }); const session = await client.createSession({ provider: "claude", cwd: process.cwd(), @@ -488,7 +488,7 @@ test("stale abort result after replacement start does not poison the new foregro return queryRef; }); - const client = new ClaudeAgentClient({ logger }); + const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" }); const session = await client.createSession({ provider: "claude", cwd: process.cwd(), @@ -564,7 +564,7 @@ test("creates an autonomous live turn when assistant output arrives without a fo return queryRef; }); - const client = new ClaudeAgentClient({ logger }); + const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" }); const session = await client.createSession({ provider: "claude", cwd: process.cwd(), @@ -634,7 +634,7 @@ test("auto-completes an open autonomous turn when a foreground prompt starts", a return queryRef; }); - const client = new ClaudeAgentClient({ logger }); + const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" }); const session = await client.createSession({ provider: "claude", cwd: process.cwd(), diff --git a/packages/server/src/server/agent/providers/claude-agent.redesign.test.ts b/packages/server/src/server/agent/providers/claude-agent.redesign.test.ts index 12b6cfca2..5befae014 100644 --- a/packages/server/src/server/agent/providers/claude-agent.redesign.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.redesign.test.ts @@ -66,6 +66,7 @@ async function createSession() { const client = new ClaudeAgentClient({ logger: createTestLogger(), queryFactory: sdkQueryFactory, + resolveBinary: async () => "/test/claude/bin", }); return client.createSession({ provider: "claude", @@ -77,6 +78,7 @@ function createSessionWithLogger(logger: Logger) { const client = new ClaudeAgentClient({ logger, queryFactory: sdkQueryFactory, + resolveBinary: async () => "/test/claude/bin", }); return client.createSession({ provider: "claude", @@ -223,6 +225,7 @@ test("logs redacted query summary and never leaks sentinel secrets", async () => PASEO_RUNTIME_SENTINEL_SECRET: runtimeSecret, }, }, + resolveBinary: async () => "/test/claude/bin", }); const session = await client.createSession({ provider: "claude", @@ -797,6 +800,7 @@ test("captures Claude stderr in the turn failure diagnostic when stderr arrives const client = new ClaudeAgentClient({ logger: loggerSpy.logger, queryFactory: sdkQueryFactory, + resolveBinary: async () => "/test/claude/bin", }); const session = await client.createSession({ provider: "claude", diff --git a/packages/server/src/server/agent/providers/claude-agent.spawn.test.ts b/packages/server/src/server/agent/providers/claude-agent.spawn.test.ts index d57f07402..5382d30a5 100644 --- a/packages/server/src/server/agent/providers/claude-agent.spawn.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.spawn.test.ts @@ -77,6 +77,7 @@ describe("Claude spawn override", () => { const client = new ClaudeAgentClient({ logger: createTestLogger(), queryFactory, + resolveBinary: async () => "/test/claude/bin", }); const session = await client.createSession({ provider: "claude", diff --git a/packages/server/src/server/agent/providers/claude-agent.sub-agent-sidechain.test.ts b/packages/server/src/server/agent/providers/claude-agent.sub-agent-sidechain.test.ts index 9b7744a22..9f340de09 100644 --- a/packages/server/src/server/agent/providers/claude-agent.sub-agent-sidechain.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.sub-agent-sidechain.test.ts @@ -251,7 +251,10 @@ describe("ClaudeAgentSession sub-agent sidechain updates", () => { }); test("accumulates lightweight sub_agent detail and preserves callId lifecycle collapse", async () => { - const session = await new ClaudeAgentClient({ logger }).createSession({ + const session = await new ClaudeAgentClient({ + logger, + resolveBinary: async () => "/test/claude/bin", + }).createSession({ provider: "claude", cwd: process.cwd(), }); @@ -300,7 +303,10 @@ describe("ClaudeAgentSession sub-agent sidechain updates", () => { test("tails sub-agent actions instead of dropping latest entries at cap", async () => { sdkMocks.query.mockImplementation(() => buildQueryMock(buildTailScenarioEvents(205))); - const session = await new ClaudeAgentClient({ logger }).createSession({ + const session = await new ClaudeAgentClient({ + logger, + resolveBinary: async () => "/test/claude/bin", + }).createSession({ provider: "claude", cwd: process.cwd(), }); diff --git a/packages/server/src/server/agent/providers/claude-agent.test.ts b/packages/server/src/server/agent/providers/claude-agent.test.ts index aba3740cc..98878b0a9 100644 --- a/packages/server/src/server/agent/providers/claude-agent.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.test.ts @@ -346,7 +346,7 @@ describe("ClaudeAgentClient.listModels", () => { const logger = createTestLogger(); test("returns hardcoded claude models", async () => { - const client = new ClaudeAgentClient({ logger }); + const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" }); const models = await client.listModels({ cwd: "/tmp/claude-models", force: false }); expect(models.map((m) => m.id)).toEqual([ @@ -430,7 +430,10 @@ describe("normalizeClaudeAskUserQuestionUpdatedInput", () => { }); test("respondToPermission preserves full question input when UI returns answers-only payload", async () => { - const client = new ClaudeAgentClient({ logger: createTestLogger() }); + const client = new ClaudeAgentClient({ + logger: createTestLogger(), + resolveBinary: async () => "/test/claude/bin", + }); const session = await client.createSession({ provider: "claude", cwd: process.cwd(), @@ -505,7 +508,7 @@ describe("ClaudeAgentSession context window usage", () => { const logger = createTestLogger(); async function createSessionForTest(): Promise { - const client = new ClaudeAgentClient({ logger }); + const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" }); const session = await client.createSession({ provider: "claude", cwd: process.cwd(), @@ -605,6 +608,7 @@ describe("ClaudeAgentSession context window usage", () => { const nonPersistedClient = new ClaudeAgentClient({ logger, queryFactory: nonPersistedQueryFactory, + resolveBinary: async () => "/test/claude/bin", }); const nonPersistedSession = await nonPersistedClient.createSession( { @@ -623,6 +627,7 @@ describe("ClaudeAgentSession context window usage", () => { const persistedClient = new ClaudeAgentClient({ logger, queryFactory: persistedQueryFactory, + resolveBinary: async () => "/test/claude/bin", }); const persistedSession = await persistedClient.createSession( { @@ -980,7 +985,11 @@ describe("ClaudeAgentSession context window usage", () => { }, ], ]); - const client = new ClaudeAgentClient({ logger, queryFactory }); + const client = new ClaudeAgentClient({ + logger, + queryFactory, + resolveBinary: async () => "/test/claude/bin", + }); const session = await client.createSession({ provider: "claude", cwd: process.cwd(), diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index 8145ea035..55be78869 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -254,6 +254,7 @@ interface ClaudeAgentClientOptions { logger: Logger; runtimeSettings?: ProviderRuntimeSettings; queryFactory?: typeof query; + resolveBinary?: () => Promise; } interface ClaudeAgentSessionOptions { @@ -264,6 +265,7 @@ interface ClaudeAgentSessionOptions { persistSession?: boolean; logger: Logger; queryFactory?: typeof query; + resolveBinary: () => Promise; } type ClaudeThinkingEffort = "low" | "medium" | "high" | "xhigh" | "max"; @@ -1251,12 +1253,14 @@ export class ClaudeAgentClient implements AgentClient { private readonly logger: Logger; private readonly runtimeSettings?: ProviderRuntimeSettings; private readonly queryFactory: typeof query; + private readonly resolveBinary: () => Promise; constructor(options: ClaudeAgentClientOptions) { this.defaults = options.defaults; this.logger = options.logger.child({ module: "agent", provider: "claude" }); this.runtimeSettings = options.runtimeSettings; this.queryFactory = options.queryFactory ?? query; + this.resolveBinary = options.resolveBinary ?? resolveClaudeBinary; } async createSession( @@ -1272,6 +1276,7 @@ export class ClaudeAgentClient implements AgentClient { persistSession: options?.persistSession, logger: this.logger, queryFactory: this.queryFactory, + resolveBinary: this.resolveBinary, }); } @@ -1298,6 +1303,7 @@ export class ClaudeAgentClient implements AgentClient { launchEnv: launchContext?.env, logger: this.logger, queryFactory: this.queryFactory, + resolveBinary: this.resolveBinary, }); } @@ -1381,6 +1387,16 @@ export class ClaudeAgentClient implements AgentClient { } } +async function resolveClaudeBinary(): Promise { + const found = await findExecutable("claude"); + if (found) { + return found; + } + throw new Error( + "Claude binary not found. Install Claude Code (https://github.com/anthropics/claude-code) and ensure it is available in your shell PATH.", + ); +} + async function resolveClaudeVersion( runtimeSettings?: ProviderRuntimeSettings, ): Promise { @@ -1549,6 +1565,7 @@ class ClaudeAgentSession implements AgentSession { private readonly persistSession?: boolean; private readonly logger: Logger; private readonly queryFactory: typeof query; + private readonly resolveBinary: () => Promise; private query: Query | null = null; private input: AsyncMessageInput | null = null; private claudeSessionId: string | null; @@ -1597,6 +1614,7 @@ class ClaudeAgentSession implements AgentSession { this.persistSession = options.persistSession; this.logger = options.logger; this.queryFactory = options.queryFactory ?? query; + this.resolveBinary = options.resolveBinary; const handle = options.handle; if (handle) { @@ -2283,7 +2301,7 @@ class ClaudeAgentSession implements AgentSession { ], }); - const claudeBinary = await findExecutable("claude"); + const claudeBinary = await this.resolveBinary(); this.logger.debug( { claudeBinary, @@ -2304,7 +2322,7 @@ class ClaudeAgentSession implements AgentSession { allowDangerouslySkipPermissions: true, agents: this.defaults?.agents, canUseTool: this.handlePermissionRequest, - ...(claudeBinary ? { pathToClaudeCodeExecutable: claudeBinary } : {}), + pathToClaudeCodeExecutable: claudeBinary, // Use Claude Code preset system prompt and load CLAUDE.md files // Append provider-agnostic system prompt and orchestrator instructions for agents. systemPrompt: { diff --git a/packages/server/src/server/agent/providers/claude-agent.voice-history-regression.test.ts b/packages/server/src/server/agent/providers/claude-agent.voice-history-regression.test.ts index e10f1af97..af0393141 100644 --- a/packages/server/src/server/agent/providers/claude-agent.voice-history-regression.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.voice-history-regression.test.ts @@ -157,7 +157,7 @@ describe("ClaudeAgentSession history replay regression", () => { test("does not replay persisted history during the first live stream turn", async () => { const logger = createTestLogger(); - const client = new ClaudeAgentClient({ logger }); + const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" }); const handle: AgentPersistenceHandle = { provider: "claude", sessionId: "history-session", @@ -194,7 +194,7 @@ describe("ClaudeAgentSession history replay regression", () => { test("still exposes persisted history through streamHistory", async () => { const logger = createTestLogger(); - const client = new ClaudeAgentClient({ logger }); + const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" }); const handle: AgentPersistenceHandle = { provider: "claude", sessionId: "history-session", @@ -223,7 +223,7 @@ describe("ClaudeAgentSession history replay regression", () => { test("listCommands includes rewind command", async () => { const logger = createTestLogger(); - const client = new ClaudeAgentClient({ logger }); + const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" }); const handle: AgentPersistenceHandle = { provider: "claude", sessionId: "history-session", @@ -245,7 +245,7 @@ describe("ClaudeAgentSession history replay regression", () => { test("slash /rewind uses latest user message id from persisted history", async () => { const logger = createTestLogger(); - const client = new ClaudeAgentClient({ logger }); + const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" }); const handle: AgentPersistenceHandle = { provider: "claude", sessionId: "history-session",