mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-07 23:31:07 +00:00
* fix(google-gemini-cli-auth): fix Gemini CLI OAuth failures on Windows Two issues prevented Gemini CLI OAuth from working on Windows: 1. resolveGeminiCliDirs: the first candidate `dirname(dirname(resolvedPath))` can resolve to an unrelated ancestor directory (e.g. the nvm root `C:\Users\<user>\AppData\Local\nvm`) when gemini is installed via nvm. The subsequent `findFile` recursive search (depth 10) then picks up an `oauth2.js` from a completely different package (e.g. `discord-api-types/payloads/v10/oauth2.js`), which naturally does not contain Google OAuth credentials, causing silent extraction failure. Fix: validate candidate directories before including them — only keep candidates that contain a `package.json` or a `node_modules/@google/ gemini-cli-core` subdirectory. 2. resolvePlatform: returns "WINDOWS" on win32, but Google's loadCodeAssist API rejects it as an invalid Platform enum value (400 INVALID_ARGUMENT), just like it rejects "LINUX". Fix: use "PLATFORM_UNSPECIFIED" for all non-macOS platforms. * test(google-gemini-cli-auth): keep oauth regressions portable * chore(changelog): add google gemini cli auth fix note --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
236 lines
6.6 KiB
TypeScript
236 lines
6.6 KiB
TypeScript
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
import type { Dirent } from "node:fs";
|
|
import { delimiter, dirname, join } from "node:path";
|
|
import { CLIENT_ID_KEYS, CLIENT_SECRET_KEYS } from "./oauth.shared.js";
|
|
|
|
type CredentialFs = {
|
|
existsSync: (path: Parameters<typeof existsSync>[0]) => ReturnType<typeof existsSync>;
|
|
readFileSync: (path: Parameters<typeof readFileSync>[0], encoding: "utf8") => string;
|
|
realpathSync: (path: Parameters<typeof realpathSync>[0]) => string;
|
|
readdirSync: (
|
|
path: Parameters<typeof readdirSync>[0],
|
|
options: { withFileTypes: true },
|
|
) => Dirent[];
|
|
};
|
|
|
|
const defaultFs: CredentialFs = {
|
|
existsSync,
|
|
readFileSync,
|
|
realpathSync,
|
|
readdirSync,
|
|
};
|
|
|
|
let credentialFs: CredentialFs = defaultFs;
|
|
|
|
function resolveEnv(keys: string[]): string | undefined {
|
|
for (const key of keys) {
|
|
const value = process.env[key]?.trim();
|
|
if (value) {
|
|
return value;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
let cachedGeminiCliCredentials: { clientId: string; clientSecret: string } | null = null;
|
|
|
|
export function clearCredentialsCache(): void {
|
|
cachedGeminiCliCredentials = null;
|
|
}
|
|
|
|
export function setOAuthCredentialsFsForTest(overrides?: Partial<CredentialFs>): void {
|
|
credentialFs = overrides ? { ...defaultFs, ...overrides } : defaultFs;
|
|
}
|
|
|
|
export function extractGeminiCliCredentials(): { clientId: string; clientSecret: string } | null {
|
|
if (cachedGeminiCliCredentials) {
|
|
return cachedGeminiCliCredentials;
|
|
}
|
|
|
|
try {
|
|
const geminiPath = findInPath("gemini");
|
|
if (!geminiPath) {
|
|
return null;
|
|
}
|
|
|
|
const resolvedPath = credentialFs.realpathSync(geminiPath);
|
|
const geminiCliDirs = resolveGeminiCliDirs(geminiPath, resolvedPath);
|
|
|
|
for (const geminiCliDir of geminiCliDirs) {
|
|
const directCredentials = readGeminiCliCredentialsFromKnownPaths(geminiCliDir);
|
|
if (directCredentials) {
|
|
cachedGeminiCliCredentials = directCredentials;
|
|
return directCredentials;
|
|
}
|
|
|
|
const discoveredCredentials = findGeminiCliCredentialsInTree(geminiCliDir, 10);
|
|
if (discoveredCredentials) {
|
|
cachedGeminiCliCredentials = discoveredCredentials;
|
|
return discoveredCredentials;
|
|
}
|
|
}
|
|
} catch {
|
|
// Gemini CLI not installed or extraction failed
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function resolveGeminiCliDirs(geminiPath: string, resolvedPath: string): string[] {
|
|
const binDir = dirname(geminiPath);
|
|
const candidates = [
|
|
dirname(dirname(resolvedPath)),
|
|
join(dirname(resolvedPath), "node_modules", "@google", "gemini-cli"),
|
|
join(binDir, "node_modules", "@google", "gemini-cli"),
|
|
join(dirname(binDir), "node_modules", "@google", "gemini-cli"),
|
|
join(dirname(binDir), "lib", "node_modules", "@google", "gemini-cli"),
|
|
];
|
|
|
|
const deduped: string[] = [];
|
|
const seen = new Set<string>();
|
|
for (const candidate of candidates) {
|
|
for (const searchDir of resolveGeminiCliSearchDirs(candidate)) {
|
|
const key =
|
|
process.platform === "win32" ? searchDir.replace(/\\/g, "/").toLowerCase() : searchDir;
|
|
if (seen.has(key)) {
|
|
continue;
|
|
}
|
|
seen.add(key);
|
|
deduped.push(searchDir);
|
|
}
|
|
}
|
|
return deduped;
|
|
}
|
|
|
|
function resolveGeminiCliSearchDirs(candidate: string): string[] {
|
|
const searchDirs = [
|
|
candidate,
|
|
join(candidate, "node_modules", "@google", "gemini-cli"),
|
|
join(candidate, "lib", "node_modules", "@google", "gemini-cli"),
|
|
];
|
|
return searchDirs.filter(looksLikeGeminiCliDir);
|
|
}
|
|
|
|
function looksLikeGeminiCliDir(candidate: string): boolean {
|
|
return (
|
|
credentialFs.existsSync(join(candidate, "package.json")) ||
|
|
credentialFs.existsSync(join(candidate, "node_modules", "@google", "gemini-cli-core"))
|
|
);
|
|
}
|
|
|
|
function findInPath(name: string): string | null {
|
|
const exts = process.platform === "win32" ? [".cmd", ".bat", ".exe", ""] : [""];
|
|
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
for (const ext of exts) {
|
|
const path = join(dir, name + ext);
|
|
if (credentialFs.existsSync(path)) {
|
|
return path;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function readGeminiCliCredentialsFile(
|
|
path: string,
|
|
): { clientId: string; clientSecret: string } | null {
|
|
try {
|
|
return parseGeminiCliCredentials(credentialFs.readFileSync(path, "utf8"));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function parseGeminiCliCredentials(
|
|
content: string,
|
|
): { clientId: string; clientSecret: string } | null {
|
|
const idMatch = content.match(/(\d+-[a-z0-9]+\.apps\.googleusercontent\.com)/);
|
|
const secretMatch = content.match(/(GOCSPX-[A-Za-z0-9_-]+)/);
|
|
if (!idMatch || !secretMatch) {
|
|
return null;
|
|
}
|
|
return { clientId: idMatch[1], clientSecret: secretMatch[1] };
|
|
}
|
|
|
|
function readGeminiCliCredentialsFromKnownPaths(
|
|
geminiCliDir: string,
|
|
): { clientId: string; clientSecret: string } | null {
|
|
const searchPaths = [
|
|
join(
|
|
geminiCliDir,
|
|
"node_modules",
|
|
"@google",
|
|
"gemini-cli-core",
|
|
"dist",
|
|
"src",
|
|
"code_assist",
|
|
"oauth2.js",
|
|
),
|
|
join(
|
|
geminiCliDir,
|
|
"node_modules",
|
|
"@google",
|
|
"gemini-cli-core",
|
|
"dist",
|
|
"code_assist",
|
|
"oauth2.js",
|
|
),
|
|
];
|
|
|
|
for (const path of searchPaths) {
|
|
if (!credentialFs.existsSync(path)) {
|
|
continue;
|
|
}
|
|
const credentials = readGeminiCliCredentialsFile(path);
|
|
if (credentials) {
|
|
return credentials;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function findGeminiCliCredentialsInTree(
|
|
dir: string,
|
|
depth: number,
|
|
): { clientId: string; clientSecret: string } | null {
|
|
if (depth <= 0) {
|
|
return null;
|
|
}
|
|
try {
|
|
for (const entry of credentialFs.readdirSync(dir, { withFileTypes: true })) {
|
|
const path = join(dir, entry.name);
|
|
if (entry.isFile() && entry.name === "oauth2.js") {
|
|
const credentials = readGeminiCliCredentialsFile(path);
|
|
if (credentials) {
|
|
return credentials;
|
|
}
|
|
continue;
|
|
}
|
|
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
const found = findGeminiCliCredentialsInTree(path, depth - 1);
|
|
if (found) {
|
|
return found;
|
|
}
|
|
}
|
|
}
|
|
} catch {}
|
|
return null;
|
|
}
|
|
|
|
export function resolveOAuthClientConfig(): { clientId: string; clientSecret?: string } {
|
|
const envClientId = resolveEnv(CLIENT_ID_KEYS);
|
|
const envClientSecret = resolveEnv(CLIENT_SECRET_KEYS);
|
|
if (envClientId) {
|
|
return { clientId: envClientId, clientSecret: envClientSecret };
|
|
}
|
|
|
|
const extracted = extractGeminiCliCredentials();
|
|
if (extracted) {
|
|
return extracted;
|
|
}
|
|
|
|
throw new Error(
|
|
"Gemini CLI not found. Install it first: brew install gemini-cli (or npm install -g @google/gemini-cli), or set GEMINI_CLI_OAUTH_CLIENT_ID.",
|
|
);
|
|
}
|