claudegem
Version:
Seamless integration of Claude Code CLI + Gemini CLI for context-aware code reasoning
397 lines (333 loc) • 14.6 kB
JavaScript
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;