Files
fusion/plugins/fusion-plugin-claude-runtime/src/cli-spawn.ts
gsxdsm adb3eb3a37 FN-8224: add Claude ACP runtime support
Add a bundled Claude Code ACP runtime with model discovery and CLI distribution integration.

- Add the Claude ACP runtime plugin, bridge, tool forwarding, and tests.
- Register Claude model discovery and cached picker support in the dashboard.
- Stage the plugin in CLI and desktop packaging with its pinned dependency.
- Add workspace, lockfile, and release metadata.

Files changed:
 .changeset/fn-8224-claude-acp-runtime.md           |   7 +
 packages/cli/package.json                          |   1 +
 packages/cli/src/__tests__/bundle-output.test.ts   |  32 +-
 .../cli/src/plugins/staged-bundled-plugin-ids.ts   |   1 +
 packages/cli/tsup.config.ts                        |  40 +-
 .../core/src/plugins/bundled-plugin-install.ts     |   2 +
 packages/dashboard/package.json                    |   3 +-
 packages/dashboard/src/claude-model-cache.ts       | 185 +++++++
 packages/dashboard/src/routes.ts                   |   1 +
 .../dashboard/src/routes/register-model-routes.ts  |  11 +
 packages/dashboard/src/runtime-provider-probes.ts  |  15 +
 packages/dashboard/vitest.config.ts                |   4 +
 packages/desktop/scripts/workspace-tools.ts        |   1 +
 plugins/fusion-plugin-claude-runtime/README.md     |   7 +
 plugins/fusion-plugin-claude-runtime/manifest.json |   1 +
 plugins/fusion-plugin-claude-runtime/package.json  |   1 +
 .../src/__tests__/cli-spawn.test.ts                |   3 +
 .../src/__tests__/index.test.ts                    |   3 +
 .../src/__tests__/provider.test.ts                 |   8 +
 .../src/__tests__/runtime-adapter.test.ts          |   7 +
 .../src/__tests__/tool-bridge.test.ts              |  79 +++
 .../src/acp-settings.ts                            |  21 +
 .../src/acp/VENDORED.md                            |  30 ++
 .../src/acp/cli-spawn.ts                           | 188 +++++++
 .../src/acp/control-handler.ts                     | 302 ++++++++++++
 .../src/acp/event-bridge.ts                        | 308 ++++++++++++
 .../src/acp/fs-capabilities.ts                     | 263 ++++++++++
 .../fusion-plugin-claude-runtime/src/acp/index.ts  |  16 +
 .../src/acp/path-jail.ts                           | 229 +++++++++
 .../src/acp/process-manager.ts                     | 177 +++++++
 .../src/acp/prompt-builder.ts                      |  87 ++++
 .../src/acp/provider.ts                            | 542 +++++++++++++++++++++
 .../src/acp/runtime-adapter.ts                     | 190 ++++++++
 .../src/acp/sanitize.ts                            |  81 +++
 .../src/acp/tool-mapping.ts                        |  47 ++
 .../fusion-plugin-claude-runtime/src/acp/types.ts  | 189 +++++++
 .../fusion-plugin-claude-runtime/src/cli-spawn.ts  |  31 ++
 plugins/fusion-plugin-claude-runtime/src/index.ts  |  95 ++++
 .../src/mcp-forwarding.ts                          | 114 +++++
 .../src/mcp-schema-server.cjs                      | 155 ++++++
 plugins/fusion-plugin-claude-runtime/src/probe.ts  |   8 +
 .../fusion-plugin-claude-runtime/src/provider.ts   |   8 +
 .../src/runtime-adapter.ts                         | 424 ++++++++++++++++
 .../src/skill-loader.ts                            | 290 +++++++++++
 .../src/tool-bridge.ts                             | 264 ++++++++++
 plugins/fusion-plugin-claude-runtime/src/types.ts  | 142 ++++++
 plugins/fusion-plugin-claude-runtime/tsconfig.json |  10 +
 .../fusion-plugin-claude-runtime/vitest.config.ts  |  22 +
 pnpm-lock.yaml                                     | 131 ++++-
 pnpm-workspace.yaml                                |   1 +
 50 files changed, 4753 insertions(+), 24 deletions(-)

Fusion-Task-Id: FN-8224

Fusion-Task-Lineage: 4e626913-32bb-4baa-a142-816c7f4ee7c4

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-17 12:00:26 -07:00

32 lines
2.6 KiB
TypeScript

import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
export const CLAUDE_CODE_CLI_ACP_BINARY = "claude-code-cli-acp";
export interface ClaudeBridgeResolution { kind: "resolved" | "not_resolved"; requested: string; path?: string; reason?: string }
/*
FNXC:ClaudeAcp 2026-07-18-11:55:
The plugin runs both from source (`src/`), a standalone build (`dist/`), and
Fusion's single-file `bundled.js`. Only the first two sit one directory below the
plugin root; bundled.js sits at the root. Resolve the staged bridge relative to
that layout so published CLI sessions do not look in `dist/plugins/bridge`.
*/
function pluginRootDir(): string {
const moduleDir = dirname(fileURLToPath(import.meta.url));
return ["src", "dist"].includes(basename(moduleDir)) ? resolve(moduleDir, "..") : moduleDir;
}
/** Resolve only the identity-pinned bridge staged beside this bundled plugin. */
export function bundledClaudeBridgeBinPath(pluginRoot = pluginRootDir()): string {
return join(pluginRoot, "bridge", `${CLAUDE_CODE_CLI_ACP_BINARY}${process.platform === "win32" ? ".cmd" : ""}`);
}
export function resolveBundledClaudeBridgeBinary(options: { pluginRoot?: string; exists?: (path: string) => boolean } = {}): ClaudeBridgeResolution {
const candidate = bundledClaudeBridgeBinPath(options.pluginRoot ?? pluginRootDir());
const exists = options.exists ?? existsSync;
if (!exists(candidate) || !isAbsolute(candidate)) return { kind: "not_resolved", requested: CLAUDE_CODE_CLI_ACP_BINARY, path: candidate, reason: `Staged ${CLAUDE_CODE_CLI_ACP_BINARY} bridge was not found at ${candidate}` };
return { kind: "resolved", requested: CLAUDE_CODE_CLI_ACP_BINARY, path: candidate };
}
export async function runClaudeCommand(binary: string, args: string[], timeoutMs: number): Promise<{ code: number | null; stdout: string; stderr: string }> {
return new Promise((done) => { let stdout="", stderr="", settled=false; const finish=(r:{code:number|null;stdout:string;stderr:string})=>{if(!settled){settled=true;clearTimeout(timer);done(r)}}; const child=spawn(binary,args,{stdio:["ignore","pipe","pipe"],shell:process.platform==="win32"}); const timer=setTimeout(()=>{try{child.kill("SIGKILL")}catch{ /* process already exited */ } finish({code:124,stdout,stderr})},timeoutMs); child.stdout?.on("data",c=>stdout+=String(c)); child.stderr?.on("data",c=>stderr+=String(c)); child.once("error",e=>finish({code:127,stdout,stderr:`${stderr}${e.message}`})); child.once("close",code=>finish({code,stdout,stderr})); });
}