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

439 lines (438 loc) 17 kB
import { spawn } from 'child_process'; import { join } from 'path'; import { logger } from '../utils/logger.js'; import { safeExecute } from '../utils/errorHandler.js'; import { AuthVerifier } from '../auth/AuthVerifier.js'; import { SearchCache } from '../utils/SearchCache.js'; export class GeminiCLILayer { authVerifier; searchCache; geminiPath = 'gemini'; isInitialized = false; DEFAULT_TIMEOUT = 60000; DEFAULT_MODEL = 'gemini-2.5-flash'; constructor() { this.authVerifier = new AuthVerifier(); this.searchCache = new SearchCache({ ttl: parseInt(process.env.CACHE_TTL || '1800000'), maxEntries: parseInt(process.env.MAX_CACHE_ENTRIES || '1000'), enableMetrics: process.env.ENABLE_CACHING === 'true', similarityThreshold: 0.8 }); } async initialize() { if (this.isInitialized) { return; } return safeExecute(async () => { logger.info('Initializing Gemini CLI layer...'); const authResult = await this.authVerifier.verifyGeminiAuth(); if (!authResult.success) { logger.info('Gemini authentication not configured. Some features may not work.', { error: authResult.error, instructions: 'Run "cgmb auth --service gemini" to authenticate' }); } this.geminiPath = 'gemini'; this.isInitialized = true; logger.info('Gemini CLI layer initialized', { authenticated: authResult.success, authMethod: authResult.status?.method || 'none', }); }, { operationName: 'initialize-gemini-cli-layer', layer: 'gemini', timeout: 10000, }); } async isAvailable() { try { if (!this.isInitialized) { await this.initialize(); } return this.isInitialized; } catch (error) { logger.debug('Gemini CLI layer not available', { error: error.message }); return false; } } canHandle(task) { if (!task || typeof task !== 'object') { return false; } return !!(task.type === 'search' || task.type === 'grounding' || task.action === 'grounded_search' || task.useSearch !== false || task.needsGrounding || (task.type === 'gemini' && !task.files) || task.type === 'text_processing'); } async execute(task) { const startTime = Date.now(); if (!this.isInitialized) { await this.initialize(); } return safeExecute(async () => { logger.info('Executing Gemini CLI task', { taskType: task.type || 'general', useSearch: task.useSearch !== false, hasFiles: !!(task.files && task.files.length > 0), promptLength: task.prompt?.length || 0, }); const prompt = this.extractPrompt(task); if (!prompt.trim()) { throw new Error('No prompt provided for Gemini CLI execution'); } if (task.useSearch !== false) { const cachedResult = await this.searchCache.get(prompt, 'gemini'); if (cachedResult) { logger.debug('Cache hit for Gemini search', { promptLength: prompt.length, cacheAge: Date.now() - cachedResult.timestamp }); return { success: true, data: cachedResult.content, metadata: { layer: 'gemini', duration: Date.now() - startTime, cache_hit: true, model: task.model || this.DEFAULT_MODEL, } }; } } const result = await this.executeGeminiCLI(prompt, { model: task.model || this.DEFAULT_MODEL }); const duration = Date.now() - startTime; if (task.useSearch !== false && result.trim()) { await this.searchCache.set(prompt, { content: result, sources: this.extractSources(result), grounded: true, search_used: true, timestamp: Date.now() }, 'gemini', duration); } return { success: true, data: result, metadata: { layer: 'gemini', duration, cache_hit: false, model: task.model || this.DEFAULT_MODEL, search_enabled: task.useSearch !== false, } }; }, { operationName: 'execute-gemini-cli-task', layer: 'gemini', timeout: this.DEFAULT_TIMEOUT, }); } async executeWithGrounding(prompt, context) { const task = { type: 'grounding', prompt, useSearch: context.useSearch !== false, }; const result = await this.execute(task); return { content: result.data, sources: this.extractSources(result.data), grounded: true, search_used: context.useSearch !== false, }; } async processFiles(files, prompt) { const textFiles = files.filter(f => f.type === 'text' || f.path.endsWith('.txt') || f.path.endsWith('.md')); if (textFiles.length === 0) { throw new Error('Gemini CLI layer only supports text files. Use AI Studio layer for other file types.'); } logger.debug('Processing text files with Gemini CLI', { fileCount: textFiles.length, promptLength: prompt.length, }); const result = await this.execute({ type: 'multimodal', prompt, files: textFiles, }); return { content: result.data, success: true, files_processed: textFiles.map(f => f.path), processing_time: result.metadata?.duration || 0, workflow_used: 'analysis', layers_involved: ['gemini'], metadata: { total_duration: result.metadata?.duration || 0, ...result.metadata, }, }; } getCapabilities() { return [ 'real_time_search', 'web_grounding', 'current_information', 'text_processing', 'simple_analysis', 'search_integration', ]; } getCost(task) { return 0; } getEstimatedDuration(task) { const baseTime = 3000; if (task.useSearch !== false) { return baseTime + 5000; } return baseTime; } async executeGeminiCLI(prompt, options = {}) { if (this.geminiPath === 'gemini') { const resolvedPath = await this.findGeminiPath(); if (resolvedPath) { this.geminiPath = resolvedPath; } } const isWindows = process.platform === 'win32'; return new Promise((resolve, reject) => { let child; const args = ['-y']; if (options.model && options.model !== 'auto') { args.push('-m', options.model); } args.push(prompt); if (isWindows) { logger.debug('Executing Gemini CLI (Windows)', { command: this.geminiPath, argsCount: args.length, promptLength: prompt.length }); child = spawn(this.geminiPath, args, { stdio: ['ignore', 'pipe', 'pipe'], shell: true, windowsHide: true }); } else { logger.debug('Executing Gemini CLI (Unix)', { command: this.geminiPath, args, promptLength: prompt.length }); child = spawn(this.geminiPath, args, { stdio: ['ignore', 'pipe', 'pipe'] }); } let stdout = ''; let stderr = ''; child.stdout?.on('data', (data) => { stdout += data.toString(); }); child.stderr?.on('data', (data) => { const chunk = data.toString(); stderr += chunk; if (chunk.includes('429') || chunk.includes('quota') || chunk.includes('Quota exceeded')) { logger.warn('Quota limit detected in Gemini CLI service', { error: chunk.substring(0, 200) }); child.kill(); reject(new Error(`Gemini CLI quota exceeded (different from Gemini API quota). This is a Gemini CLI service limitation. Please wait or try using AI Studio layer instead. Details: ${chunk.substring(0, 300)}`)); return; } }); child.on('close', (code) => { logger.debug('Gemini CLI process closed', { code, stdoutLength: stdout.length, stderrLength: stderr.length, }); if (code === 0) { resolve(stdout.trim()); } else { if (stderr.includes('function response parts') || stderr.includes('function call parts')) { reject(new Error(`Gemini CLI authentication error. Try:\n1. gemini auth (OAuth - recommended)\n2. Check API key configuration\n\nOriginal error: ${stderr}`)); } else if (stderr.includes('UNAUTHENTICATED') || stderr.includes('API_KEY')) { reject(new Error(`Authentication failed. Try: gemini auth\n${stderr}`)); } else if (stderr.includes('quota') || stderr.includes('limit')) { reject(new Error(`Quota/rate limit exceeded: ${stderr}`)); } else { reject(new Error(`Gemini CLI failed (code ${code}): ${stderr || 'Unknown error'}`)); } } }); child.on('error', (err) => { reject(new Error(`Gemini CLI spawn error: ${err.message}`)); }); const timeout = setTimeout(() => { logger.warn('Gemini CLI timeout reached, terminating process', { timeoutMs: this.DEFAULT_TIMEOUT, promptLength: prompt.length, platform: process.platform }); if (isWindows) { child.kill(); } else { child.kill('SIGTERM'); } setTimeout(() => { if (!child.killed) { if (isWindows) { child.kill(); } else { child.kill('SIGKILL'); } } }, 2000); reject(new Error(`Gemini CLI timeout after ${this.DEFAULT_TIMEOUT / 1000}s. This may be due to quota limits or network issues.`)); }, this.DEFAULT_TIMEOUT); child.on('close', () => clearTimeout(timeout)); }); } extractPrompt(task) { return task.prompt || task.request || task.input || ''; } async findGeminiPath() { const { execSync } = await import('child_process'); const isWindows = process.platform === 'win32'; if (process.env.GEMINI_CLI_PATH) { try { execSync(`"${process.env.GEMINI_CLI_PATH}" --version`, { stdio: 'ignore', timeout: 5000 }); logger.debug('Found Gemini CLI from GEMINI_CLI_PATH', { path: process.env.GEMINI_CLI_PATH }); return process.env.GEMINI_CLI_PATH; } catch { logger.debug('GEMINI_CLI_PATH set but command failed', { path: process.env.GEMINI_CLI_PATH }); } } if (isWindows) { try { const result = execSync('where gemini 2>nul', { encoding: 'utf8', timeout: 5000 }); const firstPath = result.split('\n')[0]?.trim(); if (firstPath) { logger.debug('Found Gemini CLI via where command', { path: firstPath }); return firstPath; } } catch { logger.debug('where gemini command failed, trying fallback paths'); } } const possiblePaths = isWindows ? [ 'gemini', 'gemini.cmd', join(process.env.APPDATA || '', 'npm', 'gemini.cmd'), join(process.env.LOCALAPPDATA || '', 'npm', 'gemini.cmd'), join(process.env.USERPROFILE || '', 'AppData', 'Roaming', 'npm', 'gemini.cmd'), ...(process.env.NVM_HOME ? [ join(process.env.NVM_HOME, process.version, 'gemini.cmd'), join(process.env.NVM_HOME, process.version.replace('v', ''), 'gemini.cmd'), ] : []), join(process.env.ProgramFiles || '', 'nodejs', 'gemini.cmd'), join(process.env['ProgramFiles(x86)'] || '', 'nodejs', 'gemini.cmd'), ] : [ 'gemini', '/usr/local/bin/gemini', '/opt/homebrew/bin/gemini', join(process.env.HOME || '', '.nvm', 'versions', 'node', process.version, 'bin', 'gemini'), ]; for (const geminiPath of possiblePaths) { try { const cmd = isWindows ? `"${geminiPath}" --version` : `${geminiPath} --version`; execSync(cmd, { stdio: 'ignore', timeout: 5000 }); logger.debug('Found Gemini CLI at', { path: geminiPath }); return geminiPath; } catch { continue; } } logger.warn('Gemini CLI not found in any known location', { platform: process.platform, checkedPaths: possiblePaths }); return undefined; } extractSources(output) { const sources = []; const urlPattern = /https?:\/\/[^\s]+/g; const urls = output.match(urlPattern); if (urls) { sources.push(...urls); } const sourcePattern = /Source: (.+?)(?:\n|$)/g; let match; while ((match = sourcePattern.exec(output)) !== null) { sources.push(match[1]?.trim() || ''); } return [...new Set(sources)]; } getCacheStats() { return this.searchCache.getStats(); } async clearCache() { await this.searchCache.clear(); } async translateToEnglish(text, sourceLang) { const languageNames = { ja: 'Japanese', ko: 'Korean', zh: 'Chinese', fr: 'French', de: 'German', es: 'Spanish', ru: 'Russian', ar: 'Arabic', hi: 'Hindi', th: 'Thai' }; const languageName = languageNames[sourceLang] || sourceLang; const translationPrompt = `Translate to English for image generation: ${text}`; logger.info(`Translating ${languageName} prompt to English using Gemini CLI`, { originalText: text, sourceLang, languageName }); try { const result = await this.execute({ type: 'translation', prompt: translationPrompt, useSearch: false, model: 'gemini-2.5-pro' }); if (!result.success || !result.data) { throw new Error('Translation failed: No result returned'); } const translatedText = result.data.trim(); logger.info('Translation completed successfully', { originalText: text, translatedText, sourceLang, duration: result.metadata?.duration || 0 }); return translatedText; } catch (error) { logger.error('Translation failed, using original text', { error: error instanceof Error ? error.message : String(error), originalText: text, sourceLang }); return text; } } }