UNPKG

claude-gemini-multimodal-bridge

Version:

Enterprise-grade AI integration bridge connecting Claude Code, Gemini CLI, and Google AI Studio with intelligent routing and advanced multimodal processing capabilities

269 lines (268 loc) 9.51 kB
import { execSync, spawn } from 'child_process'; import { AuthenticationError, AuthErrorCode } from '../core/types.js'; import { logger } from '../utils/logger.js'; import { safeExecute } from '../utils/errorHandler.js'; import path from 'path'; import fs from 'fs'; import os from 'os'; export class OAuthManager { authCache = new Map(); CACHE_TTL = 3600000; async checkGeminiAuthentication() { return safeExecute(async () => { const cached = this.authCache.get('gemini'); if (cached && this.isCacheValid('gemini')) { return cached; } const oauthFile = path.join(os.homedir(), '.gemini', 'oauth_creds.json'); if (fs.existsSync(oauthFile)) { try { const creds = JSON.parse(fs.readFileSync(oauthFile, 'utf8')); if (creds.access_token || creds.refresh_token) { const status = { isAuthenticated: true, method: 'oauth', userInfo: { planType: 'free', email: undefined, quotaRemaining: undefined } }; this.updateCache('gemini', status); return status; } } catch (error) { logger.warn('OAuth credentials file exists but could not be read', { error: error instanceof Error ? error.message : String(error) }); } } const apiKey = process.env.GEMINI_API_KEY; if (apiKey && apiKey.length > 10) { const status = { isAuthenticated: true, method: 'api_key', userInfo: { planType: 'free', email: undefined, quotaRemaining: undefined } }; this.updateCache('gemini', status); return status; } return { isAuthenticated: false, method: 'oauth', userInfo: undefined, }; }, { operationName: 'check-gemini-auth', layer: 'gemini', timeout: 10000, }); } async promptGeminiLogin() { return safeExecute(async () => { logger.info('Prompting Gemini authentication...'); const method = await this.detectAuthMethod(); if (method === 'api_key') { logger.info('API key authentication detected. Please ensure GEMINI_API_KEY is set.'); const status = await this.checkApiKeyAuth(); return status.isAuthenticated; } logger.info('Starting OAuth authentication flow...'); try { const result = await this.executeGeminiAuth(); if (result) { logger.info('OAuth authentication successful'); const status = await this.checkGeminiAuthentication(); return status.isAuthenticated; } return false; } catch (error) { logger.error('OAuth authentication failed', { error: error.message }); throw new AuthenticationError('OAuth authentication failed. Please try again or use API key method.', 'gemini', AuthErrorCode.OAUTH_FLOW_FAILED, { method: 'oauth', instructions: 'Run "gemini auth" manually or set GEMINI_API_KEY environment variable', canRetry: true }); } }, { operationName: 'prompt-gemini-login', layer: 'gemini', timeout: 60000, }); } async refreshGeminiToken() { return safeExecute(async () => { const method = await this.detectAuthMethod(); if (method === 'api_key') { const status = await this.checkApiKeyAuth(); this.updateCache('gemini', status); return status.isAuthenticated; } logger.info('Attempting to refresh OAuth token...'); try { this.authCache.delete('gemini'); const currentStatus = await this.checkOAuthAuth(); if (currentStatus.isAuthenticated) { this.updateCache('gemini', currentStatus); return true; } logger.warn('OAuth token expired, re-authentication required'); return await this.promptGeminiLogin(); } catch (error) { logger.error('Token refresh failed', { error: error.message }); return false; } }, { operationName: 'refresh-gemini-token', layer: 'gemini', timeout: 30000, }); } async detectAuthMethod() { const oauthFile = path.join(os.homedir(), '.gemini', 'oauth_creds.json'); if (fs.existsSync(oauthFile)) { return 'oauth'; } const geminiApiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY; if (geminiApiKey && geminiApiKey.length > 10) { return 'api_key'; } return 'none'; } async checkApiKeyAuth() { const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY; if (!apiKey) { return { isAuthenticated: false, method: 'api_key', userInfo: undefined, }; } if (apiKey.length < 10) { logger.warn('API key format appears invalid'); return { isAuthenticated: false, method: 'api_key', userInfo: undefined, }; } return { isAuthenticated: true, method: 'api_key', userInfo: { email: undefined, quotaRemaining: undefined, planType: 'free', }, }; } async checkOAuthAuth() { const oauthFile = path.join(os.homedir(), '.gemini', 'oauth_creds.json'); if (fs.existsSync(oauthFile)) { try { const creds = JSON.parse(fs.readFileSync(oauthFile, 'utf8')); if (creds.access_token || creds.refresh_token) { return { isAuthenticated: true, method: 'oauth', userInfo: { email: undefined, quotaRemaining: undefined, planType: 'free', }, }; } } catch (error) { logger.debug('OAuth credentials file could not be parsed', { error: error.message }); } } return { isAuthenticated: false, method: 'oauth', userInfo: undefined, }; } async executeGeminiAuth() { return new Promise((resolve, reject) => { logger.info('Executing: gemini auth'); const child = spawn('gemini', ['auth'], { stdio: 'inherit', }); const timeout = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('Authentication timeout')); }, 120000); child.on('close', (code) => { clearTimeout(timeout); if (code === 0) { resolve(true); } else { reject(new Error(`Authentication failed with exit code: ${code}`)); } }); child.on('error', (error) => { clearTimeout(timeout); reject(error); }); }); } async checkGeminiCLIAvailable() { try { execSync('which gemini', { stdio: 'ignore' }); return true; } catch { try { execSync('gemini --version', { stdio: 'ignore' }); return true; } catch { return false; } } } updateCache(service, status) { this.authCache.set(service, { ...status, }); } isCacheValid(_service) { return false; } clearCache() { this.authCache.clear(); logger.debug('Authentication cache cleared'); } async getAuthMethod(service) { if (service === 'gemini' || service === 'aistudio') { return await this.detectAuthMethod(); } if (service === 'claude') { return 'session'; } return 'none'; } validateApiKey(apiKey) { if (!apiKey || typeof apiKey !== 'string') { return false; } return apiKey.length >= 20 && apiKey.startsWith('AI'); } maskApiKey(apiKey) { if (!apiKey || apiKey.length < 8) { return '***'; } return apiKey.substring(0, 8) + '***'; } }