UNPKG

@mrtkrcm/acp-claude-code

Version:

ACP (Agent Client Protocol) bridge for Claude Code

430 lines 16.1 kB
/** * Enhanced error handling and classification system for ACP-Claude-Code Bridge */ export var ErrorCategory; (function (ErrorCategory) { ErrorCategory["SECURITY"] = "security"; ErrorCategory["NETWORK"] = "network"; ErrorCategory["FILESYSTEM"] = "filesystem"; ErrorCategory["PROTOCOL"] = "protocol"; ErrorCategory["AUTHENTICATION"] = "authentication"; ErrorCategory["RESOURCE"] = "resource"; ErrorCategory["VALIDATION"] = "validation"; ErrorCategory["CLAUDE_SDK"] = "claude_sdk"; ErrorCategory["SESSION"] = "session"; ErrorCategory["UNKNOWN"] = "unknown"; })(ErrorCategory || (ErrorCategory = {})); export var ErrorSeverity; (function (ErrorSeverity) { ErrorSeverity["CRITICAL"] = "critical"; ErrorSeverity["HIGH"] = "high"; ErrorSeverity["MEDIUM"] = "medium"; ErrorSeverity["LOW"] = "low"; })(ErrorSeverity || (ErrorSeverity = {})); export class ErrorClassifier { /** * Classify an error based on its characteristics */ static classify(error, context = {}) { const message = error.message.toLowerCase(); const stack = error.stack?.toLowerCase() || ''; const name = error.name.toLowerCase(); // Security errors if (this.isSecurityError(message, stack, name)) { return { category: ErrorCategory.SECURITY, severity: ErrorSeverity.CRITICAL, code: this.getSecurityErrorCode(message), message: error.message, originalError: error, context, recoverable: false, retryable: false, suggestedAction: 'Review security configuration and input validation' }; } // Network errors if (this.isNetworkError(message, stack, name)) { return { category: ErrorCategory.NETWORK, severity: ErrorSeverity.HIGH, code: this.getNetworkErrorCode(message), message: error.message, originalError: error, context, recoverable: true, retryable: true, suggestedAction: 'Check network connectivity and retry' }; } // Filesystem errors if (this.isFilesystemError(message, stack, name)) { return { category: ErrorCategory.FILESYSTEM, severity: ErrorSeverity.MEDIUM, code: this.getFilesystemErrorCode(message), message: error.message, originalError: error, context, recoverable: true, retryable: false, suggestedAction: 'Check file permissions and disk space' }; } // Authentication errors if (this.isAuthenticationError(message, stack, name)) { return { category: ErrorCategory.AUTHENTICATION, severity: ErrorSeverity.HIGH, code: 'AUTH_FAILED', message: error.message, originalError: error, context, recoverable: true, retryable: false, suggestedAction: 'Run "claude setup-token" to authenticate' }; } // Claude SDK errors if (this.isClaudeSDKError(message, stack, name)) { return { category: ErrorCategory.CLAUDE_SDK, severity: ErrorSeverity.HIGH, code: this.getClaudeSDKErrorCode(message), message: error.message, originalError: error, context, recoverable: true, retryable: true, suggestedAction: 'Check Claude Code installation and authentication' }; } // Session errors if (this.isSessionError(message, stack, name)) { return { category: ErrorCategory.SESSION, severity: ErrorSeverity.MEDIUM, code: this.getSessionErrorCode(message), message: error.message, originalError: error, context, recoverable: true, retryable: true, suggestedAction: 'Create a new session or check session persistence' }; } // Protocol errors if (this.isProtocolError(message, stack, name)) { return { category: ErrorCategory.PROTOCOL, severity: ErrorSeverity.MEDIUM, code: 'PROTOCOL_ERROR', message: error.message, originalError: error, context, recoverable: true, retryable: false, suggestedAction: 'Check ACP client compatibility' }; } // Validation errors if (this.isValidationError(message, stack, name)) { return { category: ErrorCategory.VALIDATION, severity: ErrorSeverity.LOW, code: 'VALIDATION_FAILED', message: error.message, originalError: error, context, recoverable: true, retryable: false, suggestedAction: 'Check input parameters and format' }; } // Default classification return { category: ErrorCategory.UNKNOWN, severity: ErrorSeverity.MEDIUM, code: 'UNKNOWN_ERROR', message: error.message, originalError: error, context, recoverable: true, retryable: false, suggestedAction: 'Contact support with error details' }; } static isSecurityError(message, stack, _name) { const securityPatterns = [ 'invalid session id format', 'path traversal', 'security violation', 'unauthorized', 'forbidden', 'access denied' ]; return securityPatterns.some(pattern => message.includes(pattern) || stack.includes(pattern)); } static getSecurityErrorCode(message) { if (message.includes('session id')) return 'INVALID_SESSION_ID'; if (message.includes('path traversal')) return 'PATH_TRAVERSAL_ATTEMPT'; if (message.includes('unauthorized')) return 'UNAUTHORIZED_ACCESS'; return 'SECURITY_VIOLATION'; } static isNetworkError(message, stack, name) { const networkPatterns = [ 'fetch failed', 'network error', 'connection refused', 'timeout', 'dns', 'enotfound', 'econnrefused', 'econnreset' ]; return networkPatterns.some(pattern => message.includes(pattern) || stack.includes(pattern) || name.includes(pattern)); } static getNetworkErrorCode(message) { if (message.includes('timeout')) return 'NETWORK_TIMEOUT'; if (message.includes('connection refused')) return 'CONNECTION_REFUSED'; if (message.includes('dns') || message.includes('enotfound')) return 'DNS_ERROR'; return 'NETWORK_ERROR'; } static isFilesystemError(message, stack, name) { const fsPatterns = [ 'enoent', 'eacces', 'eperm', 'enospc', 'file not found', 'permission denied', 'no such file' ]; return fsPatterns.some(pattern => message.includes(pattern) || stack.includes(pattern) || name.includes(pattern)); } static getFilesystemErrorCode(message) { if (message.includes('enoent') || message.includes('file not found')) return 'FILE_NOT_FOUND'; if (message.includes('eacces') || message.includes('permission denied')) return 'PERMISSION_DENIED'; if (message.includes('enospc')) return 'DISK_FULL'; return 'FILESYSTEM_ERROR'; } static isAuthenticationError(message, stack, _name) { const authPatterns = [ 'authentication failed', 'invalid token', 'unauthorized', 'not authenticated', 'login required' ]; return authPatterns.some(pattern => message.includes(pattern) || stack.includes(pattern)); } static isClaudeSDKError(message, stack, _name) { const claudePatterns = [ 'claude code', 'anthropic', 'claude sdk', 'claude executable', 'claude process' ]; return claudePatterns.some(pattern => message.includes(pattern) || stack.includes(pattern)); } static getClaudeSDKErrorCode(message) { if (message.includes('executable')) return 'CLAUDE_EXECUTABLE_NOT_FOUND'; if (message.includes('process')) return 'CLAUDE_PROCESS_ERROR'; if (message.includes('sdk')) return 'CLAUDE_SDK_ERROR'; return 'CLAUDE_ERROR'; } static isSessionError(message, stack, _name) { const sessionPatterns = [ 'session not found', 'invalid session', 'session expired', 'session limit', 'session persistence' ]; return sessionPatterns.some(pattern => message.includes(pattern) || stack.includes(pattern)); } static getSessionErrorCode(message) { if (message.includes('not found')) return 'SESSION_NOT_FOUND'; if (message.includes('expired')) return 'SESSION_EXPIRED'; if (message.includes('limit')) return 'SESSION_LIMIT_EXCEEDED'; if (message.includes('persistence')) return 'SESSION_PERSISTENCE_ERROR'; return 'SESSION_ERROR'; } static isProtocolError(message, stack, _name) { const protocolPatterns = [ 'acp', 'protocol', 'invalid message', 'serialization', 'deserialization' ]; return protocolPatterns.some(pattern => message.includes(pattern) || stack.includes(pattern)); } static isValidationError(message, stack, _name) { const validationPatterns = [ 'validation', 'invalid format', 'required parameter', 'invalid input' ]; return validationPatterns.some(pattern => message.includes(pattern) || stack.includes(pattern)); } } export class ErrorRecovery { /** * Attempt to recover from a classified error */ static async attemptRecovery(classifiedError, context = {}) { const { retryCount = 0, maxRetries = 3 } = context; if (!classifiedError.recoverable) { return { success: false, message: `Error is not recoverable: ${classifiedError.suggestedAction || 'Manual intervention required'}` }; } if (classifiedError.retryable && retryCount < maxRetries) { // Implement exponential backoff const delayMs = Math.min(1000 * Math.pow(2, retryCount), 10000); await new Promise(resolve => setTimeout(resolve, delayMs)); return { success: true, message: `Retrying operation (attempt ${retryCount + 1}/${maxRetries}) after ${delayMs}ms delay` }; } // Category-specific recovery strategies switch (classifiedError.category) { case ErrorCategory.NETWORK: return this.recoverFromNetworkError(classifiedError, context); case ErrorCategory.SESSION: return this.recoverFromSessionError(classifiedError, context); case ErrorCategory.FILESYSTEM: return this.recoverFromFilesystemError(classifiedError, context); case ErrorCategory.AUTHENTICATION: return this.recoverFromAuthError(classifiedError, context); default: return { success: false, message: classifiedError.suggestedAction || 'No recovery strategy available' }; } } static async recoverFromNetworkError(_error, _context) { return { success: false, message: 'Network recovery: Check connectivity and retry manually' }; } static async recoverFromSessionError(error, _context) { if (error.code === 'SESSION_NOT_FOUND') { return { success: true, message: 'Session recovery: Create new session to continue' }; } return { success: false, message: 'Session recovery: Manual session management required' }; } static async recoverFromFilesystemError(_error, _context) { return { success: false, message: 'Filesystem recovery: Check permissions and disk space' }; } static async recoverFromAuthError(_error, _context) { return { success: false, message: 'Auth recovery: Run "claude setup-token" to re-authenticate' }; } } export class ErrorLogger { static logFile; static configure(logFile) { this.logFile = logFile; } /** * Log classified error with structured format */ static async log(classifiedError, context = {}) { const logEntry = { timestamp: new Date().toISOString(), category: classifiedError.category, severity: classifiedError.severity, code: classifiedError.code, message: classifiedError.message, recoverable: classifiedError.recoverable, retryable: classifiedError.retryable, suggestedAction: classifiedError.suggestedAction, context: { ...classifiedError.context, ...context }, stack: classifiedError.originalError.stack }; // Always log to console for immediate visibility const logLevel = this.getConsoleLogLevel(classifiedError.severity); console[logLevel](`[${classifiedError.category.toUpperCase()}] ${classifiedError.code}: ${classifiedError.message}`); // Log full details in debug mode if (process.env.ACP_DEBUG === 'true') { console.error('Error details:', JSON.stringify(logEntry, null, 2)); } // Write to log file if configured if (this.logFile) { try { const { writeFile } = await import('node:fs/promises'); await writeFile(this.logFile, JSON.stringify(logEntry) + '\n', { flag: 'a' }); } catch (writeError) { console.error('Failed to write to log file:', writeError); } } } static getConsoleLogLevel(severity) { switch (severity) { case ErrorSeverity.CRITICAL: case ErrorSeverity.HIGH: return 'error'; case ErrorSeverity.MEDIUM: return 'warn'; case ErrorSeverity.LOW: return 'info'; default: return 'log'; } } } /** * Utility function to handle errors consistently across the application */ export async function handleError(error, context = {}, options = {}) { const { log = true, recover = false, retryContext = {} } = options; // Classify the error const classified = ErrorClassifier.classify(error, context); // Log the error if requested if (log) { await ErrorLogger.log(classified, context); } // Attempt recovery if requested let recovery; if (recover) { recovery = await ErrorRecovery.attemptRecovery(classified, retryContext); } return { classified, recovery }; } //# sourceMappingURL=error-handling.js.map