Files
fusion/packages/pi-claude-cli/index.ts
gsxdsm f4e0850f9d feat: add "Anthropic — via Claude CLI" as a first-class provider
Replaces the stray useClaudeCli settings checkbox + onboarding question
with a proper provider-card UX. The card lives next to OAuth + API-key
cards in onboarding and settings, with Enable/Disable + Test actions.

Backend:
 - Vendors rchern/pi-claude-cli@0.3.1 as packages/pi-claude-cli
   (MIT, attribution in UPSTREAM.md). Lets us bump peer-dep on
   pi-coding-agent in lockstep with Fusion (upstream pinned ^0.52.0
   vs ours ^0.62.0) and fix bugs without waiting on upstream.
 - Adds @fusion/pi-claude-cli as a workspace dep of @runfusion/fusion
   so users don't have to `npm install -g pi-claude-cli` manually.
 - serve/daemon/dashboard conditionally load the extension via
   discoverAndLoadExtensions() when GlobalSettings.useClaudeCli is on;
   no side-effects on user ~/.fusion/agent/settings.json.
 - New GET /api/providers/claude-cli/status: claude --version probe
   + toggle state + cached extension resolution.
 - New POST /api/auth/claude-cli: flips useClaudeCli, refuses if the
   claude binary is missing, fires the existing skill-backfill hook.
 - /api/auth/status now injects a synthetic {id:"claude-cli", type:"cli"}
   provider entry so onboarding + settings see a consistent list.

Frontend:
 - New ClaudeCliProviderCard component shared between ModelOnboardingModal
   and SettingsModal's Authentication section.
 - New AuthProvider.type = "cli" variant.
 - Removed the old "Route AI calls through the Claude CLI" checkbox from
   Global Models settings and the opt-in step from the onboarding wizard.
 - ProviderIcon gets a composite Anthropic-mark-plus-terminal glyph for
   the claude-cli provider id.

Tests:
 - 8 unit tests for extension resolution (@fusion/pi-claude-cli is
   workspace-linked so these run in-tree).
 - 2 unit tests for the binary probe.
 - Existing /auth/status tests filter out the new synthetic entry so
   they keep asserting structural OAuth/API-key behavior in isolation.
 - The vendored package's own 296 tests still pass unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 23:02:34 -07:00

110 lines
3.3 KiB
TypeScript

/**
* Pi extension entry point for pi-claude-cli.
*
* Registers a custom provider that routes LLM calls through the Claude Code CLI
* subprocess using stream-json NDJSON protocol.
*/
import { getModels } from "@mariozechner/pi-ai";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { streamViaCli } from "./src/provider.js";
import {
validateCliPresence,
validateCliAuth,
killAllProcesses,
} from "./src/process-manager.js";
import { getCustomToolDefs, writeMcpConfig } from "./src/mcp-config.js";
// Kill all active Claude subprocesses on process exit to prevent orphans
process.on("exit", killAllProcesses);
const PROVIDER_ID = "pi-claude-cli";
let mcpConfigPath: string | undefined;
let mcpConfigResolved = false;
/**
* Lazily generate MCP config on first request (not at load time).
* pi.getAllTools() fails during extension loading; this defers it
* until the pi runtime is fully initialized.
*
* Only locks (sets mcpConfigResolved) when getAllTools() returns a
* real array — if it returns undefined/null (registry not ready),
* we retry on the next request. Once the registry is ready we
* commit to the result even if there are zero custom tools.
*
* Uses warn-don't-block: failure logs a warning but does not
* prevent the provider from functioning (built-ins still work).
*/
function ensureMcpConfig(pi: ExtensionAPI): string | undefined {
if (mcpConfigResolved) return mcpConfigPath;
try {
const allTools = pi.getAllTools();
// Registry not ready yet — don't lock, retry on next call
if (!Array.isArray(allTools)) {
return mcpConfigPath;
}
// Registry is ready — lock regardless of whether custom tools exist
mcpConfigResolved = true;
const toolDefs = getCustomToolDefs(pi);
if (toolDefs.length > 0) {
mcpConfigPath = writeMcpConfig(toolDefs);
console.error(
`[pi-claude-cli] MCP config generated with ${toolDefs.length} custom tool(s)`,
);
}
} catch (err) {
console.warn(
"[pi-claude-cli] MCP config generation failed, custom tools unavailable:",
err,
);
}
return mcpConfigPath;
}
export default function (pi: ExtensionAPI) {
try {
// Startup validation
validateCliPresence(); // throws if CLI not on PATH
validateCliAuth(); // warns if not authenticated
const models = getModels("anthropic").map((model) => ({
id: model.id,
name: model.name,
reasoning: model.reasoning,
input: model.input,
cost: model.cost,
contextWindow: model.contextWindow,
maxTokens: model.maxTokens,
}));
// Ensure all registered tools are active so pi can execute them.
// Some tools (find, grep, ls) are registered but not activated by default.
pi.on("session_start", async () => {
const allTools = pi.getAllTools();
if (Array.isArray(allTools)) {
pi.setActiveTools(allTools.map((t: any) => t.name));
}
});
pi.registerProvider(PROVIDER_ID, {
baseUrl: "pi-claude-cli",
apiKey: "unused",
api: "pi-claude-cli",
models,
streamSimple: (model, context, options) => {
const configPath = ensureMcpConfig(pi);
return streamViaCli(model, context, {
...options,
mcpConfigPath: configPath,
});
},
});
} catch (err) {
console.error(`[pi-claude-cli] Failed to register provider:`, err);
}
}