This commit is contained in:
SALIM BIN YOUSUF 2026-01-30 17:05:38 +05:30 committed by GitHub
commit 16e22388a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 113 additions and 2 deletions

View File

@ -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: {

View File

@ -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;
}

View File

@ -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;

View File

@ -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<T> = {
let claudeCliCache: CachedValue<ClaudeCliCredential> | null = null;
let codexCliCache: CachedValue<CodexCliCredential> | null = null;
let qwenCliCache: CachedValue<QwenCliCredential> | null = null;
let geminiCliCache: CachedValue<GeminiCliCredential> | 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<string, unknown>;
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;
}