diff --git a/apps/macos/Tests/OpenClawIPCTests/OnboardingAISetupTests.swift b/apps/macos/Tests/OpenClawIPCTests/OnboardingAISetupTests.swift index b90f6e17eb01..e8c817ad0c62 100644 --- a/apps/macos/Tests/OpenClawIPCTests/OnboardingAISetupTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/OnboardingAISetupTests.swift @@ -709,17 +709,17 @@ struct OnboardingAISetupTests { #expect(local["modelRef"]?.value as? String == "lmstudio/qwen-local") } - @Test func `unavailable detected integration decodes for informational display`() throws { - let candidate = try JSONDecoder().decode( - OnboardingAISetupModel.UnavailableCandidate.self, + @Test func `unavailable detected integrations decode for informational display`() throws { + let candidates = try JSONDecoder().decode( + [OnboardingAISetupModel.UnavailableCandidate].self, from: Data( - #"{"id":"antigravity-cli","label":"Antigravity CLI","detail":"installed","reason":"Automatic setup cannot enforce a tool-free probe."}"#.utf8 + #"[{"id":"pi-cli","label":"Pi CLI","detail":"installed","reason":"Not a setup route."},{"id":"opencode-cli","label":"OpenCode CLI","detail":"installed","reason":"Not a setup route."}]"#.utf8 ) ) - #expect(candidate.id == "antigravity-cli") - #expect(candidate.label == "Antigravity CLI") - #expect(candidate.detail == "installed") + #expect(candidates.map(\.id) == ["pi-cli", "opencode-cli"]) + #expect(candidates.map(\.label) == ["Pi CLI", "OpenCode CLI"]) + #expect(candidates.allSatisfy { $0.detail == "installed" }) } @Test func `activation decodes and retains copyable setup lines`() throws { diff --git a/docs/cli/setup.md b/docs/cli/setup.md index a90d3f013943..9c9e0a14b3de 100644 --- a/docs/cli/setup.md +++ b/docs/cli/setup.md @@ -38,6 +38,8 @@ automatic pass. Detected local runtimes are auto-tested after CLI and API-key candidates; when several local models are available, OpenClaw prefers the strongest tool-calling instruct family. The selected candidate must answer a real completion before its provider and model configuration is saved. +Installed Gemini, Antigravity, Pi, and OpenCode CLIs are also reported when +they cannot serve as the reusable inference route for guided setup. `setup` accepts the same onboarding flags as `openclaw onboard`, including auth (`--auth-choice`, `--token`, provider key flags), Gateway diff --git a/docs/start/onboarding.md b/docs/start/onboarding.md index 35b1eb79a921..0fbfe5f26673 100644 --- a/docs/start/onboarding.md +++ b/docs/start/onboarding.md @@ -85,9 +85,11 @@ To use a Claude subscription when the Gateway host has no Claude CLI login, run printed token as **Anthropic setup-token** under **Connect with an API key or token**. -Gemini CLI and Antigravity remain available for normal use after setup. Their -installed CLIs are shown for context but are not auto-tested because neither can -enforce the tool-free inference probe. +Installed Gemini CLI, Antigravity, Pi, and OpenCode CLIs are shown for context +when they cannot be selected as the reusable guided-setup inference route. +Gemini and Antigravity cannot enforce the tool-free inference probe. Pi and +OpenCode are whole-agent harnesses rather than setup inference routes; their +session integrations require separate runtime and plugin setup. You can also sign in through the provider's own OAuth or device-pairing flow. The built-in choices include OpenAI/ChatGPT, OpenRouter, GitHub Copilot, Google diff --git a/docs/start/wizard.md b/docs/start/wizard.md index c68deba67d5a..b68ee53f69ab 100644 --- a/docs/start/wizard.md +++ b/docs/start/wizard.md @@ -76,8 +76,10 @@ Plain `openclaw onboard` follows this path: 2. Detect configured models, API-key environment variables, supported local AI CLIs, and already installed tool-capable models from reachable Ollama or LM Studio servers on the Gateway host. This read-only pass never downloads a - model. Gemini CLI and Antigravity installs are reported but not auto-tested - because they cannot enforce a tool-free probe. + model. Gemini CLI, Antigravity, Pi, and OpenCode installs are also reported + when they cannot serve as the reusable inference route for guided setup. + Gemini and Antigravity cannot enforce the tool-free probe; Pi and OpenCode + are whole-agent harnesses rather than setup inference routes. 3. Test the first detected candidate with a real completion. On failure, show the reason and continue to the next usable candidate. 4. If detection is exhausted, choose OpenAI, Anthropic, xAI (Grok), Google, or diff --git a/src/system-agent/setup-inference.test.ts b/src/system-agent/setup-inference.test.ts index b3734a680c3f..99d8b5d3d744 100644 --- a/src/system-agent/setup-inference.test.ts +++ b/src/system-agent/setup-inference.test.ts @@ -406,7 +406,10 @@ describe("detectSetupInference", () => { credentials: false, }, ], - probeLocalCommand: vi.fn(async (command) => ({ command, found: command === "agy" })), + probeLocalCommand: vi.fn(async (command) => ({ + command, + found: command === "agy" || command === "pi" || command === "opencode", + })), resolveManifestProviderAuthChoices: () => [ { pluginId: "local-plugin", @@ -435,6 +438,8 @@ describe("detectSetupInference", () => { expect(detection.unavailableCandidates).toEqual([ expect.objectContaining({ id: "gemini-cli" }), expect.objectContaining({ id: "antigravity-cli" }), + expect.objectContaining({ id: "pi-cli" }), + expect.objectContaining({ id: "opencode-cli" }), ]); expect(detect).toHaveBeenCalledOnce(); expect(prepare).not.toHaveBeenCalled(); @@ -677,6 +682,39 @@ describe("detectSetupInference", () => { expect.objectContaining({ id: "gemini-cli" }), ]); }); + + it("reports installed Pi and OpenCode without offering them as setup inference routes", async () => { + vi.mocked(detectInferenceBackends).mockResolvedValueOnce([]); + const probeLocalCommand = vi.fn(async (command: string) => ({ + command, + found: command === "pi" || command === "opencode", + })); + + const detection = await detectSetupInference({ + resolveManifestProviderAuthChoices: () => [], + probeLocalCommand, + }); + + expect(detection.candidates).toEqual([]); + expect(detection.unavailableCandidates).toEqual([ + { + id: "pi-cli", + label: "Pi CLI", + detail: "installed", + reason: + "Pi CLI is installed, but its whole-agent sessions require separate setup and are not a reusable guided-setup inference route.", + }, + { + id: "opencode-cli", + label: "OpenCode CLI", + detail: "installed", + reason: + "OpenCode CLI is installed, but its ACP harness requires separate setup and is not a reusable guided-setup inference route.", + }, + ]); + expect(probeLocalCommand).toHaveBeenCalledWith("pi"); + expect(probeLocalCommand).toHaveBeenCalledWith("opencode"); + }); }); async function runCodexSetupWithFinalConfig(params: { diff --git a/src/system-agent/setup-inference.ts b/src/system-agent/setup-inference.ts index 38457aa1b4d8..87a8424fb897 100644 --- a/src/system-agent/setup-inference.ts +++ b/src/system-agent/setup-inference.ts @@ -365,7 +365,12 @@ export async function detectSetupInference( reason: "Can't be auto-tested safely here. Use 'Gemini CLI OAuth' or a Gemini API key instead.", })); - const antigravity = await (deps.probeLocalCommand ?? probeLocalCommand)("agy"); + const probe = deps.probeLocalCommand ?? probeLocalCommand; + const [antigravity, pi, opencode] = await Promise.all([ + probe("agy"), + probe("pi"), + probe("opencode"), + ]); if (antigravity.found) { unavailableCandidates.push({ id: "antigravity-cli", @@ -375,6 +380,24 @@ export async function detectSetupInference( "Can't be auto-tested safely here. Sign in with a provider or use an API key instead.", }); } + if (pi.found) { + unavailableCandidates.push({ + id: "pi-cli", + label: "Pi CLI", + detail: "installed", + reason: + "Pi CLI is installed, but its whole-agent sessions require separate setup and are not a reusable guided-setup inference route.", + }); + } + if (opencode.found) { + unavailableCandidates.push({ + id: "opencode-cli", + label: "OpenCode CLI", + detail: "installed", + reason: + "OpenCode CLI is installed, but its ACP harness requires separate setup and is not a reusable guided-setup inference route.", + }); + } const raw = detected.filter((candidate) => candidate.kind !== "gemini-cli"); const candidates: SetupInferenceCandidate[] = raw.map((candidate) => // Released macOS clients require this field. Keep it false so the wire