UNPKG

claudegem

Version:

Seamless integration of Claude Code CLI + Gemini CLI for context-aware code reasoning

397 lines (333 loc) 14.6 kB
import { exec, spawn } from 'child_process'; import { promisify } from 'util'; import chalk from 'chalk'; import { homedir } from 'os'; import { join } from 'path'; import { createWriteStream, createReadStream } from 'fs'; import { pipeline } from 'stream/promises'; const execAsync = promisify(exec); const HOME = homedir(); // Timeout for commands (30 seconds) const COMMAND_TIMEOUT = 30000; class Orchestrator { constructor() { this.claudeContext = ''; this.geminiContext = ''; this.sessionActive = false; this.claudeProcess = null; } async runCommand(cmd, options = {}) { const defaultOptions = { timeout: COMMAND_TIMEOUT, maxBuffer: 1024 * 1024 * 10, // 10MB buffer env: process.env, shell: '/bin/zsh' }; try { // Use login shell to ensure all profile settings are loaded const wrappedCmd = `/bin/zsh -l -c '${cmd.replace(/'/g, "'\\''")}'`; console.log(chalk.blue('Executing:', wrappedCmd)); const { stdout, stderr } = await execAsync(wrappedCmd, { ...defaultOptions, ...options }); if (stderr && !stderr.includes('seatbelt')) { console.error(chalk.yellow('Command stderr:', stderr)); } return stdout; } catch (error) { console.error(chalk.red('Error running command:')); console.error(chalk.red('Error message:', error.message)); if (error.stderr && !error.stderr.includes('seatbelt')) { console.error(chalk.red('Error stderr:', error.stderr)); } throw error; } } async runClaudeCommand(query, claudeFlags = '') { try { console.log(chalk.blue('Running Claude command...')); // Parse Claude flags to determine if we need interactive mode const isInteractive = claudeFlags.includes('--interactive') || claudeFlags.includes('-i'); const needsFileInput = !isInteractive; if (needsFileInput) { // For non-interactive modes, use file input const tmpFile = `/tmp/claude_query_${Date.now()}.txt`; await this.runCommand(`printf '%s' ${JSON.stringify(query)} > ${JSON.stringify(tmpFile)}`); try { // Build Claude command with custom flags or default to --print const baseFlags = claudeFlags || '--print'; const stdout = await this.runCommand(`cat ${JSON.stringify(tmpFile)} | claude ${baseFlags}`); this.claudeContext = stdout; return stdout; } finally { // Clean up the temporary file await this.runCommand(`rm ${JSON.stringify(tmpFile)}`); } } else { // For interactive modes, run Claude directly with the query console.log(chalk.yellow('Interactive mode detected - running Claude directly')); const stdout = await this.runCommand(`echo ${JSON.stringify(query)} | claude ${claudeFlags}`); this.claudeContext = stdout; return stdout; } } catch (error) { if (error.code === 'ETIMEDOUT') { throw new Error('Claude command timed out after 30 seconds'); } throw new Error(`Claude CLI error: ${error.message}`); } } async runLlamaCommand(query) { try { console.log(chalk.yellow('Running Llama fallback...')); const tmpFile = `/tmp/llama_query_${Date.now()}.txt`; await this.runCommand(`echo ${JSON.stringify(query)} > ${JSON.stringify(tmpFile)}`); try { // Try ollama first const stdout = await this.runCommand(`ollama run llama3.2 "$(cat ${JSON.stringify(tmpFile)})"`); return stdout; } catch (ollamaError) { // Try llama.cpp as backup const stdout = await this.runCommand(`llama "$(cat ${JSON.stringify(tmpFile)})"`); return stdout; } finally { await this.runCommand(`rm ${JSON.stringify(tmpFile)}`); } } catch (error) { throw new Error(`Llama error: ${error.message}`); } } async runQwenCommand(query) { try { console.log(chalk.cyan('Running Qwen Coder fallback...')); const tmpFile = `/tmp/qwen_query_${Date.now()}.txt`; await this.runCommand(`echo ${JSON.stringify(query)} > ${JSON.stringify(tmpFile)}`); try { // Try ollama with qwen coder const stdout = await this.runCommand(`ollama run qwen2.5-coder "$(cat ${JSON.stringify(tmpFile)})"`); return stdout; } catch (ollamaError) { // Try other qwen implementations const stdout = await this.runCommand(`qwen-coder "$(cat ${JSON.stringify(tmpFile)})"`); return stdout; } finally { await this.runCommand(`rm ${JSON.stringify(tmpFile)}`); } } catch (error) { throw new Error(`Qwen Coder error: ${error.message}`); } } async runGeminiCommand(query, geminiModel = 'gemini-2.5-pro') { try { console.log(chalk.blue('Running Gemini command...')); // Create a temporary file for the query const tmpFile = `/tmp/gemini_query_${Date.now()}.txt`; await this.runCommand(`echo ${JSON.stringify(query)} > ${JSON.stringify(tmpFile)}`); try { // Run Gemini with specified model const stdout = await this.runCommand(`gemini --model ${geminiModel} -y -a -p "$(cat ${JSON.stringify(tmpFile)})"`); if (!stdout) { throw new Error('No output received from Gemini CLI'); } this.geminiContext = stdout; return stdout; } finally { // Clean up the temporary file await this.runCommand(`rm ${JSON.stringify(tmpFile)}`); } } catch (error) { if (error.code === 'ETIMEDOUT') { throw new Error('Gemini command timed out after 30 seconds'); } const errorMsg = error.message; const isQuotaError = errorMsg.includes('429') || errorMsg.includes('quota exceeded') || errorMsg.includes('RESOURCE_EXHAUSTED') || errorMsg.includes('rateLimitExceeded'); if (isQuotaError) { console.log(chalk.red('🚨 GEMINI API QUOTA EXHAUSTED - Switching to free alternatives...')); // Try Llama first try { const llamaResult = await this.runLlamaCommand(query); console.log(chalk.yellow('✅ Using Llama as fallback due to Gemini quota exhaustion')); this.geminiContext = `⚠️ Gemini API quota exhausted - Using Llama fallback:\n\n${llamaResult}`; return this.geminiContext; } catch (llamaError) { console.log(chalk.yellow('Llama fallback failed, trying Qwen Coder...')); // Try Qwen Coder as final fallback try { const qwenResult = await this.runQwenCommand(query); console.log(chalk.cyan('✅ Using Qwen Coder as fallback due to Gemini quota exhaustion')); this.geminiContext = `⚠️ Gemini API quota exhausted, Llama unavailable - Using Qwen Coder fallback:\n\n${qwenResult}`; return this.geminiContext; } catch (qwenError) { console.log(chalk.red('All AI models failed')); const fallbackMsg = `🚨 GEMINI API QUOTA EXHAUSTED and no fallback models available.\n\nGemini Error: ${errorMsg}\nLlama Error: ${llamaError.message}\nQwen Error: ${qwenError.message}\n\nPlease wait for Gemini quota reset or install Ollama with llama3.2 or qwen2.5-coder models.`; this.geminiContext = fallbackMsg; return fallbackMsg; } } } throw new Error(`Gemini CLI error: ${error.message}`); } } async verifyDependencies() { try { console.log(chalk.blue('Verifying Claude CLI...')); await this.runCommand('claude --version', { timeout: 5000 }); console.log(chalk.green('Claude CLI found')); console.log(chalk.blue('Verifying Gemini CLI...')); await this.runCommand('gemini --version', { timeout: 5000 }); console.log(chalk.green('Gemini CLI found')); return true; } catch (error) { console.error(chalk.red('Dependency check failed:'), error.message); throw new Error('Required CLIs not found. Please ensure both Claude and Gemini CLIs are installed and in your PATH.'); } } async delegateCommandToGemini(command, geminiModel = 'gemini-2.5-pro') { try { console.log(chalk.blue(`Delegating command to Gemini: ${command}`)); const prompt = [ 'You are a command execution assistant. Execute the following command and return the result:', '', `Command: ${command}`, '', 'Please execute this command and provide the output. If the command cannot be executed, explain why.', 'Return only the command output or error message, no additional explanation.' ].join('\n'); const result = await this.runGeminiCommand(prompt, geminiModel); console.log(chalk.green('Command executed by Gemini')); return result; } catch (error) { console.error(chalk.red('Error delegating command to Gemini:'), error.message); throw error; } } async startInteractiveSession(query, config = {}) { try { const { claudeFlags = '', skipGemini = false, geminiModel = 'gemini-2.5-pro' } = config; // First verify dependencies await this.verifyDependencies(); console.log(chalk.blue('Starting interactive Claude Code session with Gemini delegation...')); // Check if we should use interactive mode const isInteractive = claudeFlags.includes('--interactive') || claudeFlags.includes('-i') || !claudeFlags.includes('--print'); if (!isInteractive) { // For non-interactive mode, fall back to regular processing return await this.process(query, config); } // Start Claude in interactive mode const claudeArgs = ['claude']; if (claudeFlags) { claudeArgs.push(...claudeFlags.split(' ').filter(Boolean)); } console.log(chalk.blue(`Starting Claude with: ${claudeArgs.join(' ')}`)); const claudeProcess = spawn(claudeArgs[0], claudeArgs.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], shell: true }); this.claudeProcess = claudeProcess; this.sessionActive = true; // Handle Claude output claudeProcess.stdout.on('data', (data) => { const output = data.toString(); console.log(output); // Check if Claude is trying to execute a command // This is a simplified approach - you'd need more sophisticated parsing if (output.includes('Executing:') || output.includes('Running:')) { // Extract command and delegate to Gemini // This is a placeholder - needs proper command extraction const commandMatch = output.match(/Executing:\s*(.+)/); if (commandMatch) { const command = commandMatch[1]; this.delegateCommandToGemini(command, geminiModel) .then(result => { // Send result back to Claude claudeProcess.stdin.write(`Command result: ${result}\n`); }) .catch(error => { claudeProcess.stdin.write(`Command error: ${error.message}\n`); }); } } }); claudeProcess.stderr.on('data', (data) => { console.error(chalk.red(data.toString())); }); claudeProcess.on('close', (code) => { console.log(chalk.blue(`Claude session ended with code ${code}`)); this.sessionActive = false; this.claudeProcess = null; }); // Send initial query claudeProcess.stdin.write(`${query}\n`); return new Promise((resolve, reject) => { claudeProcess.on('close', (code) => { if (code === 0) { resolve('Interactive session completed'); } else { reject(new Error(`Claude session failed with code ${code}`)); } }); }); } catch (error) { console.error(chalk.red('Error in interactive session:')); console.error(error); throw error; } } async process(query, config = {}) { try { const { claudeFlags = '', skipGemini = false, geminiModel = 'gemini-2.5-pro' } = config; // Check if this should be an interactive session const isInteractive = claudeFlags.includes('--interactive') || claudeFlags.includes('-i'); if (isInteractive) { return await this.startInteractiveSession(query, config); } // First verify dependencies await this.verifyDependencies(); let enhancedQuery = query; if (!skipGemini) { // Get context from Gemini console.log(chalk.blue('Getting context from Gemini...')); const geminiResult = await this.runGeminiCommand(query, geminiModel); console.log(chalk.green('Gemini context received')); // Clean up Gemini output - extract just the file list const cleanGeminiResult = geminiResult .split('\n') .filter(line => line.includes('.js') && !line.includes('node_modules')) .filter(line => !line.includes('Flushing log events')) .map(line => line.trim()) .filter(Boolean) .join('\n'); if (!cleanGeminiResult) { console.log(chalk.yellow('No relevant files found in Gemini output, using original query')); } // Enhance query with Gemini context if available if (cleanGeminiResult) { enhancedQuery = [ 'Context from Gemini:', cleanGeminiResult, '', 'Original query: ' + query, '', 'Please analyze the above context and answer the query.' ].join('\n'); } } else { console.log(chalk.yellow('Skipping Gemini context gathering as requested')); } // Pass the query to Claude console.log(chalk.blue('Processing with Claude...')); const claudeResult = await this.runClaudeCommand(enhancedQuery, claudeFlags); console.log(chalk.green('Claude processing complete')); return claudeResult; } catch (error) { console.error(chalk.red('Error in processing:')); console.error(error); throw error; } } } export default Orchestrator;