From a7261c2fbecd73aa681314fd63eb13cb4e4a0df9 Mon Sep 17 00:00:00 2001 From: ilya-bov <111734093+ilya-bov@users.noreply.github.com> Date: Fri, 6 Mar 2026 23:20:33 +0300 Subject: [PATCH] Auto-discover Codex/Gemini OAuth files across common homes --- .env.example | 1 + README.md | 6 +- src/lib/providers/provider-auth.ts | 131 ++++++++++++++++++++++++++--- 3 files changed, 125 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index e813fa9..dddad98 100644 --- a/.env.example +++ b/.env.example @@ -38,6 +38,7 @@ APP_PORT=3000 # XDG_CACHE_HOME=/app/data/.cache # Optional CLI OAuth credential file overrides (for codex-cli / gemini-cli provider auth checks). +# If unset, Eggent auto-discovers files in common home directories. # Useful when Eggent runs under a different user than the CLI login user. # CODEX_AUTH_FILE=/home/node/.codex/auth.json # GEMINI_OAUTH_CREDS_FILE=/home/node/.gemini/oauth_creds.json diff --git a/README.md b/README.md index 00dea00..40b5c85 100644 --- a/README.md +++ b/README.md @@ -216,9 +216,9 @@ Main environment variables: | `PLAYWRIGHT_BROWSERS_PATH` | No | Browser install/cache path for Playwright (default: `/app/data/ms-playwright`) | | `NPM_CONFIG_CACHE` | No | npm cache directory for runtime installs (default: `/app/data/npm-cache`) | | `XDG_CACHE_HOME` | No | Generic CLI cache directory (default: `/app/data/.cache`) | -| `CODEX_AUTH_FILE` | No | Explicit path to Codex OAuth file (default: `$HOME/.codex/auth.json`) | -| `GEMINI_OAUTH_CREDS_FILE` | No | Explicit path to Gemini OAuth creds file (default: `$HOME/.gemini/oauth_creds.json`) | -| `GEMINI_SETTINGS_FILE` | No | Explicit path to Gemini settings file (default: `$HOME/.gemini/settings.json`) | +| `CODEX_AUTH_FILE` | No | Explicit path to Codex OAuth file (if unset, Eggent auto-discovers `.codex/auth.json` in common home dirs) | +| `GEMINI_OAUTH_CREDS_FILE` | No | Explicit path to Gemini OAuth creds file (if unset, Eggent auto-discovers `.gemini/oauth_creds.json` in common home dirs) | +| `GEMINI_SETTINGS_FILE` | No | Explicit path to Gemini settings file (if unset, Eggent auto-discovers `.gemini/settings.json` in common home dirs) | ## Data Persistence diff --git a/src/lib/providers/provider-auth.ts b/src/lib/providers/provider-auth.ts index bf59e28..cbd7cd6 100644 --- a/src/lib/providers/provider-auth.ts +++ b/src/lib/providers/provider-auth.ts @@ -83,11 +83,88 @@ function resolveAuthPath(envName: string, defaultPath: string): string { return trimmed ? trimmed : defaultPath; } +function isReadableFile(filePath: string): boolean { + try { + return fs.statSync(filePath).isFile(); + } catch { + return false; + } +} + +function listChildDirs(baseDir: string): string[] { + try { + return fs + .readdirSync(baseDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")) + .map((entry) => path.join(baseDir, entry.name)); + } catch { + return []; + } +} + +function collectHomeCandidates(): string[] { + const candidates = new Set(); + + const addCandidate = (value: string | undefined | null) => { + if (!value) return; + const trimmed = value.trim(); + if (!trimmed) return; + candidates.add(trimmed); + }; + + addCandidate(os.homedir()); + addCandidate(process.env.HOME); + addCandidate("/home/node"); + addCandidate("/root"); + addCandidate(path.join(process.cwd(), "data")); + + for (const dir of listChildDirs("/home")) { + addCandidate(dir); + } + for (const dir of listChildDirs("/Users")) { + addCandidate(dir); + } + + return Array.from(candidates); +} + +function firstExistingFile(paths: string[]): string | null { + for (const filePath of paths) { + if (isReadableFile(filePath)) { + return filePath; + } + } + return null; +} + +function discoverPath(defaultPath: string, relativePath: string): string { + if (isReadableFile(defaultPath)) { + return defaultPath; + } + const discovered = + firstExistingFile( + collectHomeCandidates().map((homeDir) => path.join(homeDir, relativePath)) + ) || null; + return discovered || defaultPath; +} + +function deriveGeminiSettingsPathFromCreds(credsPath: string): string | null { + const parsed = path.parse(credsPath); + if (parsed.base !== "oauth_creds.json") return null; + if (path.basename(parsed.dir) !== ".gemini") return null; + return path.join(parsed.dir, "settings.json"); +} + function readCodexAuth(): { path: string; parsed: CodexAuthFile | null } { - const authPath = resolveAuthPath( + const defaultPath = path.join(os.homedir(), ".codex", "auth.json"); + const configuredPath = resolveAuthPath( "CODEX_AUTH_FILE", - path.join(os.homedir(), ".codex", "auth.json") + defaultPath ); + const authPath = + configuredPath === defaultPath + ? discoverPath(defaultPath, path.join(".codex", "auth.json")) + : configuredPath; const parsed = readJsonObject(authPath) as CodexAuthFile | null; return { path: authPath, parsed }; } @@ -173,25 +250,47 @@ function checkCodexOauthStatus(): ProviderAuthStatus { } function readGeminiSettings(): { path: string; parsed: Record | null } { + const defaultPath = path.join(os.homedir(), ".gemini", "settings.json"); const settingsPath = resolveAuthPath( "GEMINI_SETTINGS_FILE", - path.join(os.homedir(), ".gemini", "settings.json") + defaultPath ); - return { path: settingsPath, parsed: readJsonObject(settingsPath) }; + const resolvedSettingsPath = + settingsPath === defaultPath + ? discoverPath(defaultPath, path.join(".gemini", "settings.json")) + : settingsPath; + return { path: resolvedSettingsPath, parsed: readJsonObject(resolvedSettingsPath) }; } function readGeminiOauthCreds(): { path: string; parsed: GeminiOauthCreds | null } { + const defaultPath = path.join(os.homedir(), ".gemini", "oauth_creds.json"); const credsPath = resolveAuthPath( "GEMINI_OAUTH_CREDS_FILE", - path.join(os.homedir(), ".gemini", "oauth_creds.json") + defaultPath ); - const parsed = readJsonObject(credsPath) as GeminiOauthCreds | null; - return { path: credsPath, parsed }; + const resolvedCredsPath = + credsPath === defaultPath + ? discoverPath(defaultPath, path.join(".gemini", "oauth_creds.json")) + : credsPath; + const parsed = readJsonObject(resolvedCredsPath) as GeminiOauthCreds | null; + return { path: resolvedCredsPath, parsed }; } function resolveGeminiCredential(): ResolvedCliOAuthCredential { - const { parsed: creds } = readGeminiOauthCreds(); - const { path: settingsPath, parsed: settings } = readGeminiSettings(); + const { path: credsPath, parsed: creds } = readGeminiOauthCreds(); + const settingsFromCreds = deriveGeminiSettingsPathFromCreds(credsPath); + const settingsConfigured = process.env.GEMINI_SETTINGS_FILE?.trim(); + const { path: discoveredSettingsPath, parsed: discoveredSettings } = readGeminiSettings(); + const settingsPath = + !settingsConfigured && + settingsFromCreds && + isReadableFile(settingsFromCreds) + ? settingsFromCreds + : discoveredSettingsPath; + const settings = + settingsPath === discoveredSettingsPath + ? discoveredSettings + : readJsonObject(settingsPath); if (!creds) { throw new Error("Gemini OAuth file is missing. Run `gemini` and login with Google."); } @@ -238,7 +337,19 @@ function resolveGeminiCredential(): ResolvedCliOAuthCredential { function checkGeminiOauthStatus(): ProviderAuthStatus { const { path: credsPath, parsed: creds } = readGeminiOauthCreds(); - const { path: settingsPath, parsed: settings } = readGeminiSettings(); + const settingsFromCreds = deriveGeminiSettingsPathFromCreds(credsPath); + const settingsConfigured = process.env.GEMINI_SETTINGS_FILE?.trim(); + const { path: discoveredSettingsPath, parsed: discoveredSettings } = readGeminiSettings(); + const settingsPath = + !settingsConfigured && + settingsFromCreds && + isReadableFile(settingsFromCreds) + ? settingsFromCreds + : discoveredSettingsPath; + const settings = + settingsPath === discoveredSettingsPath + ? discoveredSettings + : readJsonObject(settingsPath); if (!creds) { return {