From 2a4f44851a3997c910bf891f97daedc761ad4e8c Mon Sep 17 00:00:00 2001 From: Manus Contributor Date: Fri, 30 Jan 2026 04:29:55 -0500 Subject: [PATCH] fix: sync google-gemini-cli-auth tokens from external CLI (#3803) --- src/agents/auth-profiles/constants.ts | 1 + src/agents/auth-profiles/external-cli-sync.ts | 37 +++++++++- src/agents/auth-profiles/oauth.ts | 10 +++ src/agents/cli-credentials.ts | 67 +++++++++++++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) diff --git a/src/agents/auth-profiles/constants.ts b/src/agents/auth-profiles/constants.ts index 65c2f7a54..0192e16f2 100644 --- a/src/agents/auth-profiles/constants.ts +++ b/src/agents/auth-profiles/constants.ts @@ -7,6 +7,7 @@ export const LEGACY_AUTH_FILENAME = "auth.json"; export const CLAUDE_CLI_PROFILE_ID = "anthropic:claude-cli"; export const CODEX_CLI_PROFILE_ID = "openai-codex:codex-cli"; export const QWEN_CLI_PROFILE_ID = "qwen-portal:qwen-cli"; +export const GEMINI_CLI_PROFILE_ID = "google-gemini-cli:google-gemini-cli"; export const AUTH_STORE_LOCK_OPTIONS = { retries: { diff --git a/src/agents/auth-profiles/external-cli-sync.ts b/src/agents/auth-profiles/external-cli-sync.ts index d1fa31f23..610ea22d8 100644 --- a/src/agents/auth-profiles/external-cli-sync.ts +++ b/src/agents/auth-profiles/external-cli-sync.ts @@ -1,7 +1,11 @@ -import { readQwenCliCredentialsCached } from "../cli-credentials.js"; +import { + readGeminiCliCredentialsCached, + readQwenCliCredentialsCached, +} from "../cli-credentials.js"; import { EXTERNAL_CLI_NEAR_EXPIRY_MS, EXTERNAL_CLI_SYNC_TTL_MS, + GEMINI_CLI_PROFILE_ID, QWEN_CLI_PROFILE_ID, log, } from "./constants.js"; @@ -25,7 +29,7 @@ function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCr function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: number): boolean { if (!cred) return false; if (cred.type !== "oauth" && cred.type !== "token") return false; - if (cred.provider !== "qwen-portal") { + if (cred.provider !== "qwen-portal" && cred.provider !== "google-gemini-cli") { return false; } if (typeof cred.expires !== "number") return true; @@ -69,5 +73,34 @@ export function syncExternalCliCredentials(store: AuthProfileStore): boolean { } } + // Sync from Gemini CLI + const geminiCreds = readGeminiCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }); + if (geminiCreds) { + // We sync to both the generic "google-gemini-cli" profile and the email-specific one + const profileIds = [GEMINI_CLI_PROFILE_ID]; + if (geminiCreds.email) { + profileIds.push(`google-gemini-cli:${geminiCreds.email}`); + } + + for (const profileId of profileIds) { + const existing = store.profiles[profileId]; + const existingOAuth = existing?.type === "oauth" ? existing : undefined; + const shouldUpdate = + !existingOAuth || + existingOAuth.provider !== "google-gemini-cli" || + existingOAuth.expires <= now || + geminiCreds.expires > existingOAuth.expires; + + if (shouldUpdate && !shallowEqualOAuthCredentials(existingOAuth, geminiCreds)) { + store.profiles[profileId] = geminiCreds; + mutated = true; + log.info("synced gemini credentials from gemini cli", { + profileId, + expires: new Date(geminiCreds.expires).toISOString(), + }); + } + } + } + return mutated; } diff --git a/src/agents/auth-profiles/oauth.ts b/src/agents/auth-profiles/oauth.ts index 590a752a3..8a03d635d 100644 --- a/src/agents/auth-profiles/oauth.ts +++ b/src/agents/auth-profiles/oauth.ts @@ -8,6 +8,7 @@ import { AUTH_STORE_LOCK_OPTIONS, log } from "./constants.js"; import { formatAuthDoctorHint } from "./doctor.js"; import { ensureAuthStoreFile, resolveAuthStorePath } from "./paths.js"; import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js"; +import { syncExternalCliCredentials } from "./external-cli-sync.js"; import { ensureAuthProfileStore, saveAuthProfileStore } from "./store.js"; import type { AuthProfileStore } from "./types.js"; @@ -35,6 +36,15 @@ async function refreshOAuthTokenWithLock(params: { }); const store = ensureAuthProfileStore(params.agentDir); + + // Try syncing from external CLI first if this is a Gemini CLI profile + if (params.profileId.startsWith("google-gemini-cli")) { + const mutated = syncExternalCliCredentials(store); + if (mutated) { + saveAuthProfileStore(store, params.agentDir); + } + } + const cred = store.profiles[params.profileId]; if (!cred || cred.type !== "oauth") return null; diff --git a/src/agents/cli-credentials.ts b/src/agents/cli-credentials.ts index 5ca5629ff..8c0982757 100644 --- a/src/agents/cli-credentials.ts +++ b/src/agents/cli-credentials.ts @@ -14,6 +14,7 @@ const log = createSubsystemLogger("agents/auth-profiles"); const CLAUDE_CLI_CREDENTIALS_RELATIVE_PATH = ".claude/.credentials.json"; const CODEX_CLI_AUTH_FILENAME = "auth.json"; const QWEN_CLI_CREDENTIALS_RELATIVE_PATH = ".qwen/oauth_creds.json"; +const GEMINI_CLI_CREDENTIALS_RELATIVE_PATH = ".gemini-cli/credentials.json"; const CLAUDE_CLI_KEYCHAIN_SERVICE = "Claude Code-credentials"; const CLAUDE_CLI_KEYCHAIN_ACCOUNT = "Claude Code"; @@ -27,11 +28,13 @@ type CachedValue = { let claudeCliCache: CachedValue | null = null; let codexCliCache: CachedValue | null = null; let qwenCliCache: CachedValue | null = null; +let geminiCliCache: CachedValue | null = null; export function resetCliCredentialCachesForTest(): void { claudeCliCache = null; codexCliCache = null; qwenCliCache = null; + geminiCliCache = null; } export type ClaudeCliCredential = @@ -66,6 +69,16 @@ export type QwenCliCredential = { expires: number; }; +export type GeminiCliCredential = { + type: "oauth"; + provider: "google-gemini-cli"; + access: string; + refresh: string; + expires: number; + email?: string; + projectId: string; +}; + type ClaudeCliFileOptions = { homeDir?: string; }; @@ -102,6 +115,11 @@ function resolveQwenCliCredentialsPath(homeDir?: string) { return path.join(baseDir, QWEN_CLI_CREDENTIALS_RELATIVE_PATH); } +function resolveGeminiCliCredentialsPath(homeDir?: string) { + const baseDir = homeDir ?? resolveUserPath("~"); + return path.join(baseDir, GEMINI_CLI_CREDENTIALS_RELATIVE_PATH); +} + function computeCodexKeychainAccount(codexHome: string) { const hash = createHash("sha256").update(codexHome).digest("hex"); return `cli|${hash.slice(0, 16)}`; @@ -186,6 +204,33 @@ function readQwenCliCredentials(options?: { homeDir?: string }): QwenCliCredenti }; } +function readGeminiCliCredentials(options?: { homeDir?: string }): GeminiCliCredential | null { + const credPath = resolveGeminiCliCredentialsPath(options?.homeDir); + const raw = loadJsonFile(credPath); + if (!raw || typeof raw !== "object") return null; + const data = raw as Record; + const accessToken = data.access_token; + const refreshToken = data.refresh_token; + const expiresAt = data.expiry_date; + const projectId = data.project_id; + const email = data.email; + + if (typeof accessToken !== "string" || !accessToken) return null; + if (typeof refreshToken !== "string" || !refreshToken) return null; + if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return null; + if (typeof projectId !== "string" || !projectId) return null; + + return { + type: "oauth", + provider: "google-gemini-cli", + access: accessToken, + refresh: refreshToken, + expires: expiresAt, + projectId, + email: typeof email === "string" ? email : undefined, + }; +} + function readClaudeCliKeychainCredentials( execSyncImpl: ExecSyncFn = execSync, ): ClaudeCliCredential | null { @@ -497,3 +542,25 @@ export function readQwenCliCredentialsCached(options?: { } return value; } + +export function readGeminiCliCredentialsCached(options?: { + ttlMs?: number; + homeDir?: string; +}): GeminiCliCredential | null { + const ttlMs = options?.ttlMs ?? 0; + const now = Date.now(); + const cacheKey = resolveGeminiCliCredentialsPath(options?.homeDir); + if ( + ttlMs > 0 && + geminiCliCache && + geminiCliCache.cacheKey === cacheKey && + now - geminiCliCache.readAt < ttlMs + ) { + return geminiCliCache.value; + } + const value = readGeminiCliCredentials({ homeDir: options?.homeDir }); + if (ttlMs > 0) { + geminiCliCache = { value, readAt: now, cacheKey }; + } + return value; +}