@mrtkrcm/acp-claude-code
Version:
ACP (Agent Client Protocol) bridge for Claude Code
430 lines • 16.1 kB
JavaScript
/**
* 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