@deep-assistant/hive-mind
Version:
AI-powered issue solver and hive mind for collaborative problem solving
931 lines (816 loc) ⢠34.8 kB
JavaScript
// 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
};