diff --git a/extensions/anthropic/cli-migration.test.ts b/extensions/anthropic/cli-migration.test.ts index 2d33d4fd2f2..3876b0960a0 100644 --- a/extensions/anthropic/cli-migration.test.ts +++ b/extensions/anthropic/cli-migration.test.ts @@ -170,13 +170,14 @@ describe("anthropic cli migration", () => { }); it("registered cli auth returns the same migration result as the builder", async () => { - readClaudeCliCredentialsForSetup.mockReturnValue({ + const credential = { type: "oauth", provider: "anthropic", access: "access-token", refresh: "refresh-token", expires: Date.now() + 60_000, - }); + } as const; + readClaudeCliCredentialsForSetup.mockReturnValue(credential); const method = await resolveAnthropicCliAuthMethod(); const config = { agents: { @@ -195,10 +196,60 @@ describe("anthropic cli migration", () => { }; await expect(method.run(createProviderAuthContext(config))).resolves.toEqual( - buildAnthropicCliMigrationResult(config), + buildAnthropicCliMigrationResult(config, credential), ); }); + it("stores a claude-cli oauth profile when Claude CLI credentials are available", () => { + const result = buildAnthropicCliMigrationResult( + {}, + { + type: "oauth", + provider: "anthropic", + access: "access-token", + refresh: "refresh-token", + expires: 123, + }, + ); + + expect(result.profiles).toEqual([ + { + profileId: "anthropic:claude-cli", + credential: { + type: "oauth", + provider: "claude-cli", + access: "access-token", + refresh: "refresh-token", + expires: 123, + }, + }, + ]); + }); + + it("stores a claude-cli token profile when Claude CLI only exposes a bearer token", () => { + const result = buildAnthropicCliMigrationResult( + {}, + { + type: "token", + provider: "anthropic", + token: "bearer-token", + expires: 123, + }, + ); + + expect(result.profiles).toEqual([ + { + profileId: "anthropic:claude-cli", + credential: { + type: "token", + provider: "claude-cli", + token: "bearer-token", + expires: 123, + }, + }, + ]); + }); + it("registered non-interactive cli auth rewrites anthropic fallbacks before setting the claude-cli default", async () => { readClaudeCliCredentialsForSetupNonInteractive.mockReturnValue({ type: "oauth", diff --git a/extensions/anthropic/cli-migration.ts b/extensions/anthropic/cli-migration.ts index 64f017d2fb5..d346c303b65 100644 --- a/extensions/anthropic/cli-migration.ts +++ b/extensions/anthropic/cli-migration.ts @@ -1,12 +1,18 @@ -import type { OpenClawConfig, ProviderAuthResult } from "openclaw/plugin-sdk/provider-auth"; +import { + CLAUDE_CLI_PROFILE_ID, + type OpenClawConfig, + type ProviderAuthResult, +} from "openclaw/plugin-sdk/provider-auth"; import { readClaudeCliCredentialsForSetup, readClaudeCliCredentialsForSetupNonInteractive, } from "./cli-auth-seam.js"; +import { CLAUDE_CLI_BACKEND_ID } from "./cli-shared.js"; -const DEFAULT_CLAUDE_CLI_MODEL = "claude-cli/claude-sonnet-4-6"; +const DEFAULT_CLAUDE_CLI_MODEL = `${CLAUDE_CLI_BACKEND_ID}/claude-sonnet-4-6`; type AgentDefaultsModel = NonNullable["defaults"]>["model"]; type AgentDefaultsModels = NonNullable["defaults"]>["models"]; +type ClaudeCliCredential = NonNullable>; function toClaudeCliModelRef(raw: string): string | null { const trimmed = raw.trim(); @@ -104,7 +110,43 @@ export function hasClaudeCliAuth(options?: { allowKeychainPrompt?: boolean }): b ); } -export function buildAnthropicCliMigrationResult(config: OpenClawConfig): ProviderAuthResult { +function buildClaudeCliAuthProfiles( + credential?: ClaudeCliCredential | null, +): ProviderAuthResult["profiles"] { + if (!credential) { + return []; + } + if (credential.type === "oauth") { + return [ + { + profileId: CLAUDE_CLI_PROFILE_ID, + credential: { + type: "oauth", + provider: CLAUDE_CLI_BACKEND_ID, + access: credential.access, + refresh: credential.refresh, + expires: credential.expires, + }, + }, + ]; + } + return [ + { + profileId: CLAUDE_CLI_PROFILE_ID, + credential: { + type: "token", + provider: CLAUDE_CLI_BACKEND_ID, + token: credential.token, + expires: credential.expires, + }, + }, + ]; +} + +export function buildAnthropicCliMigrationResult( + config: OpenClawConfig, + credential?: ClaudeCliCredential | null, +): ProviderAuthResult { const defaults = config.agents?.defaults; const rewrittenModel = rewriteModelSelection(defaults?.model); const rewrittenModels = rewriteModelEntryMap(defaults?.models); @@ -114,7 +156,7 @@ export function buildAnthropicCliMigrationResult(config: OpenClawConfig): Provid const defaultModel = rewrittenModel.primary ?? DEFAULT_CLAUDE_CLI_MODEL; return { - profiles: [], + profiles: buildClaudeCliAuthProfiles(credential), configPatch: { agents: { defaults: { diff --git a/extensions/anthropic/index.test.ts b/extensions/anthropic/index.test.ts index 77013dde29b..84fd288807c 100644 --- a/extensions/anthropic/index.test.ts +++ b/extensions/anthropic/index.test.ts @@ -164,6 +164,17 @@ describe("anthropic provider replay hooks", () => { config: {}, } as never); - expect(result?.profiles).toEqual([]); + expect(result?.profiles).toEqual([ + { + profileId: "anthropic:claude-cli", + credential: { + type: "oauth", + provider: "claude-cli", + access: "setup-access-token", + refresh: "refresh-token", + expires: 123, + }, + }, + ]); }); }); diff --git a/extensions/anthropic/register.runtime.ts b/extensions/anthropic/register.runtime.ts index c759cb534e8..6bd7bf493f1 100644 --- a/extensions/anthropic/register.runtime.ts +++ b/extensions/anthropic/register.runtime.ts @@ -23,9 +23,9 @@ import { } from "openclaw/plugin-sdk/provider-auth"; import { cloneFirstTemplateModel } from "openclaw/plugin-sdk/provider-model-shared"; import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage"; -import { readClaudeCliCredentialsForRuntime } from "./cli-auth-seam.js"; +import * as claudeCliAuth from "./cli-auth-seam.js"; import { buildAnthropicCliBackend } from "./cli-backend.js"; -import { buildAnthropicCliMigrationResult, hasClaudeCliAuth } from "./cli-migration.js"; +import { buildAnthropicCliMigrationResult } from "./cli-migration.js"; import { CLAUDE_CLI_BACKEND_ID } from "./cli-shared.js"; import { applyAnthropicConfigDefaults, @@ -285,7 +285,7 @@ function buildAnthropicAuthDoctorHint(params: { } function resolveClaudeCliSyntheticAuth() { - const credential = readClaudeCliCredentialsForRuntime(); + const credential = claudeCliAuth.readClaudeCliCredentialsForRuntime(); if (!credential) { return undefined; } @@ -303,7 +303,8 @@ function resolveClaudeCliSyntheticAuth() { } async function runAnthropicCliMigration(ctx: ProviderAuthContext): Promise { - if (!hasClaudeCliAuth()) { + const credential = claudeCliAuth.readClaudeCliCredentialsForSetup(); + if (!credential) { throw new Error( [ "Claude CLI is not authenticated on this host.", @@ -311,7 +312,7 @@ async function runAnthropicCliMigration(ctx: ProviderAuthContext): Promise { - if (!hasClaudeCliAuth({ allowKeychainPrompt: false })) { + const credential = claudeCliAuth.readClaudeCliCredentialsForSetupNonInteractive(); + if (!credential) { ctx.runtime.error( [ 'Auth choice "anthropic-cli" requires Claude CLI auth on this host.', @@ -330,7 +332,7 @@ async function runAnthropicCliMigrationNonInteractive(ctx: { return null; } - const result = buildAnthropicCliMigrationResult(ctx.config); + const result = buildAnthropicCliMigrationResult(ctx.config, credential); const currentDefaults = ctx.config.agents?.defaults; const currentModel = currentDefaults?.model; const currentFallbacks =