refactor(auth)!: remove external CLI OAuth reuse
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -389,7 +389,7 @@ export function registerModelsCli(program: Command) {
|
||||
.description("Set per-agent auth order override (locks rotation to this list)")
|
||||
.requiredOption("--provider <name>", "Provider id (e.g. anthropic)")
|
||||
.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) => {
|
||||
await runModelsCommand(async () => {
|
||||
await modelsAuthOrderSetCommand(
|
||||
|
||||
@@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) {
|
||||
.option("--mode <mode>", "Wizard mode: local|remote")
|
||||
.option(
|
||||
"--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(
|
||||
"--token-provider <id>",
|
||||
|
||||
@@ -258,7 +258,6 @@ export async function agentsAddCommand(
|
||||
prompter,
|
||||
store: authStore,
|
||||
includeSkip: true,
|
||||
includeClaudeCliIfMissing: true,
|
||||
});
|
||||
|
||||
const authResult = await applyAuthChoice({
|
||||
|
||||
@@ -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;
|
||||
} {
|
||||
|
||||
@@ -9,8 +9,6 @@ export async function promptAuthChoiceGrouped(params: {
|
||||
prompter: WizardPrompter;
|
||||
store: AuthProfileStore;
|
||||
includeSkip: boolean;
|
||||
includeClaudeCliIfMissing?: boolean;
|
||||
platform?: NodeJS.Platform;
|
||||
}): Promise<AuthChoice> {
|
||||
const { groups, skipOption } = buildAuthChoiceGroups(params);
|
||||
const availableGroups = groups.filter((group) => group.options.length > 0);
|
||||
|
||||
@@ -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<ApplyAuthChoiceResult | null> {
|
||||
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 ?? ""),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<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 = {
|
||||
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")}\`.`;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<typeof clackConfirm>[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<string, never>, runtime
|
||||
{
|
||||
value: "setup-token",
|
||||
label: "setup-token (claude)",
|
||||
hint: "Runs `claude setup-token` (recommended)",
|
||||
hint: "Paste a setup-token from `claude setup-token`",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -360,7 +360,6 @@ export async function runOnboardingWizard(
|
||||
prompter,
|
||||
store: authStore,
|
||||
includeSkip: true,
|
||||
includeClaudeCliIfMissing: true,
|
||||
}));
|
||||
|
||||
const authResult = await applyAuthChoice({
|
||||
|
||||
Reference in New Issue
Block a user