From e88559a1417ccde62c38e272f01dbea5caf1ae68 Mon Sep 17 00:00:00 2001 From: Glucksberg Date: Fri, 30 Jan 2026 00:23:11 +0000 Subject: [PATCH] feat(auth): sync credentials from Claude CLI Extend external CLI credential sync to also read from Claude CLI's ~/.claude/.credentials.json file. This allows users who authenticate with Claude CLI to automatically have those credentials available in Moltbot without manual configuration. - Reads OAuth tokens from Claude CLI credential store - Auto-refreshes when token expires - Respects same TTL and near-expiry thresholds as Qwen CLI sync --- src/agents/auth-profiles/external-cli-sync.ts | 58 ++++++++++++++++--- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/src/agents/auth-profiles/external-cli-sync.ts b/src/agents/auth-profiles/external-cli-sync.ts index d1fa31f23..a37c6059f 100644 --- a/src/agents/auth-profiles/external-cli-sync.ts +++ b/src/agents/auth-profiles/external-cli-sync.ts @@ -1,5 +1,9 @@ -import { readQwenCliCredentialsCached } from "../cli-credentials.js"; import { + readClaudeCliCredentialsCached, + readQwenCliCredentialsCached, +} from "../cli-credentials.js"; +import { + CLAUDE_CLI_PROFILE_ID, EXTERNAL_CLI_NEAR_EXPIRY_MS, EXTERNAL_CLI_SYNC_TTL_MS, QWEN_CLI_PROFILE_ID, @@ -22,18 +26,20 @@ function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCr ); } -function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: number): boolean { +function isExternalProfileFresh( + cred: AuthProfileCredential | undefined, + now: number, + provider: string, +): boolean { if (!cred) return false; if (cred.type !== "oauth" && cred.type !== "token") return false; - if (cred.provider !== "qwen-portal") { - return false; - } + if (cred.provider !== provider) return false; if (typeof cred.expires !== "number") return true; return cred.expires > now + EXTERNAL_CLI_NEAR_EXPIRY_MS; } /** - * Sync OAuth credentials from external CLI tools (Qwen Code CLI) into the store. + * Sync OAuth credentials from external CLI tools (Claude CLI, Qwen Code CLI) into the store. * * Returns true if any credentials were updated. */ @@ -41,12 +47,50 @@ export function syncExternalCliCredentials(store: AuthProfileStore): boolean { let mutated = false; const now = Date.now(); + // Sync from Claude CLI (~/.claude/.credentials.json) + const existingClaude = store.profiles[CLAUDE_CLI_PROFILE_ID]; + const shouldSyncClaude = + !existingClaude || + existingClaude.provider !== "anthropic" || + !isExternalProfileFresh(existingClaude, now, "anthropic"); + const claudeCreds = shouldSyncClaude + ? readClaudeCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }) + : null; + if (claudeCreds) { + const existing = store.profiles[CLAUDE_CLI_PROFILE_ID]; + const existingTyped = + existing?.type === "oauth" || existing?.type === "token" ? existing : undefined; + const shouldUpdate = + !existingTyped || + existingTyped.provider !== "anthropic" || + (typeof existingTyped.expires === "number" && existingTyped.expires <= now) || + (typeof claudeCreds.expires === "number" && + typeof existingTyped.expires === "number" && + claudeCreds.expires > existingTyped.expires); + + if (shouldUpdate) { + const isSame = + claudeCreds.type === "oauth" && + existingTyped?.type === "oauth" && + shallowEqualOAuthCredentials(existingTyped, claudeCreds); + if (!isSame) { + store.profiles[CLAUDE_CLI_PROFILE_ID] = claudeCreds; + mutated = true; + log.info("synced claude credentials from claude cli", { + profileId: CLAUDE_CLI_PROFILE_ID, + type: claudeCreds.type, + expires: new Date(claudeCreds.expires).toISOString(), + }); + } + } + } + // Sync from Qwen Code CLI const existingQwen = store.profiles[QWEN_CLI_PROFILE_ID]; const shouldSyncQwen = !existingQwen || existingQwen.provider !== "qwen-portal" || - !isExternalProfileFresh(existingQwen, now); + !isExternalProfileFresh(existingQwen, now, "qwen-portal"); const qwenCreds = shouldSyncQwen ? readQwenCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }) : null;