UNPKG

@deep-assistant/hive-mind

Version:

AI-powered issue solver and hive mind for collaborative problem solving

931 lines (816 loc) • 34.8 kB
#!/usr/bin/env node // Claude CLI-related utility functions // Check if use is already defined (when imported from solve.mjs) // If not, fetch it (when running standalone) if (typeof globalThis.use === 'undefined') { globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use; } const { $ } = await use('command-stream'); const fs = (await use('fs')).promises; const path = (await use('path')).default; // Import log from general lib import { log, cleanErrorMessage } from './lib.mjs'; import { reportError } from './sentry.lib.mjs'; // Function to validate Claude CLI connection with retry logic export const validateClaudeConnection = async (model = 'sonnet') => { // Retry configuration for API overload errors const maxRetries = 3; const baseDelay = 5000; // Start with 5 seconds let retryCount = 0; const attemptValidation = async () => { try { if (retryCount === 0) { await log('šŸ” Validating Claude CLI connection...'); } else { await log(`šŸ”„ Retry attempt ${retryCount}/${maxRetries} for Claude CLI validation...`); } // First try a quick validation approach try { // Check if Claude CLI is installed and get version const versionResult = await $`timeout 10 claude --version`; if (versionResult.code === 0) { const version = versionResult.stdout?.toString().trim(); if (retryCount === 0) { await log(`šŸ“¦ Claude CLI version: ${version}`); } } } catch (versionError) { // Version check failed, but we'll continue with the main validation if (retryCount === 0) { await log(`āš ļø Claude CLI version check failed (${versionError.code}), proceeding with connection test...`); } } let result; try { // Primary validation: use printf piping with specified model result = await $`printf hi | claude --model ${model} -p`; } catch (pipeError) { // If piping fails, fallback to the timeout approach as last resort await log(`āš ļø Pipe validation failed (${pipeError.code}), trying timeout approach...`); try { result = await $`timeout 60 claude --model ${model} -p hi`; } catch (timeoutError) { if (timeoutError.code === 124) { await log('āŒ Claude CLI timed out after 60 seconds', { level: 'error' }); await log(' šŸ’” This may indicate Claude CLI is taking too long to respond', { level: 'error' }); await log(` šŸ’” Try running 'claude --model ${model} -p hi' manually to verify it works`, { level: 'error' }); return false; } // Re-throw if it's not a timeout error throw timeoutError; } } // Check for common error patterns const stdout = result.stdout?.toString() || ''; const stderr = result.stderr?.toString() || ''; // Check for JSON errors in stdout or stderr const checkForJsonError = (text) => { try { // Look for JSON error patterns if (text.includes('"error"') && text.includes('"type"')) { const jsonMatch = text.match(/\{.*"error".*\}/); if (jsonMatch) { const errorObj = JSON.parse(jsonMatch[0]); return errorObj.error; } } } catch (e) { // Not valid JSON, continue with other checks if (global.verboseMode) { reportError(e, { context: 'claude_json_error_parse', level: 'debug' }); } } return null; }; const jsonError = checkForJsonError(stdout) || checkForJsonError(stderr); // Check for API overload error pattern const isOverloadError = (stdout.includes('API Error: 500') && stdout.includes('Overloaded')) || (stderr.includes('API Error: 500') && stderr.includes('Overloaded')) || (jsonError && jsonError.type === 'api_error' && jsonError.message === 'Overloaded'); // Handle overload errors with retry if (isOverloadError) { if (retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); await log(`āš ļø API overload error during validation. Retrying in ${delay / 1000} seconds...`, { level: 'warning' }); await new Promise(resolve => setTimeout(resolve, delay)); retryCount++; return await attemptValidation(); } else { await log(`āŒ API overload error persisted after ${maxRetries} retries during validation`, { level: 'error' }); await log(' The API appears to be heavily loaded. Please try again later.', { level: 'error' }); return false; } } // Use exitCode if code is undefined (Bun shell behavior) const exitCode = result.code ?? result.exitCode ?? 0; if (exitCode !== 0) { // Command failed if (jsonError) { await log(`āŒ Claude CLI authentication failed: ${jsonError.type} - ${jsonError.message}`, { level: 'error' }); } else { await log(`āŒ Claude CLI failed with exit code ${exitCode}`, { level: 'error' }); if (stderr) await log(` Error: ${stderr.trim()}`, { level: 'error' }); } if (stderr.includes('Please run /login') || (jsonError && jsonError.type === 'forbidden')) { await log(' šŸ’” Please run: claude login', { level: 'error' }); } return false; } // Check for error patterns in successful response if (jsonError) { // Check if this is an overload error even with exit code 0 if (jsonError.type === 'api_error' && jsonError.message === 'Overloaded') { if (retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); await log(`āš ļø API overload error in response. Retrying in ${delay / 1000} seconds...`, { level: 'warning' }); await new Promise(resolve => setTimeout(resolve, delay)); retryCount++; return await attemptValidation(); } else { await log(`āŒ API overload error persisted after ${maxRetries} retries`, { level: 'error' }); return false; } } await log(`āŒ Claude CLI returned error: ${jsonError.type} - ${jsonError.message}`, { level: 'error' }); if (jsonError.type === 'forbidden') { await log(' šŸ’” Please run: claude login', { level: 'error' }); } return false; } // Success - Claude responded (LLM responses are probabilistic, so any response is good) await log('āœ… Claude CLI connection validated successfully'); return true; } catch (error) { // Check if the error is an overload error const errorStr = error.message || error.toString(); if ((errorStr.includes('API Error: 500') && errorStr.includes('Overloaded')) || (errorStr.includes('api_error') && errorStr.includes('Overloaded'))) { if (retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); await log(`āš ļø API overload error during validation. Retrying in ${delay / 1000} seconds...`, { level: 'warning' }); await new Promise(resolve => setTimeout(resolve, delay)); retryCount++; return await attemptValidation(); } else { await log(`āŒ API overload error persisted after ${maxRetries} retries`, { level: 'error' }); return false; } } await log(`āŒ Failed to validate Claude CLI connection: ${error.message}`, { level: 'error' }); await log(' šŸ’” Make sure Claude CLI is installed and accessible', { level: 'error' }); return false; } }; // End of attemptValidation function // Start the validation with retry logic return await attemptValidation(); }; // Function to handle Claude runtime switching between Node.js and Bun export const handleClaudeRuntimeSwitch = async (argv) => { if (argv['force-claude-bun-run']) { await log('\nšŸ”§ Switching Claude runtime to bun...'); try { // Check if bun is available try { await $`which bun`; await log(' āœ… Bun runtime found'); } catch (bunError) { reportError(bunError, { context: 'claude.lib.mjs - bun availability check', level: 'error' }); await log('āŒ Bun runtime not found. Please install bun first: https://bun.sh/', { level: 'error' }); process.exit(1); } // Find Claude executable path const claudePathResult = await $`which claude`; const claudePath = claudePathResult.stdout.toString().trim(); if (!claudePath) { await log('āŒ Claude executable not found', { level: 'error' }); process.exit(1); } await log(` Claude path: ${claudePath}`); // Check if file is writable try { await fs.access(claudePath, fs.constants.W_OK); } catch (accessError) { reportError(accessError, { context: 'claude.lib.mjs - Claude executable write permission check (bun)', level: 'error' }); await log('āŒ Cannot write to Claude executable (permission denied)', { level: 'error' }); await log(' Try running with sudo or changing file permissions', { level: 'error' }); process.exit(1); } // Read current shebang const firstLine = await $`head -1 "${claudePath}"`; const currentShebang = firstLine.stdout.toString().trim(); await log(` Current shebang: ${currentShebang}`); if (currentShebang.includes('bun')) { await log(' āœ… Claude is already configured to use bun'); process.exit(0); } // Create backup const backupPath = `${claudePath}.nodejs-backup`; await $`cp "${claudePath}" "${backupPath}"`; await log(` šŸ“¦ Backup created: ${backupPath}`); // Read file content and replace shebang const content = await fs.readFile(claudePath, 'utf8'); const newContent = content.replace(/^#!.*node.*$/m, '#!/usr/bin/env bun'); if (content === newContent) { await log('āš ļø No Node.js shebang found to replace', { level: 'warning' }); await log(` Current shebang: ${currentShebang}`, { level: 'warning' }); process.exit(0); } await fs.writeFile(claudePath, newContent); await log(' āœ… Claude shebang updated to use bun'); await log(' šŸ”„ Claude will now run with bun runtime'); } catch (error) { await log(`āŒ Failed to switch Claude to bun: ${cleanErrorMessage(error)}`, { level: 'error' }); process.exit(1); } // Exit after switching runtime process.exit(0); } if (argv['force-claude-nodejs-run']) { await log('\nšŸ”§ Restoring Claude runtime to Node.js...'); try { // Check if Node.js is available try { await $`which node`; await log(' āœ… Node.js runtime found'); } catch (nodeError) { reportError(nodeError, { context: 'claude.lib.mjs - Node.js availability check', level: 'error' }); await log('āŒ Node.js runtime not found. Please install Node.js first', { level: 'error' }); process.exit(1); } // Find Claude executable path const claudePathResult = await $`which claude`; const claudePath = claudePathResult.stdout.toString().trim(); if (!claudePath) { await log('āŒ Claude executable not found', { level: 'error' }); process.exit(1); } await log(` Claude path: ${claudePath}`); // Check if file is writable try { await fs.access(claudePath, fs.constants.W_OK); } catch (accessError) { reportError(accessError, { context: 'claude.lib.mjs - Claude executable write permission check (nodejs)', level: 'error' }); await log('āŒ Cannot write to Claude executable (permission denied)', { level: 'error' }); await log(' Try running with sudo or changing file permissions', { level: 'error' }); process.exit(1); } // Read current shebang const firstLine = await $`head -1 "${claudePath}"`; const currentShebang = firstLine.stdout.toString().trim(); await log(` Current shebang: ${currentShebang}`); if (currentShebang.includes('node') && !currentShebang.includes('bun')) { await log(' āœ… Claude is already configured to use Node.js'); process.exit(0); } // Check if backup exists const backupPath = `${claudePath}.nodejs-backup`; try { await fs.access(backupPath); // Restore from backup await $`cp "${backupPath}" "${claudePath}"`; await log(` āœ… Restored Claude from backup: ${backupPath}`); } catch (backupError) { reportError(backupError, { context: 'claude_restore_backup', level: 'info' }); // No backup available, manually update shebang await log(' šŸ“ No backup found, manually updating shebang...'); const content = await fs.readFile(claudePath, 'utf8'); const newContent = content.replace(/^#!.*bun.*$/m, '#!/usr/bin/env node'); if (content === newContent) { await log('āš ļø No bun shebang found to replace', { level: 'warning' }); await log(` Current shebang: ${currentShebang}`, { level: 'warning' }); process.exit(0); } await fs.writeFile(claudePath, newContent); await log(' āœ… Claude shebang updated to use Node.js'); } await log(' šŸ”„ Claude will now run with Node.js runtime'); } catch (error) { await log(`āŒ Failed to restore Claude to Node.js: ${cleanErrorMessage(error)}`, { level: 'error' }); process.exit(1); } // Exit after restoring runtime process.exit(0); } }; /** * Execute Claude with all prompts and settings * This is the main entry point that handles all prompt building and execution * @param {Object} params - Parameters for Claude execution * @returns {Object} Result of the execution including success status and session info */ export const executeClaude = async (params) => { const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, setLogFile, getLogFile, formatAligned, getResourceSnapshot, claudePath, $ } = params; // Import prompt building functions from claude.prompts.lib.mjs const { buildUserPrompt, buildSystemPrompt } = await import('./claude.prompts.lib.mjs'); // Build the user prompt const prompt = buildUserPrompt({ issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv }); // Build the system prompt const systemPrompt = buildSystemPrompt({ owner, repo, issueNumber, issueUrl, prNumber, prUrl, branchName, tempDir, isContinueMode, forkedRepo, argv }); // Log prompt details in verbose mode if (argv.verbose) { await log('\nšŸ“ Final prompt structure:', { verbose: true }); await log(` Characters: ${prompt.length}`, { verbose: true }); await log(` System prompt characters: ${systemPrompt.length}`, { verbose: true }); if (feedbackLines && feedbackLines.length > 0) { await log(' Feedback info: Included', { verbose: true }); } // In dry-run mode, output the actual prompts for debugging if (argv.dryRun) { await log('\nšŸ“‹ User prompt content:', { verbose: true }); await log('---BEGIN USER PROMPT---', { verbose: true }); await log(prompt, { verbose: true }); await log('---END USER PROMPT---', { verbose: true }); await log('\nšŸ“‹ System prompt content:', { verbose: true }); await log('---BEGIN SYSTEM PROMPT---', { verbose: true }); await log(systemPrompt, { verbose: true }); await log('---END SYSTEM PROMPT---', { verbose: true }); } } // Escape prompts for shell usage const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/\$/g, '\\$'); const escapedSystemPrompt = systemPrompt.replace(/"/g, '\\"').replace(/\$/g, '\\$'); // Execute the Claude command return await executeClaudeCommand({ tempDir, branchName, prompt, systemPrompt, escapedPrompt, escapedSystemPrompt, argv, log, setLogFile, getLogFile, formatAligned, getResourceSnapshot, forkedRepo, feedbackLines, claudePath, $ }); }; export const executeClaudeCommand = async (params) => { const { tempDir, branchName, prompt, systemPrompt, escapedPrompt, escapedSystemPrompt, argv, log, setLogFile, getLogFile, formatAligned, getResourceSnapshot, forkedRepo, feedbackLines, claudePath, $ // Add command-stream $ to params } = params; // Retry configuration for API overload errors const maxRetries = 3; const baseDelay = 5000; // Start with 5 seconds let retryCount = 0; // Function to execute with retry logic const executeWithRetry = async () => { // Execute claude command from the cloned repository directory if (retryCount === 0) { await log(`\n${formatAligned('šŸ¤–', 'Executing Claude:', argv.model.toUpperCase())}`); } else { await log(`\n${formatAligned('šŸ”„', 'Retry attempt:', `${retryCount}/${maxRetries}`)}`); } if (argv.verbose) { // Output the actual model being used const modelName = argv.model === 'opus' ? 'opus' : 'sonnet'; await log(` Model: ${modelName}`, { verbose: true }); await log(` Working directory: ${tempDir}`, { verbose: true }); await log(` Branch: ${branchName}`, { verbose: true }); await log(` Prompt length: ${prompt.length} chars`, { verbose: true }); await log(` System prompt length: ${systemPrompt.length} chars`, { verbose: true }); if (feedbackLines && feedbackLines.length > 0) { await log(` Feedback info included: Yes (${feedbackLines.length} lines)`, { verbose: true }); } else { await log(' Feedback info included: No', { verbose: true }); } } // Take resource snapshot before execution const resourcesBefore = await getResourceSnapshot(); await log('šŸ“ˆ System resources before execution:', { verbose: true }); await log(` Memory: ${resourcesBefore.memory.split('\n')[1]}`, { verbose: true }); await log(` Load: ${resourcesBefore.load}`, { verbose: true }); // Use command-stream's async iteration for real-time streaming with file logging let commandFailed = false; let sessionId = null; let limitReached = false; let messageCount = 0; let toolUseCount = 0; let lastMessage = ''; let isOverloadError = false; // Build claude command with optional resume flag let execCommand; // Build claude command arguments let claudeArgs = `--output-format stream-json --verbose --dangerously-skip-permissions --model ${argv.model}`; if (argv.resume) { await log(`šŸ”„ Resuming from session: ${argv.resume}`); claudeArgs = `--resume ${argv.resume} ${claudeArgs}`; } claudeArgs += ` -p "${escapedPrompt}" --append-system-prompt "${escapedSystemPrompt}"`; // Build the full command for display (with jq for formatting as in v0.3.2) const fullCommand = `(cd "${tempDir}" && ${claudePath} ${claudeArgs} | jq -c .)`; // Print the actual raw command being executed await log(`\n${formatAligned('šŸ“', 'Raw command:', '')}`); await log(`${fullCommand}`); await log(''); // Output prompts in verbose mode for debugging if (argv.verbose) { await log('šŸ“‹ User prompt:', { verbose: true }); await log('---BEGIN USER PROMPT---', { verbose: true }); await log(prompt, { verbose: true }); await log('---END USER PROMPT---', { verbose: true }); await log('', { verbose: true }); await log('šŸ“‹ System prompt:', { verbose: true }); await log('---BEGIN SYSTEM PROMPT---', { verbose: true }); await log(systemPrompt, { verbose: true }); await log('---END SYSTEM PROMPT---', { verbose: true }); await log('', { verbose: true }); } try { if (argv.resume) { // When resuming, pass prompt directly with -p flag // Use simpler escaping - just escape double quotes const simpleEscapedPrompt = prompt.replace(/"/g, '\\"'); const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"'); execCommand = $({ cwd: tempDir, mirror: false })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${argv.model} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`; } else { // When not resuming, pass prompt via stdin // For system prompt, escape it properly for shell - just escape double quotes const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"'); execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${argv.model} --append-system-prompt "${simpleEscapedSystem}"`; } await log(`${formatAligned('šŸ“‹', 'Command details:', '')}`); await log(formatAligned('šŸ“‚', 'Working directory:', tempDir, 2)); await log(formatAligned('🌿', 'Branch:', branchName, 2)); await log(formatAligned('šŸ¤–', 'Model:', `Claude ${argv.model.toUpperCase()}`, 2)); if (argv.fork && forkedRepo) { await log(formatAligned('šŸ“', 'Fork:', forkedRepo, 2)); } await log(`\n${formatAligned('ā–¶ļø', 'Streaming output:', '')}\n`); // Use command-stream's async iteration for real-time streaming let exitCode = 0; for await (const chunk of execCommand.stream()) { if (chunk.type === 'stdout') { const output = chunk.data.toString(); // Process complete lines from stdout const lines = output.split('\n'); for (const line of lines) { if (!line.trim()) continue; try { const data = JSON.parse(line); // Output formatted JSON as in v0.3.2 await log(JSON.stringify(data, null, 2)); // Capture session ID from the first message if (!sessionId && data.session_id) { sessionId = data.session_id; await log(`šŸ“Œ Session ID: ${sessionId}`); // Try to rename log file to include session ID let sessionLogFile; try { const currentLogFile = getLogFile(); const logDir = path.dirname(currentLogFile); sessionLogFile = path.join(logDir, `${sessionId}.log`); // Use fs.promises to rename the file await fs.rename(currentLogFile, sessionLogFile); // Update the global log file reference setLogFile(sessionLogFile); await log(`šŸ“ Log renamed to: ${sessionLogFile}`); } catch (renameError) { reportError(renameError, { context: 'rename_session_log', sessionId, sessionLogFile, operation: 'rename_log_file' }); // If rename fails, keep original filename await log(`āš ļø Could not rename log file: ${renameError.message}`, { verbose: true }); } } // Track message and tool use counts if (data.type === 'message') { messageCount++; } else if (data.type === 'tool_use') { toolUseCount++; } // Store last message for error detection if (data.type === 'text' && data.text) { lastMessage = data.text; } else if (data.type === 'error') { lastMessage = data.error || JSON.stringify(data); } // Check for API overload error if (data.type === 'assistant' && data.message && data.message.content) { const content = Array.isArray(data.message.content) ? data.message.content : [data.message.content]; for (const item of content) { if (item.type === 'text' && item.text) { // Check for the specific error pattern from the issue if (item.text.includes('API Error: 500') && item.text.includes('api_error') && item.text.includes('Overloaded')) { isOverloadError = true; lastMessage = item.text; await log('āš ļø Detected API overload error', { verbose: true }); } } } } } catch (parseError) { reportError(parseError, { context: 'parse_claude_output', line, operation: 'parse_json_output' }); // Not JSON or parsing failed, output as-is if it's not empty if (line.trim() && !line.includes('node:internal')) { await log(line, { stream: 'raw' }); lastMessage = line; } } } } if (chunk.type === 'stderr') { const errorOutput = chunk.data.toString(); // Log stderr immediately if (errorOutput) { await log(errorOutput, { stream: 'stderr' }); } } else if (chunk.type === 'exit') { exitCode = chunk.code; if (chunk.code !== 0) { commandFailed = true; } // Don't break here - let the loop finish naturally to process all output } } // Check if this is an overload error that should be retried if ((commandFailed || isOverloadError) && (isOverloadError || (lastMessage.includes('API Error: 500') && lastMessage.includes('Overloaded')) || (lastMessage.includes('api_error') && lastMessage.includes('Overloaded')))) { if (retryCount < maxRetries) { // Calculate exponential backoff delay const delay = baseDelay * Math.pow(2, retryCount); await log(`\nāš ļø API overload error detected. Retrying in ${delay / 1000} seconds...`, { level: 'warning' }); await log(` Error: ${lastMessage.substring(0, 200)}`, { verbose: true }); // Wait before retrying await new Promise(resolve => setTimeout(resolve, delay)); // Increment retry count and retry retryCount++; return await executeWithRetry(); } else { await log(`\n\nāŒ API overload error persisted after ${maxRetries} retries`, { level: 'error' }); await log(' The API appears to be heavily loaded. Please try again later.', { level: 'error' }); return { success: false, sessionId, limitReached: false, messageCount, toolUseCount }; } } if (commandFailed) { // Check if we hit a rate limit if (lastMessage.includes('rate_limit_exceeded') || lastMessage.includes('You have exceeded your rate limit') || lastMessage.includes('rate limit')) { limitReached = true; await log('\n\nā³ Rate limit reached. The session can be resumed later.', { level: 'warning' }); if (sessionId) { await log(`šŸ“Œ Session ID for resuming: ${sessionId}`); await log('\nTo continue when the rate limit resets, run:'); await log(` ${process.argv[0]} ${process.argv[1]} --auto-continue ${argv.url}`); } } else if (lastMessage.includes('context_length_exceeded')) { await log('\n\nāŒ Context length exceeded. Try with a smaller issue or split the work.', { level: 'error' }); } else { await log(`\n\nāŒ Claude command failed with exit code ${exitCode}`, { level: 'error' }); if (sessionId && !argv.resume) { await log(`šŸ“Œ Session ID for resuming: ${sessionId}`); await log('\nTo resume this session, run:'); await log(` ${process.argv[0]} ${process.argv[1]} ${argv.url} --resume ${sessionId}`); } } } // Check if command failed if (commandFailed) { // Take resource snapshot after failure const resourcesAfter = await getResourceSnapshot(); await log('\nšŸ“ˆ System resources after execution:', { verbose: true }); await log(` Memory: ${resourcesAfter.memory.split('\n')[1]}`, { verbose: true }); await log(` Load: ${resourcesAfter.load}`, { verbose: true }); // If --attach-logs is enabled, ensure we attach failure logs if (argv.attachLogs && sessionId) { await log('\nšŸ“„ Attempting to attach failure logs to PR/Issue...'); // The attach logs logic will handle this in the catch block below } return { success: false, sessionId, limitReached, messageCount, toolUseCount }; } await log('\n\nāœ… Claude command completed'); await log(`šŸ“Š Total messages: ${messageCount}, Tool uses: ${toolUseCount}`); return { success: true, sessionId, limitReached, messageCount, toolUseCount }; } catch (error) { reportError(error, { context: 'execute_claude', command: params.command, claudePath: params.claudePath, operation: 'run_claude_command' }); // Check if this is an overload error in the exception const errorStr = error.message || error.toString(); if ((errorStr.includes('API Error: 500') && errorStr.includes('Overloaded')) || (errorStr.includes('api_error') && errorStr.includes('Overloaded'))) { if (retryCount < maxRetries) { // Calculate exponential backoff delay const delay = baseDelay * Math.pow(2, retryCount); await log(`\nāš ļø API overload error in exception. Retrying in ${delay / 1000} seconds...`, { level: 'warning' }); // Wait before retrying await new Promise(resolve => setTimeout(resolve, delay)); // Increment retry count and retry retryCount++; return await executeWithRetry(); } } await log(`\n\nāŒ Error executing Claude command: ${error.message}`, { level: 'error' }); return { success: false, sessionId, limitReached, messageCount, toolUseCount }; } }; // End of executeWithRetry function // Start the execution with retry logic return await executeWithRetry(); }; export const checkForUncommittedChanges = async (tempDir, owner, repo, branchName, $, log, autoCommit = false) => { // Check for uncommitted changes made by Claude await log('\nšŸ” Checking for uncommitted changes...'); try { // Check git status to see if there are any uncommitted changes const gitStatusResult = await $({ cwd: tempDir })`git status --porcelain 2>&1`; if (gitStatusResult.code === 0) { const statusOutput = gitStatusResult.stdout.toString().trim(); if (statusOutput) { await log('šŸ“ Found uncommitted changes'); await log('Changes:'); for (const line of statusOutput.split('\n')) { await log(` ${line}`); } if (autoCommit) { // Auto-commit the changes if option is enabled await log('šŸ’¾ Auto-committing changes (--auto-commit-uncommitted-changes is enabled)...'); const addResult = await $({ cwd: tempDir })`git add -A`; if (addResult.code === 0) { const commitMessage = 'Auto-commit: Changes made by Claude during problem-solving session'; const commitResult = await $({ cwd: tempDir })`git commit -m ${commitMessage}`; if (commitResult.code === 0) { await log('āœ… Changes committed successfully'); // Push the changes await log('šŸ“¤ Pushing changes to remote...'); const pushResult = await $({ cwd: tempDir })`git push origin ${branchName}`; if (pushResult.code === 0) { await log('āœ… Changes pushed successfully'); } else { await log(`āš ļø Warning: Could not push changes: ${pushResult.stderr?.toString().trim()}`, { level: 'warning' }); } } else { await log(`āš ļø Warning: Could not commit changes: ${commitResult.stderr?.toString().trim()}`, { level: 'warning' }); } } else { await log(`āš ļø Warning: Could not stage changes: ${addResult.stderr?.toString().trim()}`, { level: 'warning' }); } return false; // No restart needed when auto-commit is enabled } else { // When auto-commit is disabled, trigger auto-restart await log(''); await log('āš ļø IMPORTANT: Uncommitted changes detected!'); await log(' Claude made changes that were not committed.'); await log(''); await log('šŸ”„ AUTO-RESTART: Restarting Claude to handle uncommitted changes...'); await log(' Claude will review the changes and decide what to commit.'); await log(''); return true; // Return true to indicate restart is needed } } else { await log('āœ… No uncommitted changes found'); return false; // No restart needed } } else { await log(`āš ļø Warning: Could not check git status: ${gitStatusResult.stderr?.toString().trim()}`, { level: 'warning' }); return false; // No restart needed on error } } catch (gitError) { reportError(gitError, { context: 'check_uncommitted_changes', tempDir, operation: 'git_status_check' }); await log(`āš ļø Warning: Error checking for uncommitted changes: ${gitError.message}`, { level: 'warning' }); return false; // No restart needed on error } }; // Export all functions as default object too export default { validateClaudeConnection, handleClaudeRuntimeSwitch, executeClaude, executeClaudeCommand, checkForUncommittedChanges };