refactor(auth)!: remove external CLI OAuth reuse

This commit is contained in:
Peter Steinberger
2026-01-26 19:04:29 +00:00
parent 39d219da59
commit 526303d9a2
21 changed files with 260 additions and 567 deletions

View File

@@ -2,12 +2,10 @@ import type { ClawdbotConfig } from "../config/config.js";
import { import {
type AuthProfileCredential, type AuthProfileCredential,
type AuthProfileStore, type AuthProfileStore,
CLAUDE_CLI_PROFILE_ID,
CODEX_CLI_PROFILE_ID,
resolveAuthProfileDisplayLabel, resolveAuthProfileDisplayLabel,
} from "./auth-profiles.js"; } from "./auth-profiles.js";
export type AuthProfileSource = "claude-cli" | "codex-cli" | "store"; export type AuthProfileSource = "store";
export type AuthProfileHealthStatus = "ok" | "expiring" | "expired" | "missing" | "static"; 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 const DEFAULT_OAUTH_WARN_MS = 24 * 60 * 60 * 1000;
export function resolveAuthProfileSource(profileId: string): AuthProfileSource { export function resolveAuthProfileSource(_profileId: string): AuthProfileSource {
if (profileId === CLAUDE_CLI_PROFILE_ID) return "claude-cli";
if (profileId === CODEX_CLI_PROFILE_ID) return "codex-cli";
return "store"; return "store";
} }

View File

@@ -1,22 +1,11 @@
import { readQwenCliCredentialsCached } from "../cli-credentials.js";
import { import {
readClaudeCliCredentialsCached,
readCodexCliCredentialsCached,
readQwenCliCredentialsCached,
} from "../cli-credentials.js";
import {
CLAUDE_CLI_PROFILE_ID,
CODEX_CLI_PROFILE_ID,
EXTERNAL_CLI_NEAR_EXPIRY_MS, EXTERNAL_CLI_NEAR_EXPIRY_MS,
EXTERNAL_CLI_SYNC_TTL_MS, EXTERNAL_CLI_SYNC_TTL_MS,
QWEN_CLI_PROFILE_ID, QWEN_CLI_PROFILE_ID,
log, log,
} from "./constants.js"; } from "./constants.js";
import type { import type { AuthProfileCredential, AuthProfileStore, OAuthCredential } from "./types.js";
AuthProfileCredential,
AuthProfileStore,
OAuthCredential,
TokenCredential,
} from "./types.js";
function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCredential): boolean { function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCredential): boolean {
if (!a) return false; 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 { function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: number): boolean {
if (!cred) return false; if (!cred) return false;
if (cred.type !== "oauth" && cred.type !== "token") return false; if (cred.type !== "oauth" && cred.type !== "token") return false;
if ( if (cred.provider !== "qwen-portal") {
cred.provider !== "anthropic" &&
cred.provider !== "openai-codex" &&
cred.provider !== "qwen-portal"
) {
return false; return false;
} }
if (typeof cred.expires !== "number") return true; 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 * Sync OAuth credentials from external CLI tools (Qwen Code CLI) into the store.
* 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.
* *
* Returns true if any credentials were updated. * Returns true if any credentials were updated.
*/ */
export function syncExternalCliCredentials( export function syncExternalCliCredentials(store: AuthProfileStore): boolean {
store: AuthProfileStore,
options?: { allowKeychainPrompt?: boolean },
): boolean {
let mutated = false; let mutated = false;
const now = Date.now(); 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 // Sync from Qwen Code CLI
const existingQwen = store.profiles[QWEN_CLI_PROFILE_ID]; const existingQwen = store.profiles[QWEN_CLI_PROFILE_ID];
const shouldSyncQwen = const shouldSyncQwen =

View File

@@ -4,8 +4,7 @@ import lockfile from "proper-lockfile";
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
import { refreshChutesTokens } from "../chutes-oauth.js"; import { refreshChutesTokens } from "../chutes-oauth.js";
import { refreshQwenPortalCredentials } from "../../providers/qwen-portal-oauth.js"; import { refreshQwenPortalCredentials } from "../../providers/qwen-portal-oauth.js";
import { writeClaudeCliCredentials } from "../cli-credentials.js"; import { AUTH_STORE_LOCK_OPTIONS } from "./constants.js";
import { AUTH_STORE_LOCK_OPTIONS, CLAUDE_CLI_PROFILE_ID } from "./constants.js";
import { formatAuthDoctorHint } from "./doctor.js"; import { formatAuthDoctorHint } from "./doctor.js";
import { ensureAuthStoreFile, resolveAuthStorePath } from "./paths.js"; import { ensureAuthStoreFile, resolveAuthStorePath } from "./paths.js";
import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js"; import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js";
@@ -72,12 +71,6 @@ async function refreshOAuthTokenWithLock(params: {
}; };
saveAuthProfileStore(store, params.agentDir); 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; return result;
} finally { } finally {
if (release) { if (release) {

View File

@@ -3,13 +3,8 @@ import type { OAuthCredentials } from "@mariozechner/pi-ai";
import lockfile from "proper-lockfile"; import lockfile from "proper-lockfile";
import { resolveOAuthPath } from "../../config/paths.js"; import { resolveOAuthPath } from "../../config/paths.js";
import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js"; import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js";
import { import { AUTH_STORE_LOCK_OPTIONS, AUTH_STORE_VERSION, log } from "./constants.js";
AUTH_STORE_LOCK_OPTIONS, import { syncExternalCliCredentials } from "./external-cli-sync.js";
AUTH_STORE_VERSION,
CODEX_CLI_PROFILE_ID,
log,
} from "./constants.js";
import { findDuplicateCodexProfile, syncExternalCliCredentials } from "./external-cli-sync.js";
import { ensureAuthStoreFile, resolveAuthStorePath, resolveLegacyAuthStorePath } from "./paths.js"; import { ensureAuthStoreFile, resolveAuthStorePath, resolveLegacyAuthStorePath } from "./paths.js";
import type { AuthProfileCredential, AuthProfileStore, ProfileUsageStats } from "./types.js"; import type { AuthProfileCredential, AuthProfileStore, ProfileUsageStats } from "./types.js";
@@ -229,14 +224,14 @@ export function loadAuthProfileStore(): AuthProfileStore {
function loadAuthProfileStoreForAgent( function loadAuthProfileStoreForAgent(
agentDir?: string, agentDir?: string,
options?: { allowKeychainPrompt?: boolean }, _options?: { allowKeychainPrompt?: boolean },
): AuthProfileStore { ): AuthProfileStore {
const authPath = resolveAuthStorePath(agentDir); const authPath = resolveAuthStorePath(agentDir);
const raw = loadJsonFile(authPath); const raw = loadJsonFile(authPath);
const asStore = coerceAuthStore(raw); const asStore = coerceAuthStore(raw);
if (asStore) { if (asStore) {
// Sync from external CLI tools on every load // Sync from external CLI tools on every load
const synced = syncExternalCliCredentials(asStore, options); const synced = syncExternalCliCredentials(asStore);
if (synced) { if (synced) {
saveJsonFile(authPath, asStore); saveJsonFile(authPath, asStore);
} }
@@ -297,7 +292,7 @@ function loadAuthProfileStoreForAgent(
} }
const mergedOAuth = mergeOAuthFileIntoStore(store); const mergedOAuth = mergeOAuthFileIntoStore(store);
const syncedCli = syncExternalCliCredentials(store, options); const syncedCli = syncExternalCliCredentials(store);
const shouldWrite = legacy !== null || mergedOAuth || syncedCli; const shouldWrite = legacy !== null || mergedOAuth || syncedCli;
if (shouldWrite) { if (shouldWrite) {
saveJsonFile(authPath, store); saveJsonFile(authPath, store);
@@ -337,15 +332,6 @@ export function ensureAuthProfileStore(
const mainStore = loadAuthProfileStoreForAgent(undefined, options); const mainStore = loadAuthProfileStoreForAgent(undefined, options);
const merged = mergeAuthProfileStores(mainStore, store); 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; return merged;
} }

View File

@@ -389,7 +389,7 @@ export function registerModelsCli(program: Command) {
.description("Set per-agent auth order override (locks rotation to this list)") .description("Set per-agent auth order override (locks rotation to this list)")
.requiredOption("--provider <name>", "Provider id (e.g. anthropic)") .requiredOption("--provider <name>", "Provider id (e.g. anthropic)")
.option("--agent <id>", "Agent id (default: configured default agent)") .option("--agent <id>", "Agent id (default: configured default agent)")
.argument("<profileIds...>", "Auth profile ids (e.g. anthropic:claude-cli)") .argument("<profileIds...>", "Auth profile ids (e.g. anthropic:default)")
.action(async (profileIds: string[], opts) => { .action(async (profileIds: string[], opts) => {
await runModelsCommand(async () => { await runModelsCommand(async () => {
await modelsAuthOrderSetCommand( await modelsAuthOrderSetCommand(

View File

@@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) {
.option("--mode <mode>", "Wizard mode: local|remote") .option("--mode <mode>", "Wizard mode: local|remote")
.option( .option(
"--auth-choice <choice>", "--auth-choice <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( .option(
"--token-provider <id>", "--token-provider <id>",

View File

@@ -258,7 +258,6 @@ export async function agentsAddCommand(
prompter, prompter,
store: authStore, store: authStore,
includeSkip: true, includeSkip: true,
includeClaudeCliIfMissing: true,
}); });
const authResult = await applyAuthChoice({ const authResult = await applyAuthChoice({

View File

@@ -1,6 +1,4 @@
import type { AuthProfileStore } from "../agents/auth-profiles.js"; 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"; import type { AuthChoice } from "./onboard-types.js";
export type AuthChoiceOption = { export type AuthChoiceOption = {
@@ -41,13 +39,13 @@ const AUTH_CHOICE_GROUP_DEFS: {
value: "openai", value: "openai",
label: "OpenAI", label: "OpenAI",
hint: "Codex OAuth + API key", hint: "Codex OAuth + API key",
choices: ["codex-cli", "openai-codex", "openai-api-key"], choices: ["openai-codex", "openai-api-key"],
}, },
{ {
value: "anthropic", value: "anthropic",
label: "Anthropic", label: "Anthropic",
hint: "Claude Code CLI + API key", hint: "setup-token + API key",
choices: ["token", "claude-cli", "apiKey"], choices: ["token", "apiKey"],
}, },
{ {
value: "minimax", 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: { export function buildAuthChoiceOptions(params: {
store: AuthProfileStore; store: AuthProfileStore;
includeSkip: boolean; includeSkip: boolean;
includeClaudeCliIfMissing?: boolean;
platform?: NodeJS.Platform;
}): AuthChoiceOption[] { }): AuthChoiceOption[] {
void params.store;
const options: AuthChoiceOption[] = []; 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({ options.push({
value: "token", value: "token",
@@ -245,12 +190,7 @@ export function buildAuthChoiceOptions(params: {
return options; return options;
} }
export function buildAuthChoiceGroups(params: { export function buildAuthChoiceGroups(params: { store: AuthProfileStore; includeSkip: boolean }): {
store: AuthProfileStore;
includeSkip: boolean;
includeClaudeCliIfMissing?: boolean;
platform?: NodeJS.Platform;
}): {
groups: AuthChoiceGroup[]; groups: AuthChoiceGroup[];
skipOption?: AuthChoiceOption; skipOption?: AuthChoiceOption;
} { } {

View File

@@ -9,8 +9,6 @@ export async function promptAuthChoiceGrouped(params: {
prompter: WizardPrompter; prompter: WizardPrompter;
store: AuthProfileStore; store: AuthProfileStore;
includeSkip: boolean; includeSkip: boolean;
includeClaudeCliIfMissing?: boolean;
platform?: NodeJS.Platform;
}): Promise<AuthChoice> { }): Promise<AuthChoice> {
const { groups, skipOption } = buildAuthChoiceGroups(params); const { groups, skipOption } = buildAuthChoiceGroups(params);
const availableGroups = groups.filter((group) => group.options.length > 0); const availableGroups = groups.filter((group) => group.options.length > 0);

View File

@@ -1,8 +1,4 @@
import { import { upsertAuthProfile } from "../agents/auth-profiles.js";
CLAUDE_CLI_PROFILE_ID,
ensureAuthProfileStore,
upsertAuthProfile,
} from "../agents/auth-profiles.js";
import { import {
formatApiKeyPreview, formatApiKeyPreview,
normalizeApiKeyInput, normalizeApiKeyInput,
@@ -15,153 +11,17 @@ import { applyAuthProfileConfig, setAnthropicApiKey } from "./onboard-auth.js";
export async function applyAuthChoiceAnthropic( export async function applyAuthChoiceAnthropic(
params: ApplyAuthChoiceParams, params: ApplyAuthChoiceParams,
): Promise<ApplyAuthChoiceResult | null> { ): Promise<ApplyAuthChoiceResult | null> {
if (params.authChoice === "claude-cli") { if (
params.authChoice === "setup-token" ||
params.authChoice === "oauth" ||
params.authChoice === "token"
) {
let nextConfig = params.config; 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( await params.prompter.note(
["Run `claude setup-token` in your terminal.", "Then paste the generated token below."].join( ["Run `claude setup-token` in your terminal.", "Then paste the generated token below."].join(
"\n", "\n",
), ),
"Anthropic token", "Anthropic setup-token",
); );
const tokenRaw = await params.prompter.text({ const tokenRaw = await params.prompter.text({
@@ -174,6 +34,7 @@ export async function applyAuthChoiceAnthropic(
message: "Token name (blank = default)", message: "Token name (blank = default)",
placeholder: "default", placeholder: "default",
}); });
const provider = "anthropic";
const namedProfileId = buildTokenProfileId({ const namedProfileId = buildTokenProfileId({
provider, provider,
name: String(profileNameRaw ?? ""), name: String(profileNameRaw ?? ""),

View File

@@ -1,5 +1,4 @@
import { loginOpenAICodex } from "@mariozechner/pi-ai"; 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 { resolveEnvApiKey } from "../agents/model-auth.js";
import { upsertSharedEnvVar } from "../infra/env-file.js"; import { upsertSharedEnvVar } from "../infra/env-file.js";
import { isRemoteEnvironment } from "./oauth-env.js"; import { isRemoteEnvironment } from "./oauth-env.js";
@@ -146,45 +145,5 @@ export async function applyAuthChoiceOpenAI(
return { config: nextConfig, agentModelOverride }; 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; return null;
} }

View File

@@ -1,8 +1,4 @@
import { import { loadAuthProfileStore } from "../../agents/auth-profiles.js";
CLAUDE_CLI_PROFILE_ID,
CODEX_CLI_PROFILE_ID,
loadAuthProfileStore,
} from "../../agents/auth-profiles.js";
import { listChannelPlugins } from "../../channels/plugins/index.js"; import { listChannelPlugins } from "../../channels/plugins/index.js";
import { buildChannelAccountSnapshot } from "../../channels/plugins/status.js"; import { buildChannelAccountSnapshot } from "../../channels/plugins/status.js";
import type { ChannelAccountSnapshot, ChannelPlugin } from "../../channels/plugins/types.js"; import type { ChannelAccountSnapshot, ChannelPlugin } from "../../channels/plugins/types.js";
@@ -115,7 +111,7 @@ export async function channelsListCommand(
id: profileId, id: profileId,
provider: profile.provider, provider: profile.provider,
type: profile.type, type: profile.type,
isExternal: profileId === CLAUDE_CLI_PROFILE_ID || profileId === CODEX_CLI_PROFILE_ID, isExternal: false,
})); }));
if (opts.json) { if (opts.json) {
const usage = includeUsage ? await loadProviderUsageSummary() : undefined; const usage = includeUsage ? await loadProviderUsageSummary() : undefined;

View File

@@ -47,7 +47,6 @@ export async function promptAuthConfig(
allowKeychainPrompt: false, allowKeychainPrompt: false,
}), }),
includeSkip: true, includeSkip: true,
includeClaudeCliIfMissing: true,
}); });
let next = cfg; let next = cfg;
@@ -74,10 +73,7 @@ export async function promptAuthConfig(
} }
const anthropicOAuth = const anthropicOAuth =
authChoice === "claude-cli" || authChoice === "setup-token" || authChoice === "token" || authChoice === "oauth";
authChoice === "setup-token" ||
authChoice === "token" ||
authChoice === "oauth";
const allowlistSelection = await promptModelAllowlist({ const allowlistSelection = await promptModelAllowlist({
config: next, config: next,

View File

@@ -11,6 +11,7 @@ import {
resolveApiKeyForProfile, resolveApiKeyForProfile,
resolveProfileUnusableUntilForDisplay, resolveProfileUnusableUntilForDisplay,
} from "../agents/auth-profiles.js"; } from "../agents/auth-profiles.js";
import { updateAuthProfileStoreWithLock } from "../agents/auth-profiles/store.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { note } from "../terminal/note.js"; import { note } from "../terminal/note.js";
import { formatCliCommand } from "../cli/command-format.js"; import { formatCliCommand } from "../cli/command-format.js";
@@ -38,6 +39,148 @@ export async function maybeRepairAnthropicOAuthProfileId(
return repair.config; return repair.config;
} }
function pruneAuthOrder(
order: Record<string, string[]> | undefined,
profileIds: Set<string>,
): { next: Record<string, string[]> | undefined; changed: boolean } {
if (!order) return { next: order, changed: false };
let changed = false;
const next: Record<string, string[]> = {};
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<string>,
): { 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<ClawdbotConfig> {
const store = ensureAuthProfileStore(undefined, { allowKeychainPrompt: false });
const deprecated = new Set<string>();
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 = { type AuthIssue = {
profileId: string; profileId: string;
provider: string; provider: string;
@@ -47,10 +190,14 @@ type AuthIssue = {
function formatAuthIssueHint(issue: AuthIssue): string | null { function formatAuthIssueHint(issue: AuthIssue): string | null {
if (issue.provider === "anthropic" && issue.profileId === CLAUDE_CLI_PROFILE_ID) { 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) { 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")}\`.`; return `Re-auth via \`${formatCliCommand("clawdbot configure")}\` or \`${formatCliCommand("clawdbot onboard")}\`.`;
} }

View File

@@ -22,7 +22,11 @@ import { defaultRuntime } from "../runtime.js";
import { note } from "../terminal/note.js"; import { note } from "../terminal/note.js";
import { stylePromptTitle } from "../terminal/prompt-style.js"; import { stylePromptTitle } from "../terminal/prompt-style.js";
import { shortenHomePath } from "../utils.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 { loadAndMaybeMigrateDoctorConfig } from "./doctor-config-flow.js";
import { maybeRepairGatewayDaemon } from "./doctor-gateway-daemon-flow.js"; import { maybeRepairGatewayDaemon } from "./doctor-gateway-daemon-flow.js";
import { checkGatewayHealth } from "./doctor-gateway-health.js"; import { checkGatewayHealth } from "./doctor-gateway-health.js";
@@ -104,6 +108,7 @@ export async function doctorCommand(
} }
cfg = await maybeRepairAnthropicOAuthProfileId(cfg, prompter); cfg = await maybeRepairAnthropicOAuthProfileId(cfg, prompter);
cfg = await maybeRemoveDeprecatedCliAuthProfiles(cfg, prompter);
await noteAuthProfileHealth({ await noteAuthProfileHealth({
cfg, cfg,
prompter, prompter,

View File

@@ -1,12 +1,6 @@
import { spawnSync } from "node:child_process";
import { confirm as clackConfirm, select as clackSelect, text as clackText } from "@clack/prompts"; import { confirm as clackConfirm, select as clackSelect, text as clackText } from "@clack/prompts";
import { import { upsertAuthProfile } from "../../agents/auth-profiles.js";
CLAUDE_CLI_PROFILE_ID,
ensureAuthProfileStore,
upsertAuthProfile,
} from "../../agents/auth-profiles.js";
import { normalizeProviderId } from "../../agents/model-selection.js"; import { normalizeProviderId } from "../../agents/model-selection.js";
import { import {
resolveAgentDir, resolveAgentDir,
@@ -33,6 +27,7 @@ import type {
ProviderPlugin, ProviderPlugin,
} from "../../plugins/types.js"; } from "../../plugins/types.js";
import type { AuthProfileCredential } from "../../agents/auth-profiles/types.js"; import type { AuthProfileCredential } from "../../agents/auth-profiles/types.js";
import { validateAnthropicSetupToken } from "../auth-token.js";
const confirm = (params: Parameters<typeof clackConfirm>[0]) => const confirm = (params: Parameters<typeof clackConfirm>[0]) =>
clackConfirm({ clackConfirm({
@@ -73,9 +68,7 @@ export async function modelsAuthSetupTokenCommand(
) { ) {
const provider = resolveTokenProvider(opts.provider ?? "anthropic"); const provider = resolveTokenProvider(opts.provider ?? "anthropic");
if (provider !== "anthropic") { if (provider !== "anthropic") {
throw new Error( throw new Error("Only --provider anthropic is supported for setup-token.");
"Only --provider anthropic is supported for setup-token (uses `claude setup-token`).",
);
} }
if (!process.stdin.isTTY) { if (!process.stdin.isTTY) {
@@ -84,38 +77,38 @@ export async function modelsAuthSetupTokenCommand(
if (!opts.yes) { if (!opts.yes) {
const proceed = await confirm({ const proceed = await confirm({
message: "Run `claude setup-token` now?", message: "Have you run `claude setup-token` and copied the token?",
initialValue: true, initialValue: true,
}); });
if (!proceed) return; if (!proceed) return;
} }
const res = spawnSync("claude", ["setup-token"], { stdio: "inherit" }); const tokenInput = await text({
if (res.error) throw res.error; message: "Paste Anthropic setup-token",
if (typeof res.status === "number" && res.status !== 0) { validate: (value) => validateAnthropicSetupToken(String(value ?? "")),
throw new Error(`claude setup-token failed (exit ${res.status})`); });
} const token = String(tokenInput).trim();
const profileId = resolveDefaultTokenProfileId(provider);
const store = ensureAuthProfileStore(undefined, {
allowKeychainPrompt: true, 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) => await updateConfig((cfg) =>
applyAuthProfileConfig(cfg, { applyAuthProfileConfig(cfg, {
profileId: CLAUDE_CLI_PROFILE_ID, profileId,
provider: "anthropic", provider,
mode: "oauth", mode: "token",
}), }),
); );
logConfigUpdated(runtime); logConfigUpdated(runtime);
runtime.log(`Auth profile: ${CLAUDE_CLI_PROFILE_ID} (anthropic/oauth)`); runtime.log(`Auth profile: ${profileId} (${provider}/token)`);
} }
export async function modelsAuthPasteTokenCommand( export async function modelsAuthPasteTokenCommand(
@@ -189,7 +182,7 @@ export async function modelsAuthAddCommand(_opts: Record<string, never>, runtime
{ {
value: "setup-token", value: "setup-token",
label: "setup-token (claude)", label: "setup-token (claude)",
hint: "Runs `claude setup-token` (recommended)", hint: "Paste a setup-token from `claude setup-token`",
}, },
] ]
: []), : []),

View File

@@ -487,7 +487,7 @@ export async function modelsStatusCommand(
for (const provider of missingProvidersInUse) { for (const provider of missingProvidersInUse) {
const hint = const hint =
provider === "anthropic" 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.`; : `Run \`${formatCliCommand("clawdbot configure")}\` or set an API key env var.`;
runtime.log(`- ${theme.heading(provider)} ${hint}`); runtime.log(`- ${theme.heading(provider)} ${hint}`);
} }
@@ -558,9 +558,7 @@ export async function modelsStatusCommand(
: profile.expiresAt : profile.expiresAt
? ` expires in ${formatRemainingShort(profile.remainingMs)}` ? ` expires in ${formatRemainingShort(profile.remainingMs)}`
: " expires unknown"; : " expires unknown";
const source = runtime.log(` - ${label} ${status}${expiry}`);
profile.source !== "store" ? colorize(rich, theme.muted, ` (${profile.source})`) : "";
runtime.log(` - ${label} ${status}${expiry}${source}`);
} }
} }
} }

View File

@@ -1,9 +1,4 @@
import { import { upsertAuthProfile } from "../../../agents/auth-profiles.js";
CLAUDE_CLI_PROFILE_ID,
CODEX_CLI_PROFILE_ID,
ensureAuthProfileStore,
upsertAuthProfile,
} from "../../../agents/auth-profiles.js";
import { normalizeProviderId } from "../../../agents/model-selection.js"; import { normalizeProviderId } from "../../../agents/model-selection.js";
import { parseDurationMs } from "../../../cli/parse-duration.js"; import { parseDurationMs } from "../../../cli/parse-duration.js";
import type { ClawdbotConfig } from "../../../config/config.js"; import type { ClawdbotConfig } from "../../../config/config.js";
@@ -36,7 +31,6 @@ import {
setZaiApiKey, setZaiApiKey,
} from "../../onboard-auth.js"; } from "../../onboard-auth.js";
import type { AuthChoice, OnboardOptions } from "../../onboard-types.js"; import type { AuthChoice, OnboardOptions } from "../../onboard-types.js";
import { applyOpenAICodexModelDefault } from "../../openai-codex-model-default.js";
import { resolveNonInteractiveApiKey } from "../api-keys.js"; import { resolveNonInteractiveApiKey } from "../api-keys.js";
import { shortenHomePath } from "../../../utils.js"; import { shortenHomePath } from "../../../utils.js";
@@ -50,6 +44,28 @@ export async function applyNonInteractiveAuthChoice(params: {
const { authChoice, opts, runtime, baseConfig } = params; const { authChoice, opts, runtime, baseConfig } = params;
let nextConfig = params.nextConfig; 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") { if (authChoice === "apiKey") {
const resolved = await resolveNonInteractiveApiKey({ const resolved = await resolveNonInteractiveApiKey({
provider: "anthropic", provider: "anthropic",
@@ -318,41 +334,6 @@ export async function applyNonInteractiveAuthChoice(params: {
return applyMinimaxApiConfig(nextConfig, modelId); 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 === "minimax") return applyMinimaxConfig(nextConfig);
if (authChoice === "opencode-zen") { if (authChoice === "opencode-zen") {

View File

@@ -12,9 +12,33 @@ import type { OnboardOptions } from "./onboard-types.js";
export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv = defaultRuntime) { export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv = defaultRuntime) {
assertSupportedRuntime(runtime); assertSupportedRuntime(runtime);
const authChoice = opts.authChoice === "oauth" ? ("setup-token" as const) : opts.authChoice; 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 flow = opts.flow === "manual" ? ("advanced" as const) : opts.flow;
const normalizedOpts = 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) { if (normalizedOpts.nonInteractive && normalizedOpts.acceptRisk !== true) {
runtime.error( runtime.error(

View File

@@ -3,7 +3,6 @@ import os from "node:os";
import path from "node:path"; import path from "node:path";
import { import {
CLAUDE_CLI_PROFILE_ID,
ensureAuthProfileStore, ensureAuthProfileStore,
listProfilesForProvider, listProfilesForProvider,
resolveApiKeyForProfile, resolveApiKeyForProfile,
@@ -111,9 +110,7 @@ async function resolveOAuthToken(params: {
provider: params.provider, provider: params.provider,
}); });
// Claude Code CLI creds are the only Anthropic tokens that reliably include the const candidates = order;
// `user:profile` scope required for the OAuth usage endpoint.
const candidates = params.provider === "anthropic" ? [CLAUDE_CLI_PROFILE_ID, ...order] : order;
const deduped: string[] = []; const deduped: string[] = [];
for (const entry of candidates) { for (const entry of candidates) {
if (!deduped.includes(entry)) deduped.push(entry); if (!deduped.includes(entry)) deduped.push(entry);

View File

@@ -360,7 +360,6 @@ export async function runOnboardingWizard(
prompter, prompter,
store: authStore, store: authStore,
includeSkip: true, includeSkip: true,
includeClaudeCliIfMissing: true,
})); }));
const authResult = await applyAuthChoice({ const authResult = await applyAuthChoice({