UNPKG

termcode

Version:

Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative

1,002 lines (995 loc) 38.9 kB
import { log } from "../util/logging.js"; import { runShell } from "../tools/shell.js"; import { getProvider } from "../providers/index.js"; import { workspaceManager } from "../workspace/manager.js"; import path from "node:path"; /** * Advanced Error Recovery System with AI-powered suggestions * Far superior to Claude Code's basic error handling */ export class IntelligentErrorRecovery { errorPatterns = []; recoveryHistory = new Map(); successfulRecoveries = new Map(); knowledgeBase = new Map(); constructor() { this.initializeErrorPatterns(); this.loadKnowledgeBase(); } /** * Analyze error and generate comprehensive recovery plan */ async analyzeAndRecover(context) { log.info(`Analyzing error: ${context.error.substring(0, 100)}...`); // Step 1: Pattern matching for known errors const pattern = this.matchErrorPattern(context); // Step 2: Context-aware analysis const contextualInfo = await this.gatherContextualInformation(context); // Step 3: AI-powered analysis for unknown errors const aiAnalysis = pattern ? null : await this.performAIAnalysis(context, contextualInfo); // Step 4: Generate recovery plan const plan = this.generateRecoveryPlan(context, pattern, contextualInfo, aiAnalysis); // Step 5: Learn from this error await this.learnFromError(context, plan); return plan; } /** * Execute recovery plan with user confirmation */ async executeRecoveryPlan(plan, context, options = {}) { const executedActions = []; const maxRetries = options.maxRetries || 3; let currentRetry = 0; log.info(`Executing recovery plan for: ${plan.category}`); for (const action of plan.actions) { if (currentRetry >= maxRetries) { log.warn('Maximum retry count reached, stopping recovery execution'); break; } try { // Check prerequisites if (action.prerequisites && !await this.checkPrerequisites(action.prerequisites, context)) { log.warn(`Prerequisites not met for action: ${action.title}`); continue; } // Determine if we should execute automatically const shouldExecute = this.shouldAutoExecute(action, options.autoExecute); if (!shouldExecute) { log.info(`Skipping action (user confirmation required): ${action.title}`); continue; } log.info(`Executing recovery action: ${action.title}`); const success = await this.executeAction(action, context); if (success) { executedActions.push(action); log.success(`Recovery action succeeded: ${action.title}`); // Execute follow-up actions if any if (action.followUp) { for (const followUp of action.followUp) { const followUpSuccess = await this.executeAction(followUp, context); if (followUpSuccess) { executedActions.push(followUp); } } } // Test if original issue is resolved if (await this.verifyRecovery(context)) { return { success: true, executedActions }; } } else { log.warn(`Recovery action failed: ${action.title}`); } } catch (error) { log.error(`Error executing recovery action ${action.title}:`, error); currentRetry++; } } // If we get here, recovery didn't fully succeed return { success: false, executedActions, finalError: 'Recovery plan execution incomplete or unsuccessful' }; } /** * Initialize comprehensive error patterns */ initializeErrorPatterns() { this.errorPatterns = [ // Node.js / NPM errors { id: 'npm_missing_package', name: 'Missing NPM Package', patterns: [ /Cannot find module ['"]([^'"]+)['"]/, /Module not found: Error: Can't resolve ['"]([^'"]+)['"]/, /Error: Cannot resolve module ['"]([^'"]+)['"]/ ], category: 'dependency', severity: 'medium', platforms: ['node', 'npm'], languages: ['javascript', 'typescript'], frameworks: ['react', 'vue', 'angular', 'express'], solutions: [ { title: 'Install missing package', commands: ['npm install {package}'], description: 'Install the missing package using npm', confidence: 0.9, automatic: true }, { title: 'Install as dev dependency', commands: ['npm install --save-dev {package}'], description: 'Install as development dependency if it\'s a dev tool', confidence: 0.7, automatic: false } ] }, { id: 'npm_peer_dependency', name: 'NPM Peer Dependency Warning', patterns: [ /npm WARN .* requires a peer of ([^@\s]+)@([^\s]+)/, /Could not resolve dependency.*peer ([^@\s]+)@([^\s]+)/ ], category: 'dependency', severity: 'medium', platforms: ['npm'], languages: ['javascript', 'typescript'], frameworks: ['react', 'vue', 'angular'], solutions: [ { title: 'Install peer dependency', commands: ['npm install {package}@{version}'], description: 'Install the required peer dependency', confidence: 0.8, automatic: true } ] }, { id: 'npm_package_json_missing', name: 'Package.json Missing', patterns: [ /ENOENT.*package\.json/, /npm ERR!.*no such file or directory.*package\.json/ ], category: 'configuration', severity: 'high', platforms: ['npm'], languages: ['javascript', 'typescript'], frameworks: [], solutions: [ { title: 'Initialize npm project', commands: ['npm init -y'], description: 'Create a new package.json file', confidence: 0.9, automatic: false } ] }, // Python errors { id: 'python_module_not_found', name: 'Python Module Not Found', patterns: [ /ModuleNotFoundError: No module named '([^']+)'/, /ImportError: No module named ([^\s]+)/ ], category: 'dependency', severity: 'medium', platforms: ['python', 'pip'], languages: ['python'], frameworks: ['django', 'flask', 'fastapi'], solutions: [ { title: 'Install missing package with pip', commands: ['pip install {package}'], description: 'Install the missing Python package', confidence: 0.9, automatic: true }, { title: 'Install from requirements.txt', commands: ['pip install -r requirements.txt'], description: 'Install all dependencies from requirements file', confidence: 0.7, automatic: true } ] }, { id: 'python_version_mismatch', name: 'Python Version Mismatch', patterns: [ /SyntaxError.*invalid syntax.*python ([0-9.]+)/, /requires python ([0-9.]+)/i ], category: 'environment', severity: 'high', platforms: ['python'], languages: ['python'], frameworks: [], solutions: [ { title: 'Check Python version', commands: ['python --version', 'python3 --version'], description: 'Verify which Python version is active', confidence: 0.8, automatic: true }, { title: 'Use Python 3 explicitly', commands: ['python3 {original_command}'], description: 'Use python3 command instead of python', confidence: 0.7, automatic: false } ] }, // Git errors { id: 'git_not_initialized', name: 'Git Repository Not Initialized', patterns: [ /fatal: not a git repository/, /Not a git repository/ ], category: 'version_control', severity: 'medium', platforms: ['git'], languages: [], frameworks: [], solutions: [ { title: 'Initialize Git repository', commands: ['git init'], description: 'Initialize a new Git repository', confidence: 0.9, automatic: false } ] }, { id: 'git_merge_conflict', name: 'Git Merge Conflict', patterns: [ /CONFLICT.*Merge conflict in/, /Automatic merge failed/, /both modified:/ ], category: 'version_control', severity: 'high', platforms: ['git'], languages: [], frameworks: [], solutions: [ { title: 'Check conflicted files', commands: ['git status --porcelain | grep "^UU"'], description: 'List files with merge conflicts', confidence: 0.9, automatic: true }, { title: 'Open merge tool', commands: ['git mergetool'], description: 'Open configured merge tool to resolve conflicts', confidence: 0.8, automatic: false }, { title: 'Abort merge', commands: ['git merge --abort'], description: 'Cancel the merge and return to previous state', confidence: 0.9, automatic: false } ] }, // Permission errors { id: 'permission_denied', name: 'Permission Denied', patterns: [ /permission denied/i, /EACCES/, /Operation not permitted/ ], category: 'permissions', severity: 'medium', platforms: ['linux', 'macos'], languages: [], frameworks: [], solutions: [ { title: 'Check file permissions', commands: ['ls -la {file}'], description: 'Check current file permissions', confidence: 0.9, automatic: true }, { title: 'Make file executable', commands: ['chmod +x {file}'], description: 'Add execute permission to file', confidence: 0.7, automatic: false }, { title: 'Change ownership', commands: ['sudo chown $USER:$USER {file}'], description: 'Change file ownership to current user', confidence: 0.6, automatic: false } ] }, // Network/connectivity errors { id: 'network_timeout', name: 'Network Timeout', patterns: [ /timeout/i, /ETIMEDOUT/, /connection timed out/i, /network is unreachable/i ], category: 'network', severity: 'medium', platforms: ['network'], languages: [], frameworks: [], solutions: [ { title: 'Check internet connectivity', commands: ['ping -c 3 8.8.8.8'], description: 'Test basic internet connectivity', confidence: 0.8, automatic: true }, { title: 'Check DNS resolution', commands: ['nslookup google.com'], description: 'Test DNS resolution', confidence: 0.8, automatic: true }, { title: 'Retry with increased timeout', commands: [], description: 'Retry the original command with increased timeout', confidence: 0.6, automatic: false } ] }, // Compilation errors { id: 'typescript_compilation_error', name: 'TypeScript Compilation Error', patterns: [ /error TS[0-9]+:/, /TypeScript error in/ ], category: 'compilation', severity: 'high', platforms: ['typescript'], languages: ['typescript'], frameworks: ['react', 'angular', 'vue'], solutions: [ { title: 'Run TypeScript compiler', commands: ['npx tsc --noEmit'], description: 'Check for TypeScript compilation errors', confidence: 0.9, automatic: true }, { title: 'Fix type errors', commands: [], description: 'Review and fix type annotations', confidence: 0.7, automatic: false } ] }, // Database connection errors { id: 'database_connection_error', name: 'Database Connection Error', patterns: [ /connection refused.*[0-9]+/, /database.*not.*connect/i, /ECONNREFUSED.*[0-9]+/ ], category: 'database', severity: 'high', platforms: ['database'], languages: [], frameworks: ['django', 'rails', 'express'], solutions: [ { title: 'Check database service status', commands: ['systemctl status postgresql', 'systemctl status mysql'], description: 'Check if database service is running', confidence: 0.8, automatic: true }, { title: 'Check database configuration', commands: [], description: 'Verify database connection parameters', confidence: 0.7, automatic: false } ] }, // Memory/resource errors { id: 'out_of_memory', name: 'Out of Memory Error', patterns: [ /out of memory/i, /killed.*signal 9/, /ENOMEM/, /heap out of memory/i ], category: 'resources', severity: 'critical', platforms: ['system'], languages: [], frameworks: [], solutions: [ { title: 'Check memory usage', commands: ['free -h', 'ps aux --sort=-%mem | head -10'], description: 'Check current memory usage and top processes', confidence: 0.9, automatic: true }, { title: 'Increase Node.js memory limit', commands: ['export NODE_OPTIONS="--max-old-space-size=4096"'], description: 'Increase Node.js heap size limit', confidence: 0.7, automatic: false } ] }, // Port/address in use errors { id: 'port_already_in_use', name: 'Port Already in Use', patterns: [ /port [0-9]+ is already in use/i, /EADDRINUSE.*:[0-9]+/, /address already in use/i ], category: 'network', severity: 'medium', platforms: ['network'], languages: [], frameworks: ['express', 'django', 'rails'], solutions: [ { title: 'Find process using port', commands: ['lsof -i :{port}', 'netstat -tulpn | grep :{port}'], description: 'Find which process is using the port', confidence: 0.9, automatic: true }, { title: 'Kill process on port', commands: ['kill -9 $(lsof -t -i:{port})'], description: 'Kill the process using the port', confidence: 0.8, automatic: false }, { title: 'Use different port', commands: [], description: 'Configure application to use a different port', confidence: 0.7, automatic: false } ] } ]; } /** * Load knowledge base with common solutions */ loadKnowledgeBase() { this.knowledgeBase.set('npm_common_solutions', { 'react-scripts': 'npm install react-scripts', 'typescript': 'npm install typescript @types/node', 'eslint': 'npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin', '@types/react': 'npm install @types/react @types/react-dom' }); this.knowledgeBase.set('python_common_solutions', { 'requests': 'pip install requests', 'numpy': 'pip install numpy', 'pandas': 'pip install pandas', 'django': 'pip install django' }); this.knowledgeBase.set('environment_fixes', { 'node_version': 'Use nvm to manage Node.js versions: nvm install node', 'python_version': 'Use pyenv to manage Python versions: pyenv install 3.x', 'path_issues': 'Add to PATH: export PATH=$PATH:/new/path' }); } /** * Match error against known patterns */ matchErrorPattern(context) { const errorText = `${context.error} ${context.stderr || ''} ${context.stdout || ''}`; for (const pattern of this.errorPatterns) { for (const regex of pattern.patterns) { if (regex.test(errorText)) { // Check if pattern applies to current context if (this.patternApplies(pattern, context)) { return pattern; } } } } return null; } /** * Check if error pattern applies to current context */ patternApplies(pattern, context) { // Check platforms if (pattern.platforms.length > 0) { const hasApplicablePlatform = pattern.platforms.some(platform => { if (platform === 'node' && context.command?.[0] === 'node') return true; if (platform === 'npm' && context.command?.[0] === 'npm') return true; if (platform === 'python' && context.command?.[0]?.includes('python')) return true; if (platform === 'git' && context.command?.[0] === 'git') return true; return false; }); if (!hasApplicablePlatform) return false; } // Check project type if (pattern.languages.length > 0 && context.projectType) { if (!pattern.languages.includes(context.projectType)) return false; } // Check framework if (pattern.frameworks.length > 0 && context.framework) { if (!pattern.frameworks.includes(context.framework)) return false; } return true; } /** * Gather contextual information about the error */ async gatherContextualInformation(context) { const info = { fileSystem: {}, processes: {}, network: {}, environment: {} }; try { // Check common files existence const commonFiles = ['package.json', 'requirements.txt', 'Cargo.toml', 'go.mod', '.git']; for (const file of commonFiles) { const filePath = path.join(context.repoPath, file); try { const fs = await import('node:fs/promises'); await fs.access(filePath); info.fileSystem[file] = true; } catch { info.fileSystem[file] = false; } } // Get environment variables info.environment.nodeVersion = process.env.NODE_VERSION || 'unknown'; info.environment.pythonPath = process.env.PYTHON_PATH || 'unknown'; info.environment.path = process.env.PATH || ''; // Check running processes (if command involves ports) if (context.error.includes('port') || context.error.includes('EADDRINUSE')) { try { const result = await runShell(['lsof', '-i', '-P', '-n'], context.repoPath); if (result.ok) { info.processes.ports = result.data.stdout; } } catch { // Ignore if lsof not available } } // Get workspace information const workspace = workspaceManager.getCurrentWorkspace(); if (workspace) { info.workspace = { type: workspace.type, framework: workspace.framework, preferences: workspace.preferences }; } } catch (error) { log.warn('Error gathering contextual information:', error); } return info; } /** * Perform AI-powered error analysis for unknown errors */ async performAIAnalysis(context, contextualInfo) { try { const provider = getProvider(context.provider); const prompt = this.buildAnalysisPrompt(context, contextualInfo); const response = await provider.chat([ { role: 'user', content: prompt } ], { model: context.model, temperature: 0.1, maxTokens: 1000 }); return this.parseAIResponse(response); } catch (error) { log.warn('AI analysis failed:', error); return null; } } /** * Build comprehensive analysis prompt for AI */ buildAnalysisPrompt(context, contextualInfo) { return `Analyze this error and provide solutions: ERROR DETAILS: Command: ${context.command?.join(' ') || 'unknown'} Error: ${context.error} Exit Code: ${context.exitCode || 'unknown'} STDERR: ${context.stderr || 'none'} STDOUT: ${context.stdout || 'none'} CONTEXT: Project Type: ${context.projectType || 'unknown'} Framework: ${context.framework || 'unknown'} Environment: ${context.environment || 'unknown'} FILE SYSTEM: ${Object.entries(contextualInfo.fileSystem) .map(([file, exists]) => `${file}: ${exists ? 'exists' : 'missing'}`) .join('\n')} WORKSPACE INFO: ${JSON.stringify(contextualInfo.workspace || {}, null, 2)} Please provide: 1. Error category (dependency, configuration, permissions, network, compilation, etc.) 2. Severity (low, medium, high, critical) 3. Root cause analysis 4. 2-3 concrete solutions with commands 5. Prevention strategies 6. Confidence level for each solution (0-1) Format as JSON: { "category": "", "severity": "", "rootCause": "", "solutions": [ { "title": "", "description": "", "commands": [], "confidence": 0.0, "automatic": true/false } ], "prevention": [], "explanation": "" }`; } /** * Parse AI response into structured format */ parseAIResponse(response) { try { // Extract JSON from response const content = response.choices?.[0]?.message?.content || response.content || ''; const jsonMatch = content.match(/\{[\s\S]*\}/); if (jsonMatch) { return JSON.parse(jsonMatch[0]); } } catch (error) { log.warn('Failed to parse AI response:', error); } return null; } /** * Generate comprehensive recovery plan */ generateRecoveryPlan(context, pattern, contextualInfo, aiAnalysis) { const actions = []; let category = 'unknown'; let severity = 'medium'; let explanation = ''; let prevention = []; if (pattern) { // Use pattern-based solutions category = pattern.category; severity = pattern.severity; explanation = `Known error pattern: ${pattern.name}`; for (const solution of pattern.solutions) { actions.push({ id: `${pattern.id}_${actions.length}`, type: solution.commands ? 'command' : 'manual', title: solution.title, description: solution.description, command: solution.commands ? this.substituteVariables(solution.commands, context) : undefined, confidence: solution.confidence, impact: 'medium', automation: solution.automatic ? 'automatic' : 'prompted' }); } } if (aiAnalysis) { // Use AI-generated solutions category = aiAnalysis.category || category; severity = aiAnalysis.severity || severity; explanation = aiAnalysis.explanation || aiAnalysis.rootCause || explanation; prevention = aiAnalysis.prevention || prevention; for (const solution of aiAnalysis.solutions || []) { actions.push({ id: `ai_${actions.length}`, type: solution.commands?.length > 0 ? 'command' : 'manual', title: solution.title, description: solution.description, command: solution.commands, confidence: solution.confidence || 0.5, impact: 'medium', automation: solution.automatic ? 'automatic' : 'prompted' }); } } // Add generic diagnostic actions if no specific solutions found if (actions.length === 0) { actions.push({ id: 'generic_diagnose', type: 'command', title: 'Basic System Check', description: 'Run basic diagnostic commands', command: ['echo "System: $(uname -a)"', 'echo "Node: $(node --version 2>/dev/null || echo \'not found\')"', 'echo "Python: $(python --version 2>/dev/null || echo \'not found\')"'], confidence: 0.3, impact: 'low', automation: 'automatic' }, { id: 'generic_retry', type: 'manual', title: 'Retry Command', description: 'Retry the original command after environment check', confidence: 0.4, impact: 'low', automation: 'prompted' }); } // Estimate time based on actions const estimatedTime = actions.reduce((total, action) => { if (action.type === 'command') return total + 30; // 30 seconds per command if (action.type === 'install') return total + 120; // 2 minutes per install return total + 60; // 1 minute for manual actions }, 0); return { error: context.error.substring(0, 200), category, severity, actions, explanation, prevention, estimatedTime, success: actions.length > 0 }; } /** * Substitute variables in command templates */ substituteVariables(commands, context) { return commands.map(cmd => { let substituted = cmd; // Extract package name from error if possible const packageMatch = context.error.match(/Cannot find module ['"]([^'"]+)['"]/); if (packageMatch) { substituted = substituted.replace('{package}', packageMatch[1]); } // Extract version if available const versionMatch = context.error.match(/requires.*@([^\s]+)/); if (versionMatch) { substituted = substituted.replace('{version}', versionMatch[1]); } // Extract port number const portMatch = context.error.match(/:([0-9]+)/); if (portMatch) { substituted = substituted.replace('{port}', portMatch[1]); } // Extract file path const fileMatch = context.error.match(/([\/\w.-]+\.[a-zA-Z]+)/); if (fileMatch) { substituted = substituted.replace('{file}', fileMatch[1]); } return substituted; }); } /** * Check if action should be executed automatically */ shouldAutoExecute(action, autoExecuteFlag) { if (action.automation === 'manual') return false; if (action.automation === 'automatic') return true; // For 'prompted' actions, check confidence and user preference if (autoExecuteFlag && action.confidence > 0.7) return true; return false; } /** * Check prerequisites for an action */ async checkPrerequisites(prerequisites, context) { for (const prereq of prerequisites) { // Simple prerequisite checks if (prereq.startsWith('command:')) { const cmd = prereq.substring(8); try { const result = await runShell([cmd, '--version'], context.repoPath); if (!result.ok) return false; } catch { return false; } } if (prereq.startsWith('file:')) { const filePath = prereq.substring(5); try { const fs = await import('node:fs/promises'); await fs.access(path.join(context.repoPath, filePath)); } catch { return false; } } } return true; } /** * Execute recovery action */ async executeAction(action, context) { try { switch (action.type) { case 'command': if (!action.command) return false; for (const cmd of action.command) { const parts = cmd.split(' '); const result = await runShell(parts, context.repoPath); if (!result.ok) { log.warn(`Command failed: ${cmd}`); return false; } } return true; case 'install': case 'config': case 'fix': // These might involve specific handlers return await this.executeSpecializedAction(action, context); case 'manual': log.info(`Manual action required: ${action.title}`); log.info(action.description); return true; // Assume user will handle it default: return false; } } catch (error) { log.error(`Error executing action ${action.title}:`, error); return false; } } /** * Execute specialized actions */ async executeSpecializedAction(action, context) { // This could be extended with specific handlers for different action types if (action.command) { return await this.executeAction({ ...action, type: 'command' }, context); } log.info(`Specialized action: ${action.title}`); return true; } /** * Verify if recovery was successful */ async verifyRecovery(context) { if (!context.command) return false; try { // Retry the original command const result = await runShell(context.command, context.repoPath); return result.ok; } catch { return false; } } /** * Learn from error patterns for future improvements */ async learnFromError(context, plan) { const errorKey = this.generateErrorKey(context); if (!this.recoveryHistory.has(errorKey)) { this.recoveryHistory.set(errorKey, []); } this.recoveryHistory.get(errorKey).push(plan); // Keep only recent recovery attempts const history = this.recoveryHistory.get(errorKey); if (history.length > 5) { this.recoveryHistory.set(errorKey, history.slice(-5)); } } /** * Generate unique key for error type */ generateErrorKey(context) { // Create a key that identifies similar errors const errorHash = context.error .replace(/['"]/g, '') .replace(/\d+/g, 'N') .replace(/[a-f0-9]{8,}/g, 'HASH') .toLowerCase() .substring(0, 100); return `${context.command?.[0] || 'unknown'}-${errorHash}`; } /** * Get recovery statistics */ getRecoveryStats() { const totalErrors = Array.from(this.recoveryHistory.values()) .reduce((sum, plans) => sum + plans.length, 0); const successful = Array.from(this.recoveryHistory.values()) .flatMap(plans => plans) .filter(plan => plan.success).length; const categories = new Map(); Array.from(this.recoveryHistory.values()) .flatMap(plans => plans) .forEach(plan => { categories.set(plan.category, (categories.get(plan.category) || 0) + 1); }); const topCategories = Array.from(categories.entries()) .map(([category, count]) => ({ category, count })) .sort((a, b) => b.count - a.count) .slice(0, 5); const avgTime = Array.from(this.recoveryHistory.values()) .flatMap(plans => plans) .reduce((sum, plan) => sum + plan.estimatedTime, 0) / Math.max(totalErrors, 1); return { totalErrors, successfulRecoveries: successful, topErrorCategories: topCategories, averageRecoveryTime: avgTime, mostEffectiveActions: [] // Could be implemented with more detailed tracking }; } } // Export singleton instance export const intelligentErrorRecovery = new IntelligentErrorRecovery();