From 99ef643c8e38efb8196d4a869c9640eac8752a10 Mon Sep 17 00:00:00 2001 From: Gagancreates Date: Mon, 1 Jun 2026 13:33:02 +0530 Subject: [PATCH] feat(code-mode): add ACP client engine (Layer 2 core) Own the Agent Client Protocol client instead of shelling out to `acpx`, so code mode can stream structured events (tool calls, diffs, plan) and surface live permission requests. Headless acpx can't do live approvals (it only supports --approve-all), which is why we drive the agent adapters ourselves. - code-mode/acp/{agents,client,permission-broker,session-store,manager,types}.ts: headless engine driving the Claude/Codex ACP adapters; one warm session per chat with create-or-resume via session/load; approval policy (ask | auto-approve-reads | yolo) in the broker. - claude-exec.ts: cross-platform claude resolver (Windows .cmd EINVAL fix + macOS/Linux GUI-PATH safety net) shared with the legacy acpx path in builtin-tools.ts. - add @agentclientprotocol/sdk + claude/codex adapters to core. --- apps/x/packages/core/package.json | 3 + .../core/src/application/lib/builtin-tools.ts | 57 +-- .../packages/core/src/code-mode/acp/agents.ts | 53 +++ .../core/src/code-mode/acp/claude-exec.ts | 91 +++++ .../packages/core/src/code-mode/acp/client.ts | 178 +++++++++ .../core/src/code-mode/acp/manager.ts | 103 +++++ .../src/code-mode/acp/permission-broker.ts | 91 +++++ .../core/src/code-mode/acp/session-store.ts | 43 ++ .../packages/core/src/code-mode/acp/types.ts | 43 ++ apps/x/packages/core/src/code-mode/status.ts | 2 +- apps/x/pnpm-lock.yaml | 366 ++++++++++++++++++ 11 files changed, 973 insertions(+), 57 deletions(-) create mode 100644 apps/x/packages/core/src/code-mode/acp/agents.ts create mode 100644 apps/x/packages/core/src/code-mode/acp/claude-exec.ts create mode 100644 apps/x/packages/core/src/code-mode/acp/client.ts create mode 100644 apps/x/packages/core/src/code-mode/acp/manager.ts create mode 100644 apps/x/packages/core/src/code-mode/acp/permission-broker.ts create mode 100644 apps/x/packages/core/src/code-mode/acp/session-store.ts create mode 100644 apps/x/packages/core/src/code-mode/acp/types.ts diff --git a/apps/x/packages/core/package.json b/apps/x/packages/core/package.json index b552eab7..08c2644d 100644 --- a/apps/x/packages/core/package.json +++ b/apps/x/packages/core/package.json @@ -11,6 +11,9 @@ "test:watch": "vitest" }, "dependencies": { + "@agentclientprotocol/claude-agent-acp": "^0.39.0", + "@agentclientprotocol/codex-acp": "^0.0.44", + "@agentclientprotocol/sdk": "^0.22.1", "@ai-sdk/anthropic": "^2.0.63", "@ai-sdk/google": "^2.0.53", "@ai-sdk/openai": "^2.0.91", diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts index 9bfb4250..89554a10 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts @@ -1,7 +1,6 @@ import { z, ZodType } from "zod"; import * as path from "path"; import * as fs from "fs/promises"; -import { existsSync, readFileSync } from "fs"; import { executeCommand, executeCommandAbortable } from "./command-executor.js"; import { resolveSkill, availableSkills } from "../assistant/skills/index.js"; import { executeTool, listServers, listTools } from "../../mcp/mcp.js"; @@ -16,6 +15,7 @@ import { executeAction as executeComposioAction, isConfigured as isComposioConfi import { CURATED_TOOLKITS, CURATED_TOOLKIT_SLUGS } from "@x/shared/dist/composio.js"; import { BrowserControlInputSchema, type BrowserControlInput } from "@x/shared/dist/browser-control.js"; import { BackgroundTaskSchema, TriggersSchema } from "@x/shared/dist/background-task.js"; +import { resolveClaudeExeOnWindows } from "../../code-mode/acp/claude-exec.js"; // Inputs for the bg-task builtin tools. Reuse the canonical schema field // descriptions; only `triggers` gets a tighter contextual override (the @@ -90,61 +90,6 @@ const LLMPARSE_MIME_TYPES: Record = { '.tiff': 'image/tiff', }; -// Windows-only workaround: the Claude ACP bridge spawns CLAUDE_CODE_EXECUTABLE -// without `shell: true`, and Node refuses to spawn .cmd files that way (EINVAL). -// When the LLM invokes acpx via executeCommand, pre-resolve claude's real .exe -// from the npm-shim layout and inject it via env so the bridge can spawn it. -function resolveClaudeExeOnWindows(): string | undefined { - // Candidate dirs = everything on PATH, plus well-known npm/pnpm/volta global - // bin dirs. Electron's runtime PATH can omit these even when the user's shell - // includes them, which would otherwise leave us unable to find claude.exe and - // force a fallback to claude.cmd (which Node refuses to spawn — EINVAL). - const home = process.env.USERPROFILE ?? ''; - const appData = process.env.APPDATA || (home && path.join(home, 'AppData', 'Roaming')); - const localAppData = process.env.LOCALAPPDATA || (home && path.join(home, 'AppData', 'Local')); - const programFiles = process.env.ProgramFiles || 'C:\\Program Files'; - const knownDirs = [ - appData && path.join(appData, 'npm'), - localAppData && path.join(localAppData, 'npm'), - appData && path.join(appData, 'pnpm'), - localAppData && path.join(localAppData, 'pnpm'), - home && path.join(home, '.volta', 'bin'), - path.join(programFiles, 'nodejs'), - ].filter(Boolean) as string[]; - - const pathDirs = (process.env.PATH ?? '').split(';').map((d) => d.trim()).filter(Boolean); - const seen = new Set(); - const candidates = [...pathDirs, ...knownDirs].filter((d) => { - const key = d.toLowerCase(); - if (seen.has(key)) return false; - seen.add(key); - return true; - }); - - for (const dir of candidates) { - // Direct npm-shim layout: \node_modules\@anthropic-ai\claude-code\bin\claude.exe - const exeFromLayout = path.join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'); - if (existsSync(exeFromLayout)) return exeFromLayout; - - // Otherwise parse the claude.cmd shim for the real exe path. - const cmdPath = path.join(dir, 'claude.cmd'); - if (!existsSync(cmdPath)) continue; - try { - const content = readFileSync(cmdPath, 'utf-8'); - const absMatch = content.match(/[A-Z]:[\\/][^\s"]*claude\.exe/i); - if (absMatch && existsSync(absMatch[0])) return absMatch[0]; - const relMatch = content.match(/%~dp0[\\/]?([^\s"%]+claude\.exe)/i); - if (relMatch) { - const resolved = path.join(dir, relMatch[1]); - if (existsSync(resolved)) return resolved; - } - } catch { - // ignore shim parse failures - } - } - return undefined; -} - function envForCommand(command: string): NodeJS.ProcessEnv | undefined { if (process.platform !== 'win32') return undefined; if (!/\bacpx\b/.test(command)) return undefined; diff --git a/apps/x/packages/core/src/code-mode/acp/agents.ts b/apps/x/packages/core/src/code-mode/acp/agents.ts new file mode 100644 index 00000000..cf9e74e3 --- /dev/null +++ b/apps/x/packages/core/src/code-mode/acp/agents.ts @@ -0,0 +1,53 @@ +import { createRequire } from 'module'; +import * as path from 'path'; +import type { CodingAgent } from './types.js'; +import { resolveClaudeExecutable } from './claude-exec.js'; + +const require = createRequire(import.meta.url); + +// The ACP adapter npm package that exposes each coding agent as an ACP server. +const ADAPTER_PACKAGE: Record = { + claude: '@agentclientprotocol/claude-agent-acp', + codex: '@agentclientprotocol/codex-acp', +}; + +export interface AgentLaunchSpec { + /** Executable to spawn — always `node` so we never hit the Windows .cmd EINVAL. */ + command: string; + /** Args = [adapter entry script]. */ + args: string[]; + /** Extra env merged over process.env (e.g. CLAUDE_CODE_EXECUTABLE on Windows). */ + env: NodeJS.ProcessEnv; +} + +// Resolve the adapter's executable ENTRY (its `bin`, not its library `main`) to an +// absolute path so we can spawn it directly with `node `. createRequire lets +// us resolve workspace/pnpm-installed packages from this module's location. +function resolveAdapterEntry(pkg: string): string { + const pkgJsonPath = require.resolve(`${pkg}/package.json`); + const pkgDir = path.dirname(pkgJsonPath); + const pkgJson = require(`${pkg}/package.json`) as { bin?: string | Record }; + const bin = pkgJson.bin; + const rel = typeof bin === 'string' ? bin : bin ? Object.values(bin)[0] : undefined; + if (!rel) { + throw new Error(`ACP adapter ${pkg} has no bin entry to spawn`); + } + return path.join(pkgDir, rel); +} + +export function getAgentLaunchSpec(agent: CodingAgent): AgentLaunchSpec { + const entry = resolveAdapterEntry(ADAPTER_PACKAGE[agent]); + const env: NodeJS.ProcessEnv = { ...process.env }; + + // Point the Claude adapter at the real claude executable. On Windows this is + // mandatory (Node can't spawn the .cmd shim — EINVAL); on macOS/Linux it's a + // PATH safety net for GUI launches. Resolver is a no-op when claude isn't found, + // leaving the adapter to do its own lookup. (Codex relies on PATH for now — wire + // an equivalent when we add Codex support.) + if (agent === 'claude' && !env.CLAUDE_CODE_EXECUTABLE) { + const exe = resolveClaudeExecutable(); + if (exe) env.CLAUDE_CODE_EXECUTABLE = exe; + } + + return { command: process.execPath, args: [entry], env }; +} diff --git a/apps/x/packages/core/src/code-mode/acp/claude-exec.ts b/apps/x/packages/core/src/code-mode/acp/claude-exec.ts new file mode 100644 index 00000000..31c7c4e8 --- /dev/null +++ b/apps/x/packages/core/src/code-mode/acp/claude-exec.ts @@ -0,0 +1,91 @@ +import { execSync } from 'child_process'; +import * as path from 'path'; +import { existsSync, readFileSync } from 'fs'; +import { commonInstallPaths } from '../status.js'; + +// Windows-only: Node refuses to spawn `.cmd` files without `shell: true` (EINVAL), +// and the Claude ACP adapter spawns its executable directly. So we pre-resolve +// claude's real `.exe` from the npm-shim layout. Also used by the legacy acpx path. +export function resolveClaudeExeOnWindows(): string | undefined { + // Candidate dirs = everything on PATH, plus well-known npm/pnpm/volta global + // bin dirs. Electron's runtime PATH can omit these even when the user's shell + // includes them, which would otherwise leave us unable to find claude.exe and + // force a fallback to claude.cmd (which Node refuses to spawn — EINVAL). + const home = process.env.USERPROFILE ?? ''; + const appData = process.env.APPDATA || (home && path.join(home, 'AppData', 'Roaming')); + const localAppData = process.env.LOCALAPPDATA || (home && path.join(home, 'AppData', 'Local')); + const programFiles = process.env.ProgramFiles || 'C:\\Program Files'; + const knownDirs = [ + appData && path.join(appData, 'npm'), + localAppData && path.join(localAppData, 'npm'), + appData && path.join(appData, 'pnpm'), + localAppData && path.join(localAppData, 'pnpm'), + home && path.join(home, '.volta', 'bin'), + path.join(programFiles, 'nodejs'), + ].filter(Boolean) as string[]; + + const pathDirs = (process.env.PATH ?? '').split(';').map((d) => d.trim()).filter(Boolean); + const seen = new Set(); + const candidates = [...pathDirs, ...knownDirs].filter((d) => { + const key = d.toLowerCase(); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + + for (const dir of candidates) { + // Direct npm-shim layout: \node_modules\@anthropic-ai\claude-code\bin\claude.exe + const exeFromLayout = path.join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'); + if (existsSync(exeFromLayout)) return exeFromLayout; + + // Otherwise parse the claude.cmd shim for the real exe path. + const cmdPath = path.join(dir, 'claude.cmd'); + if (!existsSync(cmdPath)) continue; + try { + const content = readFileSync(cmdPath, 'utf-8'); + const absMatch = content.match(/[A-Z]:[\\/][^\s"]*claude\.exe/i); + if (absMatch && existsSync(absMatch[0])) return absMatch[0]; + const relMatch = content.match(/%~dp0[\\/]?([^\s"%]+claude\.exe)/i); + if (relMatch) { + const resolved = path.join(dir, relMatch[1]); + if (existsSync(resolved)) return resolved; + } + } catch { + // ignore shim parse failures + } + } + return undefined; +} + +// macOS/Linux: find the real `claude` binary. Unlike Windows this isn't a spawn +// requirement (no .cmd problem) — it's a PATH safety net. Electron apps launched +// from the GUI (Dock/Finder) often don't inherit the login shell's PATH, so the +// spawned adapter may fail to find `claude`. We resolve the path here so the adapter +// can be pointed straight at it. +function resolveClaudeBinaryUnix(): string | undefined { + // Primary: a login shell sees the user's full PATH (~/.zprofile, nvm, homebrew, …). + try { + const out = execSync("/bin/sh -lc 'command -v claude'", { timeout: 5000, encoding: 'utf-8' }).trim(); + if (out && existsSync(out)) return out; + } catch { + // not found on the login-shell PATH + } + // Fallback: scan well-known install locations directly. + for (const candidate of commonInstallPaths('claude')) { + if (existsSync(candidate)) return candidate; + } + return undefined; +} + +let cached: string | undefined; + +// Cross-platform: the real `claude` executable to hand the ACP adapter via +// CLAUDE_CODE_EXECUTABLE (the adapter prefers this env var on every OS). Returns +// undefined if it can't be found — callers then fall back to the adapter's own lookup. +// Cached on first success so we don't re-probe the shell on every cold start. +export function resolveClaudeExecutable(): string | undefined { + if (cached) return cached; + const resolved = process.platform === 'win32' ? resolveClaudeExeOnWindows() : resolveClaudeBinaryUnix(); + if (resolved) cached = resolved; + return resolved; +} diff --git a/apps/x/packages/core/src/code-mode/acp/client.ts b/apps/x/packages/core/src/code-mode/acp/client.ts new file mode 100644 index 00000000..8f73f7a7 --- /dev/null +++ b/apps/x/packages/core/src/code-mode/acp/client.ts @@ -0,0 +1,178 @@ +import { spawn, type ChildProcess } from 'child_process'; +import { Writable, Readable } from 'node:stream'; +import fs from 'fs/promises'; +import { + ClientSideConnection, + ndJsonStream, + PROTOCOL_VERSION, + type Client, + type RequestPermissionRequest, + type RequestPermissionResponse, + type SessionNotification, + type SessionUpdate, + type PromptResponse, + type ReadTextFileRequest, + type ReadTextFileResponse, + type WriteTextFileRequest, + type WriteTextFileResponse, +} from '@agentclientprotocol/sdk'; +import type { CodingAgent, CodeRunEvent } from './types.js'; +import type { PermissionBroker } from './permission-broker.js'; +import { getAgentLaunchSpec } from './agents.js'; + +export interface AcpClientOptions { + agent: CodingAgent; + cwd: string; + broker: PermissionBroker; + onEvent: (event: CodeRunEvent) => void; +} + +// Map a raw ACP session/update notification onto our small CodeRunEvent union. +function toEvent(update: SessionUpdate): CodeRunEvent { + switch (update.sessionUpdate) { + case 'agent_message_chunk': + case 'user_message_chunk': { + const c = update.content; + const role = update.sessionUpdate === 'user_message_chunk' ? 'user' : 'agent'; + return { type: 'message', role, text: c.type === 'text' ? c.text : `[${c.type}]` }; + } + case 'agent_thought_chunk': + return { type: 'thought' }; + case 'tool_call': + return { + type: 'tool_call', + id: update.toolCallId, + title: update.title, + kind: update.kind ?? undefined, + status: update.status ?? undefined, + }; + case 'tool_call_update': { + const diffs = (update.content ?? []) + .filter((c): c is Extract => c.type === 'diff') + .map((c) => c.path); + return { type: 'tool_call_update', id: update.toolCallId, status: update.status ?? undefined, diffs }; + } + case 'plan': + return { + type: 'plan', + entries: (update.entries ?? []).map((e) => ({ + content: e.content, + status: e.status ?? undefined, + priority: e.priority ?? undefined, + })), + }; + default: + return { type: 'other', sessionUpdate: update.sessionUpdate }; + } +} + +// Owns one spawned adapter process + ACP connection. Stateless about sessions — +// the manager decides whether to newSession or loadSession. +// +// The connection is long-lived and reused across follow-up prompts, but each prompt +// may stream to a different message's UI, so broker + onEvent are swappable via +// setHandlers() rather than fixed at construction. +export class AcpClient { + readonly agent: CodingAgent; + readonly cwd: string; + private broker: PermissionBroker; + private onEvent: (event: CodeRunEvent) => void; + private child?: ChildProcess; + private connection?: ClientSideConnection; + private loadSession_ = false; + + constructor(opts: AcpClientOptions) { + this.agent = opts.agent; + this.cwd = opts.cwd; + this.broker = opts.broker; + this.onEvent = opts.onEvent; + } + + get loadSupported(): boolean { + return this.loadSession_; + } + + // Re-point the live connection at a new prompt's broker / event sink. + setHandlers(broker: PermissionBroker, onEvent: (event: CodeRunEvent) => void): void { + this.broker = broker; + this.onEvent = onEvent; + } + + // Spawn the adapter and negotiate the protocol. Returns once initialized. + async start(): Promise { + const spec = getAgentLaunchSpec(this.agent); + const child = spawn(spec.command, spec.args, { + cwd: this.cwd, + env: spec.env, + stdio: ['pipe', 'pipe', 'inherit'], + }); + this.child = child; + + const stream = ndJsonStream( + Writable.toWeb(child.stdin!) as WritableStream, + Readable.toWeb(child.stdout!) as ReadableStream, + ); + const client = this.buildClient(); + this.connection = new ClientSideConnection(() => client, stream); + + const init = await this.connection.initialize({ + protocolVersion: PROTOCOL_VERSION, + clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } }, + }); + this.loadSession_ = init.agentCapabilities?.loadSession === true; + } + + async newSession(): Promise { + const res = await this.conn().newSession({ cwd: this.cwd, mcpServers: [] }); + return res.sessionId; + } + + async loadSession(sessionId: string): Promise { + await this.conn().loadSession({ sessionId, cwd: this.cwd, mcpServers: [] }); + } + + async prompt(sessionId: string, text: string): Promise { + return this.conn().prompt({ sessionId, prompt: [{ type: 'text', text }] }); + } + + async cancel(sessionId: string): Promise { + await this.conn().cancel({ sessionId }); + } + + dispose(): void { + try { + this.child?.kill(); + } catch { + // already gone + } + this.child = undefined; + this.connection = undefined; + } + + private conn(): ClientSideConnection { + if (!this.connection) throw new Error('AcpClient not started'); + return this.connection; + } + + // The client side of ACP: the agent calls these on us. These read the CURRENT + // handlers off `self` so follow-up prompts can swap them via setHandlers(). + private buildClient(): Client { + const self = this; + return { + async requestPermission(params: RequestPermissionRequest): Promise { + return self.broker.resolve(params); + }, + async sessionUpdate(params: SessionNotification): Promise { + self.onEvent(toEvent(params.update)); + }, + async readTextFile(params: ReadTextFileRequest): Promise { + const content = await fs.readFile(params.path, 'utf8'); + return { content }; + }, + async writeTextFile(params: WriteTextFileRequest): Promise { + await fs.writeFile(params.path, params.content); + return {}; + }, + }; + } +} diff --git a/apps/x/packages/core/src/code-mode/acp/manager.ts b/apps/x/packages/core/src/code-mode/acp/manager.ts new file mode 100644 index 00000000..7e813ae4 --- /dev/null +++ b/apps/x/packages/core/src/code-mode/acp/manager.ts @@ -0,0 +1,103 @@ +import type { ApprovalPolicy, CodeRunEvent, CodingAgent, PermissionAsk, PermissionDecision, RunPromptResult } from './types.js'; +import { AcpClient } from './client.js'; +import { PermissionBroker } from './permission-broker.js'; +import { readStoredSession, writeStoredSession, clearStoredSession } from './session-store.js'; + +export interface RunPromptArgs { + runId: string; + agent: CodingAgent; + cwd: string; + prompt: string; + policy: ApprovalPolicy; + /** Called when the policy needs the user to decide (the "ask" path). */ + ask: (ask: PermissionAsk) => Promise; + /** Stream sink for this prompt's run. */ + onEvent: (event: CodeRunEvent) => void; +} + +interface ActiveRun { + client: AcpClient; + sessionId: string; + agent: CodingAgent; + cwd: string; +} + +// Drives ACP coding sessions, one live connection per chat run. Reuses a warm +// connection for follow-up prompts in the same chat; resumes a persisted session +// (via session/load) on the first prompt after an app restart. +export class CodeModeManager { + private readonly runs = new Map(); + + async runPrompt(args: RunPromptArgs): Promise { + const { runId, agent, cwd, prompt, policy, ask, onEvent } = args; + + const broker = new PermissionBroker({ + policy, + ask, + onResolved: (a, decision, auto) => onEvent({ type: 'permission', ask: a, decision, auto }), + }); + + const run = await this.ensureRun(runId, agent, cwd, broker, onEvent); + const res = await run.client.prompt(run.sessionId, prompt); + return { stopReason: res.stopReason, sessionId: run.sessionId }; + } + + async cancel(runId: string): Promise { + const run = this.runs.get(runId); + if (run) await run.client.cancel(run.sessionId); + } + + dispose(runId: string): void { + const run = this.runs.get(runId); + if (!run) return; + run.client.dispose(); + this.runs.delete(runId); + } + + disposeAll(): void { + for (const runId of [...this.runs.keys()]) this.dispose(runId); + } + + // Reuse the warm connection if it matches; otherwise (cold start, or the user + // switched agent/cwd for this chat) build a fresh one and create-or-resume its session. + private async ensureRun( + runId: string, + agent: CodingAgent, + cwd: string, + broker: PermissionBroker, + onEvent: (event: CodeRunEvent) => void, + ): Promise { + const existing = this.runs.get(runId); + if (existing && existing.agent === agent && existing.cwd === cwd) { + existing.client.setHandlers(broker, onEvent); + return existing; + } + if (existing) this.dispose(runId); // agent/cwd changed — start over + + const client = new AcpClient({ agent, cwd, broker, onEvent }); + await client.start(); + + const sessionId = await this.openSession(runId, agent, cwd, client); + const run: ActiveRun = { client, sessionId, agent, cwd }; + this.runs.set(runId, run); + return run; + } + + // Resume the persisted session for this chat when possible; else start a new one + // and persist its id so a later restart can resume it. + private async openSession(runId: string, agent: CodingAgent, cwd: string, client: AcpClient): Promise { + const stored = await readStoredSession(runId); + if (stored && stored.agent === agent && stored.cwd === cwd && client.loadSupported) { + try { + await client.loadSession(stored.sessionId); + return stored.sessionId; + } catch { + // Stored session is stale/unloadable — fall through to a fresh one. + await clearStoredSession(runId); + } + } + const sessionId = await client.newSession(); + await writeStoredSession({ runId, agent, cwd, sessionId }); + return sessionId; + } +} diff --git a/apps/x/packages/core/src/code-mode/acp/permission-broker.ts b/apps/x/packages/core/src/code-mode/acp/permission-broker.ts new file mode 100644 index 00000000..9699dec4 --- /dev/null +++ b/apps/x/packages/core/src/code-mode/acp/permission-broker.ts @@ -0,0 +1,91 @@ +import type { + RequestPermissionRequest, + RequestPermissionResponse, + PermissionOption, + PermissionOptionKind, +} from '@agentclientprotocol/sdk'; +import type { ApprovalPolicy, PermissionDecision, PermissionAsk } from './types.js'; + +// Tool kinds that don't mutate anything — eligible for `auto-approve-reads`. +const READ_KINDS = new Set(['read', 'search', 'fetch', 'think']); + +function toAsk(request: RequestPermissionRequest): PermissionAsk { + const tc = request.toolCall; + const kind = tc.kind ?? undefined; + const title = tc.title ?? kind ?? 'Tool call'; + return { + toolCallId: tc.toolCallId ?? undefined, + title, + kind, + isRead: kind ? READ_KINDS.has(kind) : false, + }; +} + +// Map a desired decision to one of the options the agent actually offered. +// Agents may offer only a subset (e.g. allow_once + reject_once, no allow_always), +// so we fall back within the same allow/reject family before giving up. +function pickOption(options: PermissionOption[], decision: PermissionDecision): PermissionOption | undefined { + const order: Record = { + allow_always: ['allow_always', 'allow_once'], + allow_once: ['allow_once', 'allow_always'], + reject: ['reject_once', 'reject_always'], + }; + for (const kind of order[decision]) { + const found = options.find((o) => o.kind === kind); + if (found) return found; + } + return undefined; +} + +function selected(optionId: string): RequestPermissionResponse { + return { outcome: { outcome: 'selected', optionId } }; +} + +// A request's identity for "always allow" memory: prefer tool kind, else title. +function memoryKey(ask: PermissionAsk): string { + return ask.kind ? `kind:${ask.kind}` : `title:${ask.title}`; +} + +export interface PermissionBrokerOptions { + policy: ApprovalPolicy; + // Called only when the policy can't decide on its own (the "ask" path). + ask: (ask: PermissionAsk) => Promise; + // Notified of every resolved request so the engine can emit a stream event. + onResolved?: (ask: PermissionAsk, decision: PermissionDecision, auto: boolean) => void; +} + +// Decides how to answer the agent's requestPermission calls. Holds per-session +// "always allow" memory so a one-time approval sticks for the rest of the run. +export class PermissionBroker { + private readonly opts: PermissionBrokerOptions; + private readonly alwaysAllow = new Set(); + + constructor(opts: PermissionBrokerOptions) { + this.opts = opts; + } + + async resolve(request: RequestPermissionRequest): Promise { + const ask = toAsk(request); + const key = memoryKey(ask); + + const finish = (decision: PermissionDecision, auto: boolean): RequestPermissionResponse => { + if (decision === 'allow_always') this.alwaysAllow.add(key); + this.opts.onResolved?.(ask, decision, auto); + const opt = pickOption(request.options, decision); + // If the agent offered no matching option we fall back to its first one + // (don't deadlock the turn); decision precedence above keeps this rare. + return selected(opt?.optionId ?? request.options[0]?.optionId ?? ''); + }; + + // 1. Sticky "always allow" from earlier this session. + if (this.alwaysAllow.has(key)) return finish('allow_always', true); + + // 2. Policy-level auto decisions. + if (this.opts.policy === 'yolo') return finish('allow_always', true); + if (this.opts.policy === 'auto-approve-reads' && ask.isRead) return finish('allow_once', true); + + // 3. Ask the user. + const decision = await this.opts.ask(ask); + return finish(decision, false); + } +} diff --git a/apps/x/packages/core/src/code-mode/acp/session-store.ts b/apps/x/packages/core/src/code-mode/acp/session-store.ts new file mode 100644 index 00000000..82e3eb68 --- /dev/null +++ b/apps/x/packages/core/src/code-mode/acp/session-store.ts @@ -0,0 +1,43 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { WorkDir } from '../../config/config.js'; +import type { CodingAgent } from './types.js'; + +// One ACP session is pinned per chat run. We persist its sessionId (plus the agent +// and cwd it belongs to) so reopening the chat after an app restart can resume the +// same agent context via session/load instead of starting over. +export interface StoredSession { + runId: string; + agent: CodingAgent; + cwd: string; + sessionId: string; +} + +function sessionFile(runId: string): string { + return path.join(WorkDir, 'config', `codesession-${runId}.json`); +} + +export async function readStoredSession(runId: string): Promise { + try { + const raw = await fs.readFile(sessionFile(runId), 'utf8'); + const parsed = JSON.parse(raw) as StoredSession; + if (parsed && parsed.sessionId && parsed.agent && parsed.cwd) return parsed; + return null; + } catch { + return null; + } +} + +export async function writeStoredSession(session: StoredSession): Promise { + const file = sessionFile(session.runId); + await fs.mkdir(path.dirname(file), { recursive: true }); + await fs.writeFile(file, JSON.stringify(session, null, 2)); +} + +export async function clearStoredSession(runId: string): Promise { + try { + await fs.rm(sessionFile(runId), { force: true }); + } catch { + // best effort + } +} diff --git a/apps/x/packages/core/src/code-mode/acp/types.ts b/apps/x/packages/core/src/code-mode/acp/types.ts new file mode 100644 index 00000000..31e41369 --- /dev/null +++ b/apps/x/packages/core/src/code-mode/acp/types.ts @@ -0,0 +1,43 @@ +// Rowboat-facing types for the ACP code-mode engine. These are intentionally +// decoupled from the raw @agentclientprotocol/sdk schema so the IPC layer (Phase 2) +// and renderer (Phase 3) consume a small, stable surface instead of the full protocol. + +export type CodingAgent = 'claude' | 'codex'; + +// How the permission broker answers an agent's requestPermission, before any +// per-tool "allow for this session" memory is applied. +// ask -> surface every gated action to the user +// auto-approve-reads -> silently allow read-only tool calls, ask for the rest +// yolo -> auto-approve everything (the safe, scoped equivalent of +// `claude --dangerously-skip-permissions` — our toggle, not a flag) +export type ApprovalPolicy = 'ask' | 'auto-approve-reads' | 'yolo'; + +// A user's decision for a single permission request. +export type PermissionDecision = 'allow_once' | 'allow_always' | 'reject'; + +// What we hand to the UI (Phase 3) when the agent asks for permission. +export interface PermissionAsk { + toolCallId?: string; + title: string; + kind?: string; // tool kind, e.g. "edit" | "execute" | "read" + /** Whether this looks like a read-only action (used by auto-approve-reads). */ + isRead: boolean; +} + +// Normalized stream events emitted for a coding run. The renderer renders these; +// the engine maps raw ACP session/update notifications onto this union. +export type CodeRunEvent = + // role distinguishes the agent's own output from replayed user turns + // (loadSession streams the whole prior conversation back on resume). + | { type: 'message'; role: 'agent' | 'user'; text: string } + | { type: 'thought' } + | { type: 'tool_call'; id?: string; title?: string; kind?: string; status?: string } + | { type: 'tool_call_update'; id?: string; status?: string; diffs: string[] } + | { type: 'plan'; entries: { content: string; status?: string; priority?: string }[] } + | { type: 'permission'; ask: PermissionAsk; decision: PermissionDecision | 'cancelled'; auto: boolean } + | { type: 'other'; sessionUpdate: string }; + +export interface RunPromptResult { + stopReason: string; + sessionId: string; +} diff --git a/apps/x/packages/core/src/code-mode/status.ts b/apps/x/packages/core/src/code-mode/status.ts index 3858708b..a78b23f4 100644 --- a/apps/x/packages/core/src/code-mode/status.ts +++ b/apps/x/packages/core/src/code-mode/status.ts @@ -12,7 +12,7 @@ const execAsync = promisify(exec); // We scan these directly because Electron's spawned shell sometimes doesn't // inherit the user's full PATH (especially on macOS GUI launches, and even on // Windows when global npm prefix isn't propagated to system PATH). -function commonInstallPaths(binary: string): string[] { +export function commonInstallPaths(binary: string): string[] { const home = os.homedir(); if (process.platform === 'win32') { const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming'); diff --git a/apps/x/pnpm-lock.yaml b/apps/x/pnpm-lock.yaml index 6c78cdce..02cb3068 100644 --- a/apps/x/pnpm-lock.yaml +++ b/apps/x/pnpm-lock.yaml @@ -362,6 +362,15 @@ importers: packages/core: dependencies: + '@agentclientprotocol/claude-agent-acp': + specifier: ^0.39.0 + version: 0.39.0(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1)) + '@agentclientprotocol/codex-acp': + specifier: ^0.0.44 + version: 0.0.44(zod@4.2.1) + '@agentclientprotocol/sdk': + specifier: ^0.22.1 + version: 0.22.1(zod@4.2.1) '@ai-sdk/anthropic': specifier: ^2.0.63 version: 2.0.70(zod@4.2.1) @@ -489,6 +498,24 @@ importers: packages: + '@agentclientprotocol/claude-agent-acp@0.39.0': + resolution: {integrity: sha512-+tCm5v32L0R3zE4qjZQowfO1L/zqvQ5FapmsMSIf4gawXfTf26CG5hgz99wARdo0zn20/1eP80gzx7PbZlSX9A==} + hasBin: true + + '@agentclientprotocol/codex-acp@0.0.44': + resolution: {integrity: sha512-iHzFWKzJ0Z8I6yJCkuLZ+nb9mF2WYmfTcHFFvc7sU/awBsQmVBmpSOXOpZ+IK2Dy9cR3iRoML/B2/Wq2/zKBCA==} + hasBin: true + + '@agentclientprotocol/sdk@0.21.1': + resolution: {integrity: sha512-ZTLH+o9QxcZDLX/9ww+W7C2iExnXFM+vD/uGFVSlR61Kzj9FaxUqBC6Rv/kwgA7qVWYUEI9c5ZNqCuO9PM4rKg==} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + '@agentclientprotocol/sdk@0.22.1': + resolution: {integrity: sha512-DfqXtl/8gO9NImq094MTaCXEU2vkhh6v7q/kT+9UjZxUqj8hYaya2OjLVIqn16MzNHcXEpShTR2RIauLSYeDQQ==} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + '@ai-sdk/anthropic@2.0.70': resolution: {integrity: sha512-W3WjQlb0Ho+CVAQUvb8Rtk3hGS3Jlgy79ihY2H0yj2k4yU8XuxpQw0Oz+7JQsB47j+jlHhk7nUXtxhAeRg3S3Q==} engines: {node: '>=18'} @@ -544,6 +571,67 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.156': + resolution: {integrity: sha512-IkjcS9dqAUlD4Nb62L9AZtmAXCa+FV4ul8lIlyXXUprh3nlecbKsWOXVd/GORrzAhMmynJaX4+iV1JiutFKXUA==} + cpu: [arm64] + os: [darwin] + + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.156': + resolution: {integrity: sha512-6PKi5fPmGRuzXu+Em/iwLmPG3mqg0hl92wcTU8fmChqyNtxhxsjCw7LTbdFqp/05o5NeZVVV4k3p7YUv5IFD6g==} + cpu: [x64] + os: [darwin] + + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.156': + resolution: {integrity: sha512-R7KEVjxkR4rYgIQoHGBzwPdUJYxRTO8I4vHjRbMLH1eW4FS7BJvVs7ogfKR/NnHFBvMVqtC+l6jHLQv8bobUiw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.156': + resolution: {integrity: sha512-H0Nfd41iw5isto9uQI1FlVSZ0eaDttr8rBpJMR25oK/mj3egMO5EmZ6aAxeeUYSLn2mSU50HA5VNxlGUE118TQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.156': + resolution: {integrity: sha512-/Q6WUizI6a+hqZZ6ElwRU0PEuFhOoN4v6CuU35HHbiZ/7uaocGht4A8ZIgK1Fw6wOGtZzGLbc00CA1OU1Zg8EA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.156': + resolution: {integrity: sha512-ymhrdlbWoYvTACUdaGdhrEv+ZMfwXLsf0BRLkr/IvY5aqybP7URzWmmZGOtDQpqkT/8xu/UCGqUYH3woJwUxfg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.156': + resolution: {integrity: sha512-5sAeNObQQrMy4NF9HwxewrMnU7mVxZDHh+/MfJVQSz0GSTvXQ6gOuRH8helMlfspoU6VOdekPxVLRooX/3foEw==} + cpu: [arm64] + os: [win32] + + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.156': + resolution: {integrity: sha512-/PofeTWoiKgnWNSNk0wG4SsRn22GGLmnLhg2R94WcNhCRFOyOTmiZcYH2DBlWZBIRVTZDsSfa/Pl1DyPvYCGKw==} + cpu: [x64] + os: [win32] + + '@anthropic-ai/claude-agent-sdk@0.3.156': + resolution: {integrity: sha512-6nM/Dj+VMds52UXJ2YaV4IKhYamlUqN0HtdDrFzYz5lvPMpDS935qD8YZDAUpy+ltdoD6PJMd1V/CKFY3/oWCQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@anthropic-ai/sdk': '>=0.93.0' + '@modelcontextprotocol/sdk': ^1.29.0 + zod: ^4.0.0 + + '@anthropic-ai/sdk@0.100.1': + resolution: {integrity: sha512-RANcEe7LpiLczkKGOwoXOTuFdPhuubS0i4xaAKOMpcqc55YO0mukgxppV7eygx3DXNjxWT6RYOLPyOy0aIAmwg==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -1743,6 +1831,47 @@ packages: resolution: {integrity: sha512-6gH/bLQJSJEg7OEpkH4wGQdA8KXHRbzL1YkGyUO12YNAgV3jxKy4K9kvfXj4+9T0OLug5k58cnPCKSSIKzp7pg==} engines: {node: '>=8.0'} + '@openai/codex@0.128.0': + resolution: {integrity: sha512-+xp6ODmFfBNnexIWRHApEaPXot2j6gyM8A5we/5IS/uY4eYHj4arETct4hQ5M4eO+MK7JY3ZU4xhuobhlysr0A==} + engines: {node: '>=16'} + hasBin: true + + '@openai/codex@0.128.0-darwin-arm64': + resolution: {integrity: sha512-w+6zohfHx/kHBdles/CyFKaY57u9I3nK8QI9+NrdwMliKA0b7xn13yblRNkMpe09j6vL1oAWoxYsMOQ/vjBGug==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@openai/codex@0.128.0-darwin-x64': + resolution: {integrity: sha512-SDbn6fO22Puy8xmMIbZi4f2znMrUEPwABApke4mo+4ihaauwuVjeqzXvW5SPJz5ty/bG11/mSupQgReT7T8BBw==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@openai/codex@0.128.0-linux-arm64': + resolution: {integrity: sha512-+SvH73H60qvCXFuQGP/EsmR//s1hHMBR22PvJkXvM/hdnTIGucx+JqRUjAWdmmQ1IU6j3kgwVvdLW/6ICB+M6w==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@openai/codex@0.128.0-linux-x64': + resolution: {integrity: sha512-2lnSPA05CRRuKAzFW8BCmmNCSieDcToLwfC2ALLbBYilGLgzhRibjlDglK9F1BkEzfohSSWJu4PBbRu/aG60lQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@openai/codex@0.128.0-win32-arm64': + resolution: {integrity: sha512-ECJvsqmYFdA9pn42xxK3Odp/G16AjmBW0BglX8L0PwPjqbstbmlew9bfHf7xvL+SNfNl4NmyotW0+RNo1phgaA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [win32] + + '@openai/codex@0.128.0-win32-x64': + resolution: {integrity: sha512-k3jmUAFrzkUtvjGTXvSKjQqJLLlzjxp/VoHJDYedgmXUn6j70HxK38IwapzmnYfiBiTuzETvGwjXHzZgzKjhoQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + '@openrouter/ai-sdk-provider@1.5.4': resolution: {integrity: sha512-xrSQPUIH8n9zuyYZR0XK7Ba0h2KsjJcMkxnwaYfmv13pKs3sDkjPzVPPhlhzqBGddHb5cFEwJ9VFuFeDcxCDSw==} engines: {node: '>=18'} @@ -3057,6 +3186,9 @@ packages: resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} engines: {node: '>=18.0.0'} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -4060,6 +4192,10 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -4540,6 +4676,14 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -4551,6 +4695,10 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -4592,6 +4740,10 @@ packages: diff3@0.0.3: resolution: {integrity: sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==} + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + dingbat-to-unicode@1.0.1: resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==} @@ -4942,6 +5094,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} @@ -5541,6 +5696,11 @@ packages: engines: {node: '>=8'} hasBin: true + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -5560,6 +5720,15 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -5617,6 +5786,10 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -5677,6 +5850,10 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -6431,6 +6608,10 @@ packages: oniguruma-to-es@4.3.4: resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + open@7.4.2: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} @@ -6679,6 +6860,10 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + preact@10.28.2: resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==} @@ -7097,6 +7282,10 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -7305,6 +7494,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -7523,6 +7715,9 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -7827,6 +8022,10 @@ packages: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} + vscode-jsonrpc@8.2.1: + resolution: {integrity: sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==} + engines: {node: '>=14.0.0'} + vscode-languageserver-protocol@3.17.5: resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} @@ -7933,6 +8132,10 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + x-is-array@0.1.0: resolution: {integrity: sha512-goHPif61oNrr0jJgsXRfc8oqtYzvfiMJpTqwE7Z4y9uH+T3UozkGqQ4d2nX9mB9khvA8U2o/UbPOFjgC7hLWIA==} @@ -8028,6 +8231,33 @@ packages: snapshots: + '@agentclientprotocol/claude-agent-acp@0.39.0(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1))': + dependencies: + '@agentclientprotocol/sdk': 0.22.1(zod@4.2.1) + '@anthropic-ai/claude-agent-sdk': 0.3.156(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1))(zod@4.2.1) + zod: 4.2.1 + transitivePeerDependencies: + - '@anthropic-ai/sdk' + - '@modelcontextprotocol/sdk' + + '@agentclientprotocol/codex-acp@0.0.44(zod@4.2.1)': + dependencies: + '@agentclientprotocol/sdk': 0.21.1(zod@4.2.1) + '@openai/codex': 0.128.0 + diff: 8.0.4 + open: 11.0.0 + vscode-jsonrpc: 8.2.1 + transitivePeerDependencies: + - zod + + '@agentclientprotocol/sdk@0.21.1(zod@4.2.1)': + dependencies: + zod: 4.2.1 + + '@agentclientprotocol/sdk@0.22.1(zod@4.2.1)': + dependencies: + zod: 4.2.1 + '@ai-sdk/anthropic@2.0.70(zod@4.2.1)': dependencies: '@ai-sdk/provider': 2.0.1 @@ -8089,6 +8319,52 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.0.2 + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.156': + optional: true + + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.156': + optional: true + + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.156': + optional: true + + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.156': + optional: true + + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.156': + optional: true + + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.156': + optional: true + + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.156': + optional: true + + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.156': + optional: true + + '@anthropic-ai/claude-agent-sdk@0.3.156(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1))(zod@4.2.1)': + dependencies: + '@anthropic-ai/sdk': 0.100.1(zod@4.2.1) + '@modelcontextprotocol/sdk': 1.25.1(hono@4.11.3)(zod@4.2.1) + zod: 4.2.1 + optionalDependencies: + '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.156 + '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.156 + '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.156 + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.156 + '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.156 + '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.156 + '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.156 + '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.156 + + '@anthropic-ai/sdk@0.100.1(zod@4.2.1)': + dependencies: + json-schema-to-ts: 3.1.1 + standardwebhooks: 1.0.0 + optionalDependencies: + zod: 4.2.1 + '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -9819,6 +10095,33 @@ snapshots: '@oozcitak/util@8.3.4': {} + '@openai/codex@0.128.0': + optionalDependencies: + '@openai/codex-darwin-arm64': '@openai/codex@0.128.0-darwin-arm64' + '@openai/codex-darwin-x64': '@openai/codex@0.128.0-darwin-x64' + '@openai/codex-linux-arm64': '@openai/codex@0.128.0-linux-arm64' + '@openai/codex-linux-x64': '@openai/codex@0.128.0-linux-x64' + '@openai/codex-win32-arm64': '@openai/codex@0.128.0-win32-arm64' + '@openai/codex-win32-x64': '@openai/codex@0.128.0-win32-x64' + + '@openai/codex@0.128.0-darwin-arm64': + optional: true + + '@openai/codex@0.128.0-darwin-x64': + optional: true + + '@openai/codex@0.128.0-linux-arm64': + optional: true + + '@openai/codex@0.128.0-linux-x64': + optional: true + + '@openai/codex@0.128.0-win32-arm64': + optional: true + + '@openai/codex@0.128.0-win32-x64': + optional: true + '@openrouter/ai-sdk-provider@1.5.4(ai@5.0.151(zod@4.2.1))(zod@4.2.1)': dependencies: '@openrouter/sdk': 0.1.27 @@ -11301,6 +11604,8 @@ snapshots: dependencies: tslib: 2.8.1 + '@stablelib/base64@1.0.1': {} + '@standard-schema/spec@1.1.0': {} '@standard-schema/utils@0.3.0': {} @@ -12431,6 +12736,10 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + bytes@3.1.2: {} cacache@16.1.3: @@ -12925,6 +13234,13 @@ snapshots: deep-is@0.1.4: {} + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + defaults@1.0.4: dependencies: clone: 1.0.4 @@ -12937,6 +13253,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + define-lazy-prop@3.0.0: {} + define-properties@1.2.1: dependencies: define-data-property: 1.1.4 @@ -12971,6 +13289,8 @@ snapshots: diff3@0.0.3: {} + diff@8.0.4: {} + dingbat-to-unicode@1.0.1: {} dir-compare@4.2.0: @@ -13473,6 +13793,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-sha256@1.3.0: {} + fast-uri@3.1.0: {} fast-xml-parser@5.2.5: @@ -14248,6 +14570,8 @@ snapshots: is-docker@2.2.1: {} + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -14260,6 +14584,12 @@ snapshots: is-hexadecimal@2.0.1: {} + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-interactive@1.0.0: {} is-lambda@1.0.1: {} @@ -14310,6 +14640,10 @@ snapshots: dependencies: is-docker: 2.2.1 + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + isarray@1.0.0: {} isarray@2.0.5: {} @@ -14378,6 +14712,11 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.28.6 + ts-algebra: 2.0.0 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -15367,6 +15706,15 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + open@7.4.2: dependencies: is-docker: 2.2.1 @@ -15606,6 +15954,8 @@ snapshots: dependencies: commander: 9.5.0 + powershell-utils@0.1.0: {} + preact@10.28.2: {} prelude-ls@1.2.1: {} @@ -16189,6 +16539,8 @@ snapshots: transitivePeerDependencies: - supports-color + run-applescript@7.1.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -16424,6 +16776,11 @@ snapshots: stackback@0.0.2: {} + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + statuses@2.0.2: {} std-env@4.1.0: {} @@ -16664,6 +17021,8 @@ snapshots: trough@2.2.0: {} + ts-algebra@2.0.0: {} + ts-api-utils@2.1.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -16965,6 +17324,8 @@ snapshots: vscode-jsonrpc@8.2.0: {} + vscode-jsonrpc@8.2.1: {} + vscode-languageserver-protocol@3.17.5: dependencies: vscode-jsonrpc: 8.2.0 @@ -17097,6 +17458,11 @@ snapshots: wrappy@1.0.2: {} + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 + x-is-array@0.1.0: {} x-is-string@0.1.0: {}