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
JavaScript
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;
}
}
}