diff --git a/src/agents/auth-health.ts b/src/agents/auth-health.ts index 96e79dc66..15bf3a07f 100644 --- a/src/agents/auth-health.ts +++ b/src/agents/auth-health.ts @@ -2,12 +2,10 @@ import type { ClawdbotConfig } from "../config/config.js"; import { type AuthProfileCredential, type AuthProfileStore, - CLAUDE_CLI_PROFILE_ID, - CODEX_CLI_PROFILE_ID, resolveAuthProfileDisplayLabel, } from "./auth-profiles.js"; -export type AuthProfileSource = "claude-cli" | "codex-cli" | "store"; +export type AuthProfileSource = "store"; export type AuthProfileHealthStatus = "ok" | "expiring" | "expired" | "missing" | "static"; @@ -41,9 +39,7 @@ export type AuthHealthSummary = { export const DEFAULT_OAUTH_WARN_MS = 24 * 60 * 60 * 1000; -export function resolveAuthProfileSource(profileId: string): AuthProfileSource { - if (profileId === CLAUDE_CLI_PROFILE_ID) return "claude-cli"; - if (profileId === CODEX_CLI_PROFILE_ID) return "codex-cli"; +export function resolveAuthProfileSource(_profileId: string): AuthProfileSource { return "store"; } diff --git a/src/agents/auth-profiles/external-cli-sync.ts b/src/agents/auth-profiles/external-cli-sync.ts index 8a7d8270f..d1fa31f23 100644 --- a/src/agents/auth-profiles/external-cli-sync.ts +++ b/src/agents/auth-profiles/external-cli-sync.ts @@ -1,22 +1,11 @@ +import { readQwenCliCredentialsCached } from "../cli-credentials.js"; import { - readClaudeCliCredentialsCached, - readCodexCliCredentialsCached, - readQwenCliCredentialsCached, -} from "../cli-credentials.js"; -import { - CLAUDE_CLI_PROFILE_ID, - CODEX_CLI_PROFILE_ID, EXTERNAL_CLI_NEAR_EXPIRY_MS, EXTERNAL_CLI_SYNC_TTL_MS, QWEN_CLI_PROFILE_ID, log, } from "./constants.js"; -import type { - AuthProfileCredential, - AuthProfileStore, - OAuthCredential, - TokenCredential, -} from "./types.js"; +import type { AuthProfileCredential, AuthProfileStore, OAuthCredential } from "./types.js"; function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCredential): boolean { if (!a) return false; @@ -33,25 +22,10 @@ function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCr ); } -function shallowEqualTokenCredentials(a: TokenCredential | undefined, b: TokenCredential): boolean { - if (!a) return false; - if (a.type !== "token") return false; - return ( - a.provider === b.provider && - a.token === b.token && - a.expires === b.expires && - a.email === b.email - ); -} - function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: number): boolean { if (!cred) return false; if (cred.type !== "oauth" && cred.type !== "token") return false; - if ( - cred.provider !== "anthropic" && - cred.provider !== "openai-codex" && - cred.provider !== "qwen-portal" - ) { + if (cred.provider !== "qwen-portal") { return false; } if (typeof cred.expires !== "number") return true; @@ -59,163 +33,14 @@ function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: nu } /** - * Find any existing openai-codex profile (other than codex-cli) that has the same - * access and refresh tokens. This prevents creating a duplicate codex-cli profile - * when the user has already set up a custom profile with the same credentials. - */ -export function findDuplicateCodexProfile( - store: AuthProfileStore, - creds: OAuthCredential, -): string | undefined { - for (const [profileId, profile] of Object.entries(store.profiles)) { - if (profileId === CODEX_CLI_PROFILE_ID) continue; - if (profile.type !== "oauth") continue; - if (profile.provider !== "openai-codex") continue; - if (profile.access === creds.access && profile.refresh === creds.refresh) { - return profileId; - } - } - return undefined; -} - -/** - * Sync OAuth credentials from external CLI tools (Claude Code CLI, Codex CLI) into the store. - * This allows clawdbot to use the same credentials as these tools without requiring - * separate authentication, and keeps credentials in sync when CLI tools refresh tokens. + * Sync OAuth credentials from external CLI tools (Qwen Code CLI) into the store. * * Returns true if any credentials were updated. */ -export function syncExternalCliCredentials( - store: AuthProfileStore, - options?: { allowKeychainPrompt?: boolean }, -): boolean { +export function syncExternalCliCredentials(store: AuthProfileStore): boolean { let mutated = false; const now = Date.now(); - // Sync from Claude Code CLI (supports both OAuth and Token credentials) - const existingClaude = store.profiles[CLAUDE_CLI_PROFILE_ID]; - const shouldSyncClaude = - !existingClaude || - existingClaude.provider !== "anthropic" || - existingClaude.type === "token" || - !isExternalProfileFresh(existingClaude, now); - const claudeCreds = shouldSyncClaude - ? readClaudeCliCredentialsCached({ - allowKeychainPrompt: options?.allowKeychainPrompt, - ttlMs: EXTERNAL_CLI_SYNC_TTL_MS, - }) - : null; - if (claudeCreds) { - const existing = store.profiles[CLAUDE_CLI_PROFILE_ID]; - const claudeCredsExpires = claudeCreds.expires ?? 0; - - // Determine if we should update based on credential comparison - let shouldUpdate = false; - let isEqual = false; - - if (claudeCreds.type === "oauth") { - const existingOAuth = existing?.type === "oauth" ? existing : undefined; - isEqual = shallowEqualOAuthCredentials(existingOAuth, claudeCreds); - // Update if: no existing profile, type changed to oauth, expired, or CLI has newer token - shouldUpdate = - !existingOAuth || - existingOAuth.provider !== "anthropic" || - existingOAuth.expires <= now || - (claudeCredsExpires > now && claudeCredsExpires > existingOAuth.expires); - } else { - const existingToken = existing?.type === "token" ? existing : undefined; - isEqual = shallowEqualTokenCredentials(existingToken, claudeCreds); - // Update if: no existing profile, expired, or CLI has newer token - shouldUpdate = - !existingToken || - existingToken.provider !== "anthropic" || - (existingToken.expires ?? 0) <= now || - (claudeCredsExpires > now && claudeCredsExpires > (existingToken.expires ?? 0)); - } - - // Also update if credential type changed (token -> oauth upgrade) - if (existing && existing.type !== claudeCreds.type) { - // Prefer oauth over token (enables auto-refresh) - if (claudeCreds.type === "oauth") { - shouldUpdate = true; - isEqual = false; - } - } - - // Avoid downgrading from oauth to token-only credentials. - if (existing?.type === "oauth" && claudeCreds.type === "token") { - shouldUpdate = false; - } - - if (shouldUpdate && !isEqual) { - store.profiles[CLAUDE_CLI_PROFILE_ID] = claudeCreds; - mutated = true; - log.info("synced anthropic credentials from claude cli", { - profileId: CLAUDE_CLI_PROFILE_ID, - type: claudeCreds.type, - expires: - typeof claudeCreds.expires === "number" - ? new Date(claudeCreds.expires).toISOString() - : "unknown", - }); - } - } - - // Sync from Codex CLI - const existingCodex = store.profiles[CODEX_CLI_PROFILE_ID]; - const existingCodexOAuth = existingCodex?.type === "oauth" ? existingCodex : undefined; - const duplicateExistingId = existingCodexOAuth - ? findDuplicateCodexProfile(store, existingCodexOAuth) - : undefined; - if (duplicateExistingId) { - delete store.profiles[CODEX_CLI_PROFILE_ID]; - mutated = true; - log.info("removed codex-cli profile: credentials already exist in another profile", { - existingProfileId: duplicateExistingId, - removedProfileId: CODEX_CLI_PROFILE_ID, - }); - } - const shouldSyncCodex = - !existingCodex || - existingCodex.provider !== "openai-codex" || - !isExternalProfileFresh(existingCodex, now); - const codexCreds = - shouldSyncCodex || duplicateExistingId - ? readCodexCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }) - : null; - if (codexCreds) { - const duplicateProfileId = findDuplicateCodexProfile(store, codexCreds); - if (duplicateProfileId) { - if (store.profiles[CODEX_CLI_PROFILE_ID]) { - delete store.profiles[CODEX_CLI_PROFILE_ID]; - mutated = true; - log.info("removed codex-cli profile: credentials already exist in another profile", { - existingProfileId: duplicateProfileId, - removedProfileId: CODEX_CLI_PROFILE_ID, - }); - } - } else { - const existing = store.profiles[CODEX_CLI_PROFILE_ID]; - const existingOAuth = existing?.type === "oauth" ? existing : undefined; - - // Codex creds don't carry expiry; use file mtime heuristic for freshness. - const shouldUpdate = - !existingOAuth || - existingOAuth.provider !== "openai-codex" || - existingOAuth.expires <= now || - codexCreds.expires > existingOAuth.expires; - - if (shouldUpdate && !shallowEqualOAuthCredentials(existingOAuth, codexCreds)) { - store.profiles[CODEX_CLI_PROFILE_ID] = codexCreds; - mutated = true; - log.info("synced openai-codex credentials from codex cli", { - profileId: CODEX_CLI_PROFILE_ID, - expires: new Date(codexCreds.expires).toISOString(), - }); - } - } - } - // Sync from Qwen Code CLI const existingQwen = store.profiles[QWEN_CLI_PROFILE_ID]; const shouldSyncQwen = diff --git a/src/agents/auth-profiles/oauth.ts b/src/agents/auth-profiles/oauth.ts index 8c59a3044..4138cda94 100644 --- a/src/agents/auth-profiles/oauth.ts +++ b/src/agents/auth-profiles/oauth.ts @@ -4,8 +4,7 @@ import lockfile from "proper-lockfile"; import type { ClawdbotConfig } from "../../config/config.js"; import { refreshChutesTokens } from "../chutes-oauth.js"; import { refreshQwenPortalCredentials } from "../../providers/qwen-portal-oauth.js"; -import { writeClaudeCliCredentials } from "../cli-credentials.js"; -import { AUTH_STORE_LOCK_OPTIONS, CLAUDE_CLI_PROFILE_ID } from "./constants.js"; +import { AUTH_STORE_LOCK_OPTIONS } from "./constants.js"; import { formatAuthDoctorHint } from "./doctor.js"; import { ensureAuthStoreFile, resolveAuthStorePath } from "./paths.js"; import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js"; @@ -72,12 +71,6 @@ async function refreshOAuthTokenWithLock(params: { }; saveAuthProfileStore(store, params.agentDir); - // Sync refreshed credentials back to Claude Code CLI if this is the claude-cli profile - // This ensures Claude Code continues to work after ClawdBot refreshes the token - if (params.profileId === CLAUDE_CLI_PROFILE_ID && cred.provider === "anthropic") { - writeClaudeCliCredentials(result.newCredentials); - } - return result; } finally { if (release) { diff --git a/src/agents/auth-profiles/store.ts b/src/agents/auth-profiles/store.ts index 010f0e9b7..ae4a999b9 100644 --- a/src/agents/auth-profiles/store.ts +++ b/src/agents/auth-profiles/store.ts @@ -3,13 +3,8 @@ import type { OAuthCredentials } from "@mariozechner/pi-ai"; import lockfile from "proper-lockfile"; import { resolveOAuthPath } from "../../config/paths.js"; import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js"; -import { - AUTH_STORE_LOCK_OPTIONS, - AUTH_STORE_VERSION, - CODEX_CLI_PROFILE_ID, - log, -} from "./constants.js"; -import { findDuplicateCodexProfile, syncExternalCliCredentials } from "./external-cli-sync.js"; +import { AUTH_STORE_LOCK_OPTIONS, AUTH_STORE_VERSION, log } from "./constants.js"; +import { syncExternalCliCredentials } from "./external-cli-sync.js"; import { ensureAuthStoreFile, resolveAuthStorePath, resolveLegacyAuthStorePath } from "./paths.js"; import type { AuthProfileCredential, AuthProfileStore, ProfileUsageStats } from "./types.js"; @@ -229,14 +224,14 @@ export function loadAuthProfileStore(): AuthProfileStore { function loadAuthProfileStoreForAgent( agentDir?: string, - options?: { allowKeychainPrompt?: boolean }, + _options?: { allowKeychainPrompt?: boolean }, ): AuthProfileStore { const authPath = resolveAuthStorePath(agentDir); const raw = loadJsonFile(authPath); const asStore = coerceAuthStore(raw); if (asStore) { // Sync from external CLI tools on every load - const synced = syncExternalCliCredentials(asStore, options); + const synced = syncExternalCliCredentials(asStore); if (synced) { saveJsonFile(authPath, asStore); } @@ -297,7 +292,7 @@ function loadAuthProfileStoreForAgent( } const mergedOAuth = mergeOAuthFileIntoStore(store); - const syncedCli = syncExternalCliCredentials(store, options); + const syncedCli = syncExternalCliCredentials(store); const shouldWrite = legacy !== null || mergedOAuth || syncedCli; if (shouldWrite) { saveJsonFile(authPath, store); @@ -337,15 +332,6 @@ export function ensureAuthProfileStore( const mainStore = loadAuthProfileStoreForAgent(undefined, options); const merged = mergeAuthProfileStores(mainStore, store); - // Keep per-agent view clean even if the main store has codex-cli. - const codexProfile = merged.profiles[CODEX_CLI_PROFILE_ID]; - if (codexProfile?.type === "oauth") { - const duplicateId = findDuplicateCodexProfile(merged, codexProfile); - if (duplicateId) { - delete merged.profiles[CODEX_CLI_PROFILE_ID]; - } - } - return merged; } diff --git a/src/cli/models-cli.ts b/src/cli/models-cli.ts index 20a476f81..d914629e7 100644 --- a/src/cli/models-cli.ts +++ b/src/cli/models-cli.ts @@ -389,7 +389,7 @@ export function registerModelsCli(program: Command) { .description("Set per-agent auth order override (locks rotation to this list)") .requiredOption("--provider ", "Provider id (e.g. anthropic)") .option("--agent ", "Agent id (default: configured default agent)") - .argument("", "Auth profile ids (e.g. anthropic:claude-cli)") + .argument("", "Auth profile ids (e.g. anthropic:default)") .action(async (profileIds: string[], opts) => { await runModelsCommand(async () => { await modelsAuthOrderSetCommand( diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index a2d5d4a66..eac6a60df 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) { .option("--mode ", "Wizard mode: local|remote") .option( "--auth-choice ", - "Auth: setup-token|claude-cli|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|codex-cli|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", + "Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", ) .option( "--token-provider ", diff --git a/src/commands/agents.commands.add.ts b/src/commands/agents.commands.add.ts index c8a6a3e0a..53b8ba049 100644 --- a/src/commands/agents.commands.add.ts +++ b/src/commands/agents.commands.add.ts @@ -258,7 +258,6 @@ export async function agentsAddCommand( prompter, store: authStore, includeSkip: true, - includeClaudeCliIfMissing: true, }); const authResult = await applyAuthChoice({ diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index f13eef365..6b49ff17b 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -1,6 +1,4 @@ import type { AuthProfileStore } from "../agents/auth-profiles.js"; -import { CLAUDE_CLI_PROFILE_ID, CODEX_CLI_PROFILE_ID } from "../agents/auth-profiles.js"; -import { colorize, isRich, theme } from "../terminal/theme.js"; import type { AuthChoice } from "./onboard-types.js"; export type AuthChoiceOption = { @@ -41,13 +39,13 @@ const AUTH_CHOICE_GROUP_DEFS: { value: "openai", label: "OpenAI", hint: "Codex OAuth + API key", - choices: ["codex-cli", "openai-codex", "openai-api-key"], + choices: ["openai-codex", "openai-api-key"], }, { value: "anthropic", label: "Anthropic", - hint: "Claude Code CLI + API key", - choices: ["token", "claude-cli", "apiKey"], + hint: "setup-token + API key", + choices: ["token", "apiKey"], }, { value: "minimax", @@ -117,65 +115,12 @@ const AUTH_CHOICE_GROUP_DEFS: { }, ]; -function formatOAuthHint(expires?: number, opts?: { allowStale?: boolean }): string { - const rich = isRich(); - if (!expires) { - return colorize(rich, theme.muted, "token unavailable"); - } - const now = Date.now(); - const remaining = expires - now; - if (remaining <= 0) { - if (opts?.allowStale) { - return colorize(rich, theme.warn, "token present · refresh on use"); - } - return colorize(rich, theme.error, "token expired"); - } - const minutes = Math.round(remaining / (60 * 1000)); - const duration = - minutes >= 120 - ? `${Math.round(minutes / 60)}h` - : minutes >= 60 - ? "1h" - : `${Math.max(minutes, 1)}m`; - const label = `token ok · expires in ${duration}`; - if (minutes <= 10) { - return colorize(rich, theme.warn, label); - } - return colorize(rich, theme.success, label); -} - export function buildAuthChoiceOptions(params: { store: AuthProfileStore; includeSkip: boolean; - includeClaudeCliIfMissing?: boolean; - platform?: NodeJS.Platform; }): AuthChoiceOption[] { + void params.store; const options: AuthChoiceOption[] = []; - const platform = params.platform ?? process.platform; - - const codexCli = params.store.profiles[CODEX_CLI_PROFILE_ID]; - if (codexCli?.type === "oauth") { - options.push({ - value: "codex-cli", - label: "OpenAI Codex OAuth (Codex CLI)", - hint: formatOAuthHint(codexCli.expires, { allowStale: true }), - }); - } - - const claudeCli = params.store.profiles[CLAUDE_CLI_PROFILE_ID]; - if (claudeCli?.type === "oauth" || claudeCli?.type === "token") { - options.push({ - value: "claude-cli", - label: "Anthropic token (Claude Code CLI)", - hint: `reuses existing Claude Code auth · ${formatOAuthHint(claudeCli.expires)}`, - }); - } else if (params.includeClaudeCliIfMissing && platform === "darwin") { - options.push({ - value: "claude-cli", - label: "Anthropic token (Claude Code CLI)", - hint: "reuses existing Claude Code auth · requires Keychain access", - }); - } options.push({ value: "token", @@ -245,12 +190,7 @@ export function buildAuthChoiceOptions(params: { return options; } -export function buildAuthChoiceGroups(params: { - store: AuthProfileStore; - includeSkip: boolean; - includeClaudeCliIfMissing?: boolean; - platform?: NodeJS.Platform; -}): { +export function buildAuthChoiceGroups(params: { store: AuthProfileStore; includeSkip: boolean }): { groups: AuthChoiceGroup[]; skipOption?: AuthChoiceOption; } { diff --git a/src/commands/auth-choice-prompt.ts b/src/commands/auth-choice-prompt.ts index 82756229e..275fa72c9 100644 --- a/src/commands/auth-choice-prompt.ts +++ b/src/commands/auth-choice-prompt.ts @@ -9,8 +9,6 @@ export async function promptAuthChoiceGrouped(params: { prompter: WizardPrompter; store: AuthProfileStore; includeSkip: boolean; - includeClaudeCliIfMissing?: boolean; - platform?: NodeJS.Platform; }): Promise { const { groups, skipOption } = buildAuthChoiceGroups(params); const availableGroups = groups.filter((group) => group.options.length > 0); diff --git a/src/commands/auth-choice.apply.anthropic.ts b/src/commands/auth-choice.apply.anthropic.ts index c5700663c..b28b8ebee 100644 --- a/src/commands/auth-choice.apply.anthropic.ts +++ b/src/commands/auth-choice.apply.anthropic.ts @@ -1,8 +1,4 @@ -import { - CLAUDE_CLI_PROFILE_ID, - ensureAuthProfileStore, - upsertAuthProfile, -} from "../agents/auth-profiles.js"; +import { upsertAuthProfile } from "../agents/auth-profiles.js"; import { formatApiKeyPreview, normalizeApiKeyInput, @@ -15,153 +11,17 @@ import { applyAuthProfileConfig, setAnthropicApiKey } from "./onboard-auth.js"; export async function applyAuthChoiceAnthropic( params: ApplyAuthChoiceParams, ): Promise { - if (params.authChoice === "claude-cli") { + if ( + params.authChoice === "setup-token" || + params.authChoice === "oauth" || + params.authChoice === "token" + ) { let nextConfig = params.config; - const store = ensureAuthProfileStore(params.agentDir, { - allowKeychainPrompt: false, - }); - const hasClaudeCli = Boolean(store.profiles[CLAUDE_CLI_PROFILE_ID]); - if (!hasClaudeCli && process.platform === "darwin") { - await params.prompter.note( - [ - "macOS will show a Keychain prompt next.", - 'Choose "Always Allow" so the launchd gateway can start without prompts.', - 'If you choose "Allow" or "Deny", each restart will block on a Keychain alert.', - ].join("\n"), - "Claude Code CLI Keychain", - ); - const proceed = await params.prompter.confirm({ - message: "Check Keychain for Claude Code CLI credentials now?", - initialValue: true, - }); - if (!proceed) return { config: nextConfig }; - } - - const storeWithKeychain = hasClaudeCli - ? store - : ensureAuthProfileStore(params.agentDir, { - allowKeychainPrompt: true, - }); - - if (!storeWithKeychain.profiles[CLAUDE_CLI_PROFILE_ID]) { - if (process.stdin.isTTY) { - const runNow = await params.prompter.confirm({ - message: "Run `claude setup-token` now?", - initialValue: true, - }); - if (runNow) { - const res = await (async () => { - const { spawnSync } = await import("node:child_process"); - return spawnSync("claude", ["setup-token"], { stdio: "inherit" }); - })(); - if (res.error) { - await params.prompter.note( - `Failed to run claude: ${String(res.error)}`, - "Claude setup-token", - ); - } - } - } else { - await params.prompter.note( - "`claude setup-token` requires an interactive TTY.", - "Claude setup-token", - ); - } - - const refreshed = ensureAuthProfileStore(params.agentDir, { - allowKeychainPrompt: true, - }); - if (!refreshed.profiles[CLAUDE_CLI_PROFILE_ID]) { - await params.prompter.note( - process.platform === "darwin" - ? 'No Claude Code CLI credentials found in Keychain ("Claude Code-credentials") or ~/.claude/.credentials.json.' - : "No Claude Code CLI credentials found at ~/.claude/.credentials.json.", - "Claude Code CLI OAuth", - ); - return { config: nextConfig }; - } - } - nextConfig = applyAuthProfileConfig(nextConfig, { - profileId: CLAUDE_CLI_PROFILE_ID, - provider: "anthropic", - mode: "oauth", - }); - return { config: nextConfig }; - } - - if (params.authChoice === "setup-token" || params.authChoice === "oauth") { - let nextConfig = params.config; - await params.prompter.note( - [ - "This will run `claude setup-token` to create a long-lived Anthropic token.", - "Requires an interactive TTY and a Claude Pro/Max subscription.", - ].join("\n"), - "Anthropic setup-token", - ); - - if (!process.stdin.isTTY) { - await params.prompter.note( - "`claude setup-token` requires an interactive TTY.", - "Anthropic setup-token", - ); - return { config: nextConfig }; - } - - const proceed = await params.prompter.confirm({ - message: "Run `claude setup-token` now?", - initialValue: true, - }); - if (!proceed) return { config: nextConfig }; - - const res = await (async () => { - const { spawnSync } = await import("node:child_process"); - return spawnSync("claude", ["setup-token"], { stdio: "inherit" }); - })(); - if (res.error) { - await params.prompter.note( - `Failed to run claude: ${String(res.error)}`, - "Anthropic setup-token", - ); - return { config: nextConfig }; - } - if (typeof res.status === "number" && res.status !== 0) { - await params.prompter.note( - `claude setup-token failed (exit ${res.status})`, - "Anthropic setup-token", - ); - return { config: nextConfig }; - } - - const store = ensureAuthProfileStore(params.agentDir, { - allowKeychainPrompt: true, - }); - if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) { - await params.prompter.note( - `No Claude Code CLI credentials found after setup-token. Expected ${CLAUDE_CLI_PROFILE_ID}.`, - "Anthropic setup-token", - ); - return { config: nextConfig }; - } - - nextConfig = applyAuthProfileConfig(nextConfig, { - profileId: CLAUDE_CLI_PROFILE_ID, - provider: "anthropic", - mode: "oauth", - }); - return { config: nextConfig }; - } - - if (params.authChoice === "token") { - let nextConfig = params.config; - const provider = (await params.prompter.select({ - message: "Token provider", - options: [{ value: "anthropic", label: "Anthropic (only supported)" }], - })) as "anthropic"; await params.prompter.note( ["Run `claude setup-token` in your terminal.", "Then paste the generated token below."].join( "\n", ), - "Anthropic token", + "Anthropic setup-token", ); const tokenRaw = await params.prompter.text({ @@ -174,6 +34,7 @@ export async function applyAuthChoiceAnthropic( message: "Token name (blank = default)", placeholder: "default", }); + const provider = "anthropic"; const namedProfileId = buildTokenProfileId({ provider, name: String(profileNameRaw ?? ""), diff --git a/src/commands/auth-choice.apply.openai.ts b/src/commands/auth-choice.apply.openai.ts index 7d96a35a1..947b81181 100644 --- a/src/commands/auth-choice.apply.openai.ts +++ b/src/commands/auth-choice.apply.openai.ts @@ -1,5 +1,4 @@ import { loginOpenAICodex } from "@mariozechner/pi-ai"; -import { CODEX_CLI_PROFILE_ID, ensureAuthProfileStore } from "../agents/auth-profiles.js"; import { resolveEnvApiKey } from "../agents/model-auth.js"; import { upsertSharedEnvVar } from "../infra/env-file.js"; import { isRemoteEnvironment } from "./oauth-env.js"; @@ -146,45 +145,5 @@ export async function applyAuthChoiceOpenAI( return { config: nextConfig, agentModelOverride }; } - if (params.authChoice === "codex-cli") { - let nextConfig = params.config; - let agentModelOverride: string | undefined; - const noteAgentModel = async (model: string) => { - if (!params.agentId) return; - await params.prompter.note( - `Default model set to ${model} for agent "${params.agentId}".`, - "Model configured", - ); - }; - - const store = ensureAuthProfileStore(params.agentDir); - if (!store.profiles[CODEX_CLI_PROFILE_ID]) { - await params.prompter.note( - "No Codex CLI credentials found at ~/.codex/auth.json.", - "Codex CLI OAuth", - ); - return { config: nextConfig, agentModelOverride }; - } - nextConfig = applyAuthProfileConfig(nextConfig, { - profileId: CODEX_CLI_PROFILE_ID, - provider: "openai-codex", - mode: "oauth", - }); - if (params.setDefaultModel) { - const applied = applyOpenAICodexModelDefault(nextConfig); - nextConfig = applied.next; - if (applied.changed) { - await params.prompter.note( - `Default model set to ${OPENAI_CODEX_DEFAULT_MODEL}`, - "Model configured", - ); - } - } else { - agentModelOverride = OPENAI_CODEX_DEFAULT_MODEL; - await noteAgentModel(OPENAI_CODEX_DEFAULT_MODEL); - } - return { config: nextConfig, agentModelOverride }; - } - return null; } diff --git a/src/commands/channels/list.ts b/src/commands/channels/list.ts index 93571312f..bd707e4e0 100644 --- a/src/commands/channels/list.ts +++ b/src/commands/channels/list.ts @@ -1,8 +1,4 @@ -import { - CLAUDE_CLI_PROFILE_ID, - CODEX_CLI_PROFILE_ID, - loadAuthProfileStore, -} from "../../agents/auth-profiles.js"; +import { loadAuthProfileStore } from "../../agents/auth-profiles.js"; import { listChannelPlugins } from "../../channels/plugins/index.js"; import { buildChannelAccountSnapshot } from "../../channels/plugins/status.js"; import type { ChannelAccountSnapshot, ChannelPlugin } from "../../channels/plugins/types.js"; @@ -115,7 +111,7 @@ export async function channelsListCommand( id: profileId, provider: profile.provider, type: profile.type, - isExternal: profileId === CLAUDE_CLI_PROFILE_ID || profileId === CODEX_CLI_PROFILE_ID, + isExternal: false, })); if (opts.json) { const usage = includeUsage ? await loadProviderUsageSummary() : undefined; diff --git a/src/commands/configure.gateway-auth.ts b/src/commands/configure.gateway-auth.ts index 6d3522ab4..d60453a98 100644 --- a/src/commands/configure.gateway-auth.ts +++ b/src/commands/configure.gateway-auth.ts @@ -47,7 +47,6 @@ export async function promptAuthConfig( allowKeychainPrompt: false, }), includeSkip: true, - includeClaudeCliIfMissing: true, }); let next = cfg; @@ -74,10 +73,7 @@ export async function promptAuthConfig( } const anthropicOAuth = - authChoice === "claude-cli" || - authChoice === "setup-token" || - authChoice === "token" || - authChoice === "oauth"; + authChoice === "setup-token" || authChoice === "token" || authChoice === "oauth"; const allowlistSelection = await promptModelAllowlist({ config: next, diff --git a/src/commands/doctor-auth.ts b/src/commands/doctor-auth.ts index 7fc17e28f..4ef6f7a0e 100644 --- a/src/commands/doctor-auth.ts +++ b/src/commands/doctor-auth.ts @@ -11,6 +11,7 @@ import { resolveApiKeyForProfile, resolveProfileUnusableUntilForDisplay, } from "../agents/auth-profiles.js"; +import { updateAuthProfileStoreWithLock } from "../agents/auth-profiles/store.js"; import type { ClawdbotConfig } from "../config/config.js"; import { note } from "../terminal/note.js"; import { formatCliCommand } from "../cli/command-format.js"; @@ -38,6 +39,148 @@ export async function maybeRepairAnthropicOAuthProfileId( return repair.config; } +function pruneAuthOrder( + order: Record | undefined, + profileIds: Set, +): { next: Record | undefined; changed: boolean } { + if (!order) return { next: order, changed: false }; + let changed = false; + const next: Record = {}; + for (const [provider, list] of Object.entries(order)) { + const filtered = list.filter((id) => !profileIds.has(id)); + if (filtered.length !== list.length) changed = true; + if (filtered.length > 0) next[provider] = filtered; + } + return { next: Object.keys(next).length > 0 ? next : undefined, changed }; +} + +function pruneAuthProfiles( + cfg: ClawdbotConfig, + profileIds: Set, +): { next: ClawdbotConfig; changed: boolean } { + const profiles = cfg.auth?.profiles; + const order = cfg.auth?.order; + const nextProfiles = profiles ? { ...profiles } : undefined; + let changed = false; + + if (nextProfiles) { + for (const id of profileIds) { + if (id in nextProfiles) { + delete nextProfiles[id]; + changed = true; + } + } + } + + const prunedOrder = pruneAuthOrder(order, profileIds); + if (prunedOrder.changed) changed = true; + + if (!changed) return { next: cfg, changed: false }; + + const nextAuth = + nextProfiles || prunedOrder.next + ? { + ...cfg.auth, + profiles: nextProfiles && Object.keys(nextProfiles).length > 0 ? nextProfiles : undefined, + order: prunedOrder.next, + } + : undefined; + + return { + next: { + ...cfg, + auth: nextAuth, + }, + changed: true, + }; +} + +export async function maybeRemoveDeprecatedCliAuthProfiles( + cfg: ClawdbotConfig, + prompter: DoctorPrompter, +): Promise { + const store = ensureAuthProfileStore(undefined, { allowKeychainPrompt: false }); + const deprecated = new Set(); + if (store.profiles[CLAUDE_CLI_PROFILE_ID] || cfg.auth?.profiles?.[CLAUDE_CLI_PROFILE_ID]) { + deprecated.add(CLAUDE_CLI_PROFILE_ID); + } + if (store.profiles[CODEX_CLI_PROFILE_ID] || cfg.auth?.profiles?.[CODEX_CLI_PROFILE_ID]) { + deprecated.add(CODEX_CLI_PROFILE_ID); + } + + if (deprecated.size === 0) return cfg; + + const lines = ["Deprecated external CLI auth profiles detected (no longer supported):"]; + if (deprecated.has(CLAUDE_CLI_PROFILE_ID)) { + lines.push( + `- ${CLAUDE_CLI_PROFILE_ID} (Anthropic): use setup-token → ${formatCliCommand("clawdbot models auth setup-token")}`, + ); + } + if (deprecated.has(CODEX_CLI_PROFILE_ID)) { + lines.push( + `- ${CODEX_CLI_PROFILE_ID} (OpenAI Codex): use OAuth → ${formatCliCommand( + "clawdbot models auth login --provider openai-codex", + )}`, + ); + } + note(lines.join("\n"), "Auth profiles"); + + const shouldRemove = await prompter.confirmRepair({ + message: "Remove deprecated CLI auth profiles now?", + initialValue: true, + }); + if (!shouldRemove) return cfg; + + await updateAuthProfileStoreWithLock({ + updater: (nextStore) => { + let mutated = false; + for (const id of deprecated) { + if (nextStore.profiles[id]) { + delete nextStore.profiles[id]; + mutated = true; + } + if (nextStore.usageStats?.[id]) { + delete nextStore.usageStats[id]; + mutated = true; + } + } + if (nextStore.order) { + for (const [provider, list] of Object.entries(nextStore.order)) { + const filtered = list.filter((id) => !deprecated.has(id)); + if (filtered.length !== list.length) { + mutated = true; + if (filtered.length > 0) { + nextStore.order[provider] = filtered; + } else { + delete nextStore.order[provider]; + } + } + } + } + if (nextStore.lastGood) { + for (const [provider, profileId] of Object.entries(nextStore.lastGood)) { + if (deprecated.has(profileId)) { + delete nextStore.lastGood[provider]; + mutated = true; + } + } + } + return mutated; + }, + }); + + const pruned = pruneAuthProfiles(cfg, deprecated); + if (pruned.changed) { + note( + Array.from(deprecated.values()) + .map((id) => `- removed ${id} from config`) + .join("\n"), + "Doctor changes", + ); + } + return pruned.next; +} + type AuthIssue = { profileId: string; provider: string; @@ -47,10 +190,14 @@ type AuthIssue = { function formatAuthIssueHint(issue: AuthIssue): string | null { if (issue.provider === "anthropic" && issue.profileId === CLAUDE_CLI_PROFILE_ID) { - return "Run `claude setup-token` on the gateway host."; + return `Deprecated profile. Use ${formatCliCommand("clawdbot models auth setup-token")} or ${formatCliCommand( + "clawdbot configure", + )}.`; } if (issue.provider === "openai-codex" && issue.profileId === CODEX_CLI_PROFILE_ID) { - return `Run \`codex login\` (or \`${formatCliCommand("clawdbot configure")}\` → OpenAI Codex OAuth).`; + return `Deprecated profile. Use ${formatCliCommand( + "clawdbot models auth login --provider openai-codex", + )} or ${formatCliCommand("clawdbot configure")}.`; } return `Re-auth via \`${formatCliCommand("clawdbot configure")}\` or \`${formatCliCommand("clawdbot onboard")}\`.`; } diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index aa4f4d7a3..658504ecc 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -22,7 +22,11 @@ import { defaultRuntime } from "../runtime.js"; import { note } from "../terminal/note.js"; import { stylePromptTitle } from "../terminal/prompt-style.js"; import { shortenHomePath } from "../utils.js"; -import { maybeRepairAnthropicOAuthProfileId, noteAuthProfileHealth } from "./doctor-auth.js"; +import { + maybeRemoveDeprecatedCliAuthProfiles, + maybeRepairAnthropicOAuthProfileId, + noteAuthProfileHealth, +} from "./doctor-auth.js"; import { loadAndMaybeMigrateDoctorConfig } from "./doctor-config-flow.js"; import { maybeRepairGatewayDaemon } from "./doctor-gateway-daemon-flow.js"; import { checkGatewayHealth } from "./doctor-gateway-health.js"; @@ -104,6 +108,7 @@ export async function doctorCommand( } cfg = await maybeRepairAnthropicOAuthProfileId(cfg, prompter); + cfg = await maybeRemoveDeprecatedCliAuthProfiles(cfg, prompter); await noteAuthProfileHealth({ cfg, prompter, diff --git a/src/commands/models/auth.ts b/src/commands/models/auth.ts index b2da0cde1..c38cf4520 100644 --- a/src/commands/models/auth.ts +++ b/src/commands/models/auth.ts @@ -1,12 +1,6 @@ -import { spawnSync } from "node:child_process"; - import { confirm as clackConfirm, select as clackSelect, text as clackText } from "@clack/prompts"; -import { - CLAUDE_CLI_PROFILE_ID, - ensureAuthProfileStore, - upsertAuthProfile, -} from "../../agents/auth-profiles.js"; +import { upsertAuthProfile } from "../../agents/auth-profiles.js"; import { normalizeProviderId } from "../../agents/model-selection.js"; import { resolveAgentDir, @@ -33,6 +27,7 @@ import type { ProviderPlugin, } from "../../plugins/types.js"; import type { AuthProfileCredential } from "../../agents/auth-profiles/types.js"; +import { validateAnthropicSetupToken } from "../auth-token.js"; const confirm = (params: Parameters[0]) => clackConfirm({ @@ -73,9 +68,7 @@ export async function modelsAuthSetupTokenCommand( ) { const provider = resolveTokenProvider(opts.provider ?? "anthropic"); if (provider !== "anthropic") { - throw new Error( - "Only --provider anthropic is supported for setup-token (uses `claude setup-token`).", - ); + throw new Error("Only --provider anthropic is supported for setup-token."); } if (!process.stdin.isTTY) { @@ -84,38 +77,38 @@ export async function modelsAuthSetupTokenCommand( if (!opts.yes) { const proceed = await confirm({ - message: "Run `claude setup-token` now?", + message: "Have you run `claude setup-token` and copied the token?", initialValue: true, }); if (!proceed) return; } - const res = spawnSync("claude", ["setup-token"], { stdio: "inherit" }); - if (res.error) throw res.error; - if (typeof res.status === "number" && res.status !== 0) { - throw new Error(`claude setup-token failed (exit ${res.status})`); - } - - const store = ensureAuthProfileStore(undefined, { - allowKeychainPrompt: true, + const tokenInput = await text({ + message: "Paste Anthropic setup-token", + validate: (value) => validateAnthropicSetupToken(String(value ?? "")), + }); + const token = String(tokenInput).trim(); + const profileId = resolveDefaultTokenProfileId(provider); + + upsertAuthProfile({ + profileId, + credential: { + type: "token", + provider, + token, + }, }); - const synced = store.profiles[CLAUDE_CLI_PROFILE_ID]; - if (!synced) { - throw new Error( - `No Claude Code CLI credentials found after setup-token. Expected auth profile ${CLAUDE_CLI_PROFILE_ID}.`, - ); - } await updateConfig((cfg) => applyAuthProfileConfig(cfg, { - profileId: CLAUDE_CLI_PROFILE_ID, - provider: "anthropic", - mode: "oauth", + profileId, + provider, + mode: "token", }), ); logConfigUpdated(runtime); - runtime.log(`Auth profile: ${CLAUDE_CLI_PROFILE_ID} (anthropic/oauth)`); + runtime.log(`Auth profile: ${profileId} (${provider}/token)`); } export async function modelsAuthPasteTokenCommand( @@ -189,7 +182,7 @@ export async function modelsAuthAddCommand(_opts: Record, runtime { value: "setup-token", label: "setup-token (claude)", - hint: "Runs `claude setup-token` (recommended)", + hint: "Paste a setup-token from `claude setup-token`", }, ] : []), diff --git a/src/commands/models/list.status-command.ts b/src/commands/models/list.status-command.ts index 8aa7015c8..fc29cc5d5 100644 --- a/src/commands/models/list.status-command.ts +++ b/src/commands/models/list.status-command.ts @@ -487,7 +487,7 @@ export async function modelsStatusCommand( for (const provider of missingProvidersInUse) { const hint = provider === "anthropic" - ? `Run \`claude setup-token\` or \`${formatCliCommand("clawdbot configure")}\`.` + ? `Run \`claude setup-token\`, then \`${formatCliCommand("clawdbot models auth setup-token")}\` or \`${formatCliCommand("clawdbot configure")}\`.` : `Run \`${formatCliCommand("clawdbot configure")}\` or set an API key env var.`; runtime.log(`- ${theme.heading(provider)} ${hint}`); } @@ -558,9 +558,7 @@ export async function modelsStatusCommand( : profile.expiresAt ? ` expires in ${formatRemainingShort(profile.remainingMs)}` : " expires unknown"; - const source = - profile.source !== "store" ? colorize(rich, theme.muted, ` (${profile.source})`) : ""; - runtime.log(` - ${label} ${status}${expiry}${source}`); + runtime.log(` - ${label} ${status}${expiry}`); } } } diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 02e0a75b9..c5558596a 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -1,9 +1,4 @@ -import { - CLAUDE_CLI_PROFILE_ID, - CODEX_CLI_PROFILE_ID, - ensureAuthProfileStore, - upsertAuthProfile, -} from "../../../agents/auth-profiles.js"; +import { upsertAuthProfile } from "../../../agents/auth-profiles.js"; import { normalizeProviderId } from "../../../agents/model-selection.js"; import { parseDurationMs } from "../../../cli/parse-duration.js"; import type { ClawdbotConfig } from "../../../config/config.js"; @@ -36,7 +31,6 @@ import { setZaiApiKey, } from "../../onboard-auth.js"; import type { AuthChoice, OnboardOptions } from "../../onboard-types.js"; -import { applyOpenAICodexModelDefault } from "../../openai-codex-model-default.js"; import { resolveNonInteractiveApiKey } from "../api-keys.js"; import { shortenHomePath } from "../../../utils.js"; @@ -50,6 +44,28 @@ export async function applyNonInteractiveAuthChoice(params: { const { authChoice, opts, runtime, baseConfig } = params; let nextConfig = params.nextConfig; + if (authChoice === "claude-cli" || authChoice === "codex-cli") { + runtime.error( + [ + `Auth choice "${authChoice}" is deprecated.`, + 'Use "--auth-choice token" (Anthropic setup-token) or "--auth-choice openai-codex".', + ].join("\n"), + ); + runtime.exit(1); + return null; + } + + if (authChoice === "setup-token") { + runtime.error( + [ + 'Auth choice "setup-token" requires interactive mode.', + 'Use "--auth-choice token" with --token and --token-provider anthropic.', + ].join("\n"), + ); + runtime.exit(1); + return null; + } + if (authChoice === "apiKey") { const resolved = await resolveNonInteractiveApiKey({ provider: "anthropic", @@ -318,41 +334,6 @@ export async function applyNonInteractiveAuthChoice(params: { return applyMinimaxApiConfig(nextConfig, modelId); } - if (authChoice === "claude-cli") { - const store = ensureAuthProfileStore(undefined, { - allowKeychainPrompt: false, - }); - if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) { - runtime.error( - process.platform === "darwin" - ? 'No Claude Code CLI credentials found. Run interactive onboarding to approve Keychain access for "Claude Code-credentials".' - : "No Claude Code CLI credentials found at ~/.claude/.credentials.json", - ); - runtime.exit(1); - return null; - } - return applyAuthProfileConfig(nextConfig, { - profileId: CLAUDE_CLI_PROFILE_ID, - provider: "anthropic", - mode: "oauth", - }); - } - - if (authChoice === "codex-cli") { - const store = ensureAuthProfileStore(); - if (!store.profiles[CODEX_CLI_PROFILE_ID]) { - runtime.error("No Codex CLI credentials found at ~/.codex/auth.json"); - runtime.exit(1); - return null; - } - nextConfig = applyAuthProfileConfig(nextConfig, { - profileId: CODEX_CLI_PROFILE_ID, - provider: "openai-codex", - mode: "oauth", - }); - return applyOpenAICodexModelDefault(nextConfig).next; - } - if (authChoice === "minimax") return applyMinimaxConfig(nextConfig); if (authChoice === "opencode-zen") { diff --git a/src/commands/onboard.ts b/src/commands/onboard.ts index d8618a871..348aca613 100644 --- a/src/commands/onboard.ts +++ b/src/commands/onboard.ts @@ -12,9 +12,33 @@ import type { OnboardOptions } from "./onboard-types.js"; export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv = defaultRuntime) { assertSupportedRuntime(runtime); const authChoice = opts.authChoice === "oauth" ? ("setup-token" as const) : opts.authChoice; + const normalizedAuthChoice = + authChoice === "claude-cli" + ? ("setup-token" as const) + : authChoice === "codex-cli" + ? ("openai-codex" as const) + : authChoice; + if (opts.nonInteractive && (authChoice === "claude-cli" || authChoice === "codex-cli")) { + runtime.error( + [ + `Auth choice "${authChoice}" is deprecated.`, + 'Use "--auth-choice token" (Anthropic setup-token) or "--auth-choice openai-codex".', + ].join("\n"), + ); + runtime.exit(1); + return; + } + if (authChoice === "claude-cli") { + runtime.log('Auth choice "claude-cli" is deprecated; using setup-token flow instead.'); + } + if (authChoice === "codex-cli") { + runtime.log('Auth choice "codex-cli" is deprecated; using OpenAI Codex OAuth instead.'); + } const flow = opts.flow === "manual" ? ("advanced" as const) : opts.flow; const normalizedOpts = - authChoice === opts.authChoice && flow === opts.flow ? opts : { ...opts, authChoice, flow }; + normalizedAuthChoice === opts.authChoice && flow === opts.flow + ? opts + : { ...opts, authChoice: normalizedAuthChoice, flow }; if (normalizedOpts.nonInteractive && normalizedOpts.acceptRisk !== true) { runtime.error( diff --git a/src/infra/provider-usage.auth.ts b/src/infra/provider-usage.auth.ts index 43e4c10c9..90d73bb59 100644 --- a/src/infra/provider-usage.auth.ts +++ b/src/infra/provider-usage.auth.ts @@ -3,7 +3,6 @@ import os from "node:os"; import path from "node:path"; import { - CLAUDE_CLI_PROFILE_ID, ensureAuthProfileStore, listProfilesForProvider, resolveApiKeyForProfile, @@ -111,9 +110,7 @@ async function resolveOAuthToken(params: { provider: params.provider, }); - // Claude Code CLI creds are the only Anthropic tokens that reliably include the - // `user:profile` scope required for the OAuth usage endpoint. - const candidates = params.provider === "anthropic" ? [CLAUDE_CLI_PROFILE_ID, ...order] : order; + const candidates = order; const deduped: string[] = []; for (const entry of candidates) { if (!deduped.includes(entry)) deduped.push(entry); diff --git a/src/wizard/onboarding.ts b/src/wizard/onboarding.ts index 77b7f770d..39d17befa 100644 --- a/src/wizard/onboarding.ts +++ b/src/wizard/onboarding.ts @@ -360,7 +360,6 @@ export async function runOnboardingWizard( prompter, store: authStore, includeSkip: true, - includeClaudeCliIfMissing: true, })); const authResult = await applyAuthChoice({