Merge 2a4f44851a into da71eaebd2
This commit is contained in:
commit
16e22388a5
@ -7,6 +7,7 @@ export const LEGACY_AUTH_FILENAME = "auth.json";
|
|||||||
export const CLAUDE_CLI_PROFILE_ID = "anthropic:claude-cli";
|
export const CLAUDE_CLI_PROFILE_ID = "anthropic:claude-cli";
|
||||||
export const CODEX_CLI_PROFILE_ID = "openai-codex:codex-cli";
|
export const CODEX_CLI_PROFILE_ID = "openai-codex:codex-cli";
|
||||||
export const QWEN_CLI_PROFILE_ID = "qwen-portal:qwen-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 = {
|
export const AUTH_STORE_LOCK_OPTIONS = {
|
||||||
retries: {
|
retries: {
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
import { readQwenCliCredentialsCached } from "../cli-credentials.js";
|
import {
|
||||||
|
readGeminiCliCredentialsCached,
|
||||||
|
readQwenCliCredentialsCached,
|
||||||
|
} from "../cli-credentials.js";
|
||||||
import {
|
import {
|
||||||
EXTERNAL_CLI_NEAR_EXPIRY_MS,
|
EXTERNAL_CLI_NEAR_EXPIRY_MS,
|
||||||
EXTERNAL_CLI_SYNC_TTL_MS,
|
EXTERNAL_CLI_SYNC_TTL_MS,
|
||||||
|
GEMINI_CLI_PROFILE_ID,
|
||||||
QWEN_CLI_PROFILE_ID,
|
QWEN_CLI_PROFILE_ID,
|
||||||
log,
|
log,
|
||||||
} from "./constants.js";
|
} from "./constants.js";
|
||||||
@ -25,7 +29,7 @@ function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCr
|
|||||||
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 (cred.provider !== "qwen-portal") {
|
if (cred.provider !== "qwen-portal" && cred.provider !== "google-gemini-cli") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (typeof cred.expires !== "number") return true;
|
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;
|
return mutated;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { AUTH_STORE_LOCK_OPTIONS, log } 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";
|
||||||
|
import { syncExternalCliCredentials } from "./external-cli-sync.js";
|
||||||
import { ensureAuthProfileStore, saveAuthProfileStore } from "./store.js";
|
import { ensureAuthProfileStore, saveAuthProfileStore } from "./store.js";
|
||||||
import type { AuthProfileStore } from "./types.js";
|
import type { AuthProfileStore } from "./types.js";
|
||||||
|
|
||||||
@ -35,6 +36,15 @@ async function refreshOAuthTokenWithLock(params: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const store = ensureAuthProfileStore(params.agentDir);
|
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];
|
const cred = store.profiles[params.profileId];
|
||||||
if (!cred || cred.type !== "oauth") return null;
|
if (!cred || cred.type !== "oauth") return null;
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ const log = createSubsystemLogger("agents/auth-profiles");
|
|||||||
const CLAUDE_CLI_CREDENTIALS_RELATIVE_PATH = ".claude/.credentials.json";
|
const CLAUDE_CLI_CREDENTIALS_RELATIVE_PATH = ".claude/.credentials.json";
|
||||||
const CODEX_CLI_AUTH_FILENAME = "auth.json";
|
const CODEX_CLI_AUTH_FILENAME = "auth.json";
|
||||||
const QWEN_CLI_CREDENTIALS_RELATIVE_PATH = ".qwen/oauth_creds.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_SERVICE = "Claude Code-credentials";
|
||||||
const CLAUDE_CLI_KEYCHAIN_ACCOUNT = "Claude Code";
|
const CLAUDE_CLI_KEYCHAIN_ACCOUNT = "Claude Code";
|
||||||
@ -27,11 +28,13 @@ type CachedValue<T> = {
|
|||||||
let claudeCliCache: CachedValue<ClaudeCliCredential> | null = null;
|
let claudeCliCache: CachedValue<ClaudeCliCredential> | null = null;
|
||||||
let codexCliCache: CachedValue<CodexCliCredential> | null = null;
|
let codexCliCache: CachedValue<CodexCliCredential> | null = null;
|
||||||
let qwenCliCache: CachedValue<QwenCliCredential> | null = null;
|
let qwenCliCache: CachedValue<QwenCliCredential> | null = null;
|
||||||
|
let geminiCliCache: CachedValue<GeminiCliCredential> | null = null;
|
||||||
|
|
||||||
export function resetCliCredentialCachesForTest(): void {
|
export function resetCliCredentialCachesForTest(): void {
|
||||||
claudeCliCache = null;
|
claudeCliCache = null;
|
||||||
codexCliCache = null;
|
codexCliCache = null;
|
||||||
qwenCliCache = null;
|
qwenCliCache = null;
|
||||||
|
geminiCliCache = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ClaudeCliCredential =
|
export type ClaudeCliCredential =
|
||||||
@ -66,6 +69,16 @@ export type QwenCliCredential = {
|
|||||||
expires: number;
|
expires: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GeminiCliCredential = {
|
||||||
|
type: "oauth";
|
||||||
|
provider: "google-gemini-cli";
|
||||||
|
access: string;
|
||||||
|
refresh: string;
|
||||||
|
expires: number;
|
||||||
|
email?: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
type ClaudeCliFileOptions = {
|
type ClaudeCliFileOptions = {
|
||||||
homeDir?: string;
|
homeDir?: string;
|
||||||
};
|
};
|
||||||
@ -102,6 +115,11 @@ function resolveQwenCliCredentialsPath(homeDir?: string) {
|
|||||||
return path.join(baseDir, QWEN_CLI_CREDENTIALS_RELATIVE_PATH);
|
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) {
|
function computeCodexKeychainAccount(codexHome: string) {
|
||||||
const hash = createHash("sha256").update(codexHome).digest("hex");
|
const hash = createHash("sha256").update(codexHome).digest("hex");
|
||||||
return `cli|${hash.slice(0, 16)}`;
|
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(
|
function readClaudeCliKeychainCredentials(
|
||||||
execSyncImpl: ExecSyncFn = execSync,
|
execSyncImpl: ExecSyncFn = execSync,
|
||||||
): ClaudeCliCredential | null {
|
): ClaudeCliCredential | null {
|
||||||
@ -497,3 +542,25 @@ export function readQwenCliCredentialsCached(options?: {
|
|||||||
}
|
}
|
||||||
return value;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user