claude-gemini
Version:
Global CLI tool for Claude-Gemini integration across projects
372 lines ⢠17.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.sync = sync;
const chalk_1 = __importDefault(require("chalk"));
const ora_1 = __importDefault(require("ora"));
const child_process_1 = require("child_process");
const utils_1 = require("../utils");
const config_1 = require("../config");
const path_1 = __importDefault(require("path"));
async function sync(query, options) {
// Validate input
if (!query || typeof query !== 'string') {
console.error(chalk_1.default.red('Error: Invalid query provided. Please provide a valid query string.'));
process.exit(1);
}
const config = await (0, config_1.loadConfig)();
const geminiPath = await (0, utils_1.findGeminiPath)();
if (!geminiPath) {
console.error(chalk_1.default.red('Error: Gemini CLI not found. Please ensure it is installed.'));
console.error(chalk_1.default.yellow('\nInstall with: npm install -g gemini'));
process.exit(1);
}
// Attempt to use code2prompt for codebase analysis by default
let processedQuery = query;
let tempPromptFile = null;
let usedCode2Prompt = false;
// Debug logging
if (process.env.DEBUG || process.env.CG_DEBUG) {
console.log(chalk_1.default.gray(`[DEBUG] options.useCode2prompt: ${options.useCode2prompt}`));
console.log(chalk_1.default.gray(`[DEBUG] hasCodebaseContent: ${await hasCodebaseContent(query)}`));
}
if (options.useCode2prompt && await hasCodebaseContent(query)) {
try {
const code2promptResult = await processWithCode2Prompt(query, options);
processedQuery = code2promptResult.processedQuery;
tempPromptFile = code2promptResult.tempFile;
usedCode2Prompt = true;
if (options.format !== false) {
console.log(chalk_1.default.cyan('š Using code2prompt for enhanced codebase analysis...'));
if (code2promptResult.tokenCount) {
console.log(chalk_1.default.gray(`š Code context: ${code2promptResult.tokenCount} tokens`));
}
}
}
catch (error) {
if (options.format !== false) {
console.warn(chalk_1.default.yellow(`ā ļø code2prompt unavailable: ${error.message}`));
console.warn(chalk_1.default.gray('Falling back to standard path processing...'));
}
// Fall through to legacy processing
}
}
// Convert relative paths to absolute (only if not using code2prompt)
const convertedQuery = usedCode2Prompt ? processedQuery : (0, utils_1.convertPaths)(query, process.cwd());
// Validate the converted query
if (!convertedQuery) {
console.error(chalk_1.default.red('Error: Failed to process the query. Please check your input.'));
process.exit(1);
}
// Validate paths exist (but only warn, don't block)
const pathValidation = (0, utils_1.validatePaths)(convertedQuery);
if (!pathValidation.valid && pathValidation.warnings.length > 0) {
console.warn(chalk_1.default.yellow('\nā ļø Path warnings:'));
pathValidation.warnings.forEach(warning => {
console.warn(chalk_1.default.gray(` - ${warning}`));
});
console.warn(chalk_1.default.cyan('\nTip: Use @./ for current directory or check that paths exist with ls\n'));
}
// Show Claude instructions
if (options.format !== false) {
console.log(chalk_1.default.yellow('\n# IMPORTANT: Gemini Analysis in Progress\n'));
console.log('Streaming results from Gemini in real-time. Please wait for completion.\n');
console.log(chalk_1.default.cyan('ā'.repeat(80)));
}
const spinner = (0, ora_1.default)({
text: 'Connecting to Gemini...',
spinner: 'dots',
isEnabled: options.format !== false
}).start();
const startTime = Date.now();
const timeout = parseInt(options.timeout || config.timeout.toString() || '60') * 1000;
// Model fallback chain - prioritize Flash models to avoid quota issues
const modelFallbackChain = [
options.model || config.model || 'gemini-2.5-flash', // Default to 2.5-flash or user specified
'gemini-2.5-flash', // New frontier flash model (primary)
'gemini-2.0-flash-exp', // Fast experimental model
'gemini-1.5-flash', // Stable flash model
'gemini-1.5-flash-8b' // Lightweight model
].filter((m, i, arr) => arr.indexOf(m) === i); // Remove duplicates
let lastError;
for (let i = 0; i < modelFallbackChain.length; i++) {
const currentModel = modelFallbackChain[i];
if (i > 0 && options.format !== false) {
spinner.stop();
console.log(chalk_1.default.yellow(`\nā” Retrying with model: ${currentModel || 'auto-select'}\n`));
spinner.start();
}
try {
const result = await runGeminiWithTimeout(geminiPath, convertedQuery, currentModel, timeout, (elapsed) => {
if (elapsed === -1) {
// Clear spinner when streaming starts
spinner.stop();
spinner.clear();
if (options.format !== false) {
console.log(chalk_1.default.green('\nā¶ Streaming Gemini response:\n'));
}
}
else {
const progress = Math.floor((elapsed / timeout) * 100);
spinner.text = `Waiting for Gemini... ${progress}% (${Math.floor(elapsed / 1000)}s/${timeout / 1000}s)`;
}
}, options);
if (options.format !== false) {
// Don't show spinner success if we were streaming
console.log(chalk_1.default.cyan('\n' + 'ā'.repeat(80)));
console.log(chalk_1.default.green('\nā
Analysis complete!\n'));
console.log(chalk_1.default.yellow('Results have been streamed above. I can now see and use them.'));
}
else {
// For non-formatted output, just print the result
console.log(result);
}
return; // Success, exit the function
}
catch (error) {
lastError = error;
// Check if it's a quota error and we have more models to try
if ((error.toString().includes('429') || error.toString().includes('Quota exceeded'))
&& i < modelFallbackChain.length - 1) {
continue; // Try next model
}
// Otherwise, break and handle the error
break;
}
}
// If we get here, all attempts failed
spinner.fail(chalk_1.default.red('Analysis failed'));
console.error(chalk_1.default.red(`Error: ${lastError}`));
// Provide helpful guidance for common errors
if (lastError.toString().includes('429') || lastError.toString().includes('Quota exceeded')) {
console.error(chalk_1.default.yellow('\nQuota exceeded. Gemini should have auto-switched models.'));
console.error(chalk_1.default.cyan('If it didn\'t, try:'));
console.error(chalk_1.default.gray('1. Set up a Gemini API key for higher quotas'));
console.error(chalk_1.default.gray('2. Wait for daily quota reset'));
}
else if (lastError.toString().includes('400') || lastError.toString().includes('Bad Request') || lastError.toString().includes('invalid argument')) {
console.error(chalk_1.default.yellow('\nBad Request Error. Common causes:'));
console.error(chalk_1.default.gray('1. Non-existent file paths (e.g., @app/, @lib/ when these don\'t exist)'));
console.error(chalk_1.default.gray('2. Incorrectly formatted query'));
console.error(chalk_1.default.gray('3. Special characters that need escaping'));
console.error(chalk_1.default.cyan('\nTips:'));
console.error(chalk_1.default.gray('1. Use @src/ or @./ for the current directory'));
console.error(chalk_1.default.gray('2. Ensure paths exist: ls -la'));
console.error(chalk_1.default.gray('3. Try simpler queries first'));
console.error(chalk_1.default.gray('4. Enable debug: CG_DEBUG=1 cg "your query"'));
}
else if (lastError.toString().includes('Timeout')) {
console.error(chalk_1.default.yellow('\nThe Gemini CLI is not responding. This could be due to:'));
console.error(chalk_1.default.gray('1. Network connectivity issues'));
console.error(chalk_1.default.gray('2. Gemini API server problems'));
console.error(chalk_1.default.gray('3. Authentication issues'));
console.error(chalk_1.default.cyan('\nTroubleshooting steps:'));
console.error(chalk_1.default.gray('1. Try running gemini directly: gemini -p "test"'));
console.error(chalk_1.default.gray('2. Check if you\'re logged in: gemini auth status'));
console.error(chalk_1.default.gray('3. Enable debug mode: CG_DEBUG=1 cg "@package.json test"'));
}
process.exit(1);
}
async function hasCodebaseContent(query) {
// Check if the query contains any @path references or file/directory indicators
const { paths } = (0, utils_1.extractPathsFromQuery)(query);
// Always try code2prompt if there are any paths mentioned
if (paths.length > 0) {
// Check if code2prompt is available
const code2promptPath = await (0, utils_1.findCode2PromptPath)();
return code2promptPath !== null;
}
// Also use code2prompt for codebase analysis keywords even without explicit paths
const analysisKeywords = [
'analyze', 'architecture', 'structure', 'codebase', 'project', 'code',
'patterns', 'security', 'audit', 'review', 'overview', 'summary', 'files'
];
const hasAnalysisKeywords = analysisKeywords.some(keyword => query.toLowerCase().includes(keyword));
if (hasAnalysisKeywords) {
const code2promptPath = await (0, utils_1.findCode2PromptPath)();
return code2promptPath !== null;
}
return false;
}
async function processWithCode2Prompt(query, options) {
const { paths, cleanQuery } = (0, utils_1.extractPathsFromQuery)(query);
// If no explicit paths, use current directory for codebase analysis
let targetPaths = paths;
if (paths.length === 0) {
targetPaths = ['.'];
}
// Find the primary path (first directory or current dir if only files)
let primaryPath = process.cwd();
const dirPaths = targetPaths.filter(p => p.endsWith('/') || !p.includes('.'));
if (dirPaths.length > 0) {
primaryPath = path_1.default.resolve(dirPaths[0]);
}
else if (targetPaths.length > 0 && targetPaths[0] !== '.') {
primaryPath = path_1.default.dirname(path_1.default.resolve(targetPaths[0]));
}
// Configure code2prompt options
const code2promptOptions = {
include: options.includePatterns || [],
exclude: options.excludePatterns || [
'node_modules/**',
'.git/**',
'dist/**',
'build/**',
'*.log',
'.env*'
],
lineNumbers: options.lineNumbers || false,
template: options.template,
outputFormat: 'json',
tokens: true,
encoding: 'cl100k'
};
// If specific files are mentioned, include them
const fileIncludes = targetPaths
.filter(p => p.includes('.') && !p.endsWith('/') && p !== '.')
.map(p => path_1.default.basename(p))
.filter(f => f.length > 0);
if (fileIncludes.length > 0) {
code2promptOptions.include = [...code2promptOptions.include, ...fileIncludes];
}
// Run code2prompt
const result = await (0, utils_1.runCode2Prompt)(primaryPath, code2promptOptions);
// Create a comprehensive prompt and pass it directly
const enhancedPrompt = `${cleanQuery}\n\n# Codebase Context\n\n${result.output}`;
// Return the enhanced prompt directly instead of using a temp file
return {
processedQuery: enhancedPrompt,
tempFile: null, // No temp file needed
tokenCount: result.tokenCount
};
}
function runGeminiWithTimeout(geminiPath, query, model, timeout, onProgress, options) {
return new Promise((resolve, reject) => {
// Add -y flag to accept all actions automatically (non-interactive)
const args = ['-y'];
// Only add model if specified and not empty
if (model && model.trim() !== '') {
args.push('-m', model);
}
args.push('-p', query);
// Add debug logging
if (process.env.DEBUG || process.env.CG_DEBUG) {
console.error(chalk_1.default.gray(`[DEBUG] Running: ${geminiPath} ${args.join(' ')}`));
console.error(chalk_1.default.gray(`[DEBUG] Working directory: ${process.cwd()}`));
}
const gemini = (0, child_process_1.spawn)(geminiPath, args, {
env: { ...process.env },
shell: false,
stdio: ['pipe', 'pipe', 'pipe'],
// Kill the process group on timeout
detached: process.platform !== 'win32'
});
// Close stdin immediately since we're not sending any input
gemini.stdin.end();
let output = '';
let error = '';
let progressInterval;
let hasStartedStreaming = false;
const startTime = Date.now();
progressInterval = setInterval(() => {
if (!hasStartedStreaming) {
const elapsed = Date.now() - startTime;
onProgress(elapsed);
}
}, 2000);
const timeoutHandle = setTimeout(() => {
clearInterval(progressInterval);
// Try graceful shutdown first
gemini.kill('SIGTERM');
// Force kill after 5 seconds if still running
setTimeout(() => {
try {
// Kill the entire process group
if (process.platform !== 'win32' && gemini.pid) {
process.kill(-gemini.pid, 'SIGKILL');
}
else {
gemini.kill('SIGKILL');
}
}
catch (e) {
// Process might already be dead
}
}, 5000);
reject(new Error(`Timeout after ${timeout / 1000} seconds. The Gemini CLI appears to be hanging.`));
}, timeout);
gemini.stdout.on('data', (data) => {
const chunk = data.toString();
output += chunk;
// First data received - clear spinner and start streaming
if (!hasStartedStreaming) {
hasStartedStreaming = true;
clearInterval(progressInterval);
onProgress(-1); // Signal to clear spinner
}
// Stream output in real-time so Claude sees progress
if (options.format !== false) {
// Remove ANSI color codes for cleaner output
const cleanChunk = chunk.replace(/\x1b\[[0-9;]*m/g, '');
process.stdout.write(cleanChunk);
}
});
gemini.stderr.on('data', (data) => {
const errorData = data.toString();
error += errorData;
// Gemini might output some info to stderr that's not errors
if (errorData.includes('[dotenv@')) {
// This is just dotenv info, not an error
return;
}
// Stream stderr info that might be useful (like model switching)
if (options.format !== false) {
if (errorData.includes('Slow response times detected') ||
errorData.includes('switching from') ||
errorData.includes('Automatically switching')) {
// Clear any spinner first
if (!hasStartedStreaming) {
hasStartedStreaming = true;
clearInterval(progressInterval);
onProgress(-1);
}
console.log(chalk_1.default.yellow('\nā” ' + errorData.trim() + '\n'));
}
}
// Debug mode: show all stderr
if (process.env.DEBUG || process.env.CG_DEBUG) {
console.error(chalk_1.default.gray(`[DEBUG] stderr: ${errorData}`));
}
});
gemini.on('close', (code) => {
clearTimeout(timeoutHandle);
clearInterval(progressInterval);
if (code === 0) {
resolve(output);
}
else {
// Extract the actual error message if it's a quota error
const quotaMatch = error.match(/Quota exceeded.*?\./g);
if (quotaMatch) {
reject(new Error(`429: ${quotaMatch[0]}`));
}
else if (error.includes('400') || error.includes('Bad Request') || error.includes('invalid argument')) {
// Handle 400 errors specifically
const errorMsg = `Invalid request: ${error}\n\nThis often happens when:\n1. File paths don't exist\n2. Query format is incorrect\n3. Special characters aren't properly escaped`;
reject(new Error(errorMsg));
}
else {
reject(new Error(`Gemini exited with code ${code}: ${error}`));
}
}
});
gemini.on('error', (err) => {
clearTimeout(timeoutHandle);
clearInterval(progressInterval);
reject(err);
});
});
}
//# sourceMappingURL=sync.js.map