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>
This commit is contained in:
gsxdsm
2026-04-23 16:48:57 -07:00
parent 82b4c696d8
commit f4e0850f9d
44 changed files with 9390 additions and 76 deletions

View File

@@ -15,6 +15,7 @@ import {
import type { ToastType } from "../hooks/useToast";
import { CustomModelDropdown } from "./CustomModelDropdown";
import { ProviderIcon } from "./ProviderIcon";
import { ClaudeCliProviderCard } from "./ClaudeCliProviderCard";
import { appendTokenQuery } from "../auth";
/** Provider-specific API key setup metadata for onboarding form rendering */
@@ -493,7 +494,6 @@ export function ModelOnboardingModal({
const [authActionInProgress, setAuthActionInProgress] = useState<string | null>(null);
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
const [selectedModel, setSelectedModel] = useState<string>("");
const [useClaudeCli, setUseClaudeCli] = useState<boolean>(false);
const [saving, setSaving] = useState(false);
const [apiKeyInputs, setApiKeyInputs] = useState<Record<string, string>>({});
const [apiKeyErrors, setApiKeyErrors] = useState<Record<string, string>>({});
@@ -1151,20 +1151,13 @@ export function ModelOnboardingModal({
}
}
// Only write useClaudeCli when the user explicitly opted in; leaving
// the field undefined keeps the legacy packages-array fallback in play
// for anyone who had pi-claude-cli installed before this toggle existed.
if (useClaudeCli) {
updates.useClaudeCli = true;
}
await updateGlobalSettings(updates);
// Mark onboarding as completed (preserves state for completion timestamp)
markOnboardingCompleted();
} catch {
// Best-effort: continue even if save fails
}
}, [selectedModel, availableModels, useClaudeCli, updateGlobalSettings, markOnboardingCompleted]);
}, [selectedModel, availableModels, updateGlobalSettings, markOnboardingCompleted]);
// Complete onboarding
const handleComplete = useCallback(async () => {
@@ -1296,6 +1289,7 @@ export function ModelOnboardingModal({
(p) => !p.type || p.type === "oauth",
);
const apiKeyProviders = authProviders.filter((p) => p.type === "api_key");
const cliProviders = authProviders.filter((p) => p.type === "cli");
// Filter out GitHub from AI providers list
const aiOauthProviders = oauthProviders.filter((p) => p.id !== "github");
@@ -1716,6 +1710,23 @@ export function ModelOnboardingModal({
</>
)}
{/* Claude CLI — synthetic provider card. Rendered alongside
OAuth + API-key cards but with its own action set
(Enable/Disable + Test) since it's backed by a binary
probe rather than stored credentials. */}
{cliProviders.some((p) => p.id === "claude-cli") && (
<ClaudeCliProviderCard
authenticated={
cliProviders.find((p) => p.id === "claude-cli")?.authenticated ?? false
}
onToggled={() => {
// Refetch auth status so the parent provider list
// reflects the new authenticated state.
void loadAuthStatus();
}}
/>
)}
{/* Model Selection */}
<div className="onboarding-model-section">
<h3 className="onboarding-section-title">
@@ -1761,38 +1772,6 @@ export function ModelOnboardingModal({
)}
</div>
{/* Claude CLI routing toggle.
Opt-in: we only flip useClaudeCli=true when the user ticks
this. The backend install hooks (fn init, dashboard project
add, server startup) will then symlink the fusion skill into
every project's .claude/skills/fusion so Claude Code can see
fn_* tools. */}
<div className="onboarding-model-section">
<h3 className="onboarding-section-title">
Use the Claude CLI? (Optional)
</h3>
<label htmlFor="onboarding-useClaudeCli" className="checkbox-label">
<input
id="onboarding-useClaudeCli"
type="checkbox"
checked={useClaudeCli}
onChange={(e) => setUseClaudeCli(e.target.checked)}
/>
Route AI calls through my locally-installed Claude CLI
</label>
<OnboardingDisclosure summary="What does this do?">
<p className="onboarding-helper-text">
If you already have Claude CLI installed and an active
Claude subscription, Fusion can send its model calls through
the CLI instead of the Anthropic API using your existing
quota. Requires the <code>pi-claude-cli</code> pi extension.
Fusion will also install its skill into each project's{" "}
<code>.claude/skills/fusion/</code> so Claude Code can use
Fusion's tools directly. You can toggle this later in
Settings Global Models.
</p>
</OnboardingDisclosure>
</div>
</div>
)}