error-handling.js•12.5 kB
export class ToolError extends Error {
  constructor(message, code = 'TOOL_ERROR', toolName = 'unknown', retryable = false, suggestions = []) {
    super(message);
    this.name = 'ToolError';
    this.code = code;
    this.tool = toolName;
    this.timestamp = Date.now();
    this.retryable = retryable;
    this.suggestions = suggestions;
  }
  toJSON() {
    return {
      code: this.code,
      message: this.message,
      tool: this.tool,
      timestamp: this.timestamp,
      retryable: this.retryable,
      suggestions: this.suggestions
    };
  }
}
export class ValidationError extends ToolError {
  constructor(message, toolName = 'unknown') {
    super(message, 'VALIDATION_ERROR', toolName, false, [
      'Check that all required parameters are provided',
      'Verify parameter types match the expected schema',
      'Review the tool documentation for parameter requirements'
    ]);
    this.name = 'ValidationError';
  }
}
export class ExecutionError extends ToolError {
  constructor(message, toolName = 'unknown') {
    super(message, 'EXECUTION_ERROR', toolName, true, [
      'Try running the operation again',
      'Check if the working directory is accessible',
      'Verify that required dependencies are installed'
    ]);
    this.name = 'ExecutionError';
  }
}
export class SearchError extends ToolError {
  constructor(message, toolName = 'unknown') {
    super(message, 'SEARCH_ERROR', toolName, true, [
      'Try a different search query',
      'Check if the search path exists',
      'Consider using a more specific search pattern'
    ]);
    this.name = 'SearchError';
  }
}
export class TimeoutError extends ToolError {
  constructor(message, toolName = 'unknown', timeoutMs = 0) {
    super(message, 'TIMEOUT', toolName, true, [
      'Try reducing the scope of the operation',
      'Consider using a simpler tool for this task',
      'Break the operation into smaller chunks',
      `Increase timeout beyond ${timeoutMs}ms if needed`
    ]);
    this.name = 'TimeoutError';
    this.timeoutMs = timeoutMs;
  }
}
export class PermissionError extends ToolError {
  constructor(message, toolName = 'unknown') {
    super(message, 'PERMISSION_DENIED', toolName, false, [
      'Check file and directory permissions',
      'Ensure the tool has necessary access rights',
      'Try running with appropriate permissions'
    ]);
    this.name = 'PermissionError';
  }
}
export class NetworkError extends ToolError {
  constructor(message, toolName = 'unknown') {
    super(message, 'NETWORK_ERROR', toolName, true, [
      'Check your internet connection',
      'Verify the target URL is accessible',
      'Try the operation again in a few moments'
    ]);
    this.name = 'NetworkError';
  }
}
export class ResourceError extends ToolError {
  constructor(message, toolName = 'unknown') {
    super(message, 'RESOURCE_ERROR', toolName, true, [
      'Check available disk space and memory',
      'Close unnecessary applications',
      'Try processing smaller amounts of data'
    ]);
    this.name = 'ResourceError';
  }
}
export class ToolErrorHandler {
  constructor(toolName = 'unknown') {
    this.toolName = toolName;
  }
  handleError(error, context = {}) {
    // Handle null/undefined errors
    if (!error) {
      return new ToolError(
        'Unknown error occurred',
        'UNKNOWN_ERROR',
        this.toolName,
        false,
        ['No error details available']
      );
    }
    // Handle ToolError instances
    if (error instanceof ToolError) {
      if (error.tool === 'unknown') {
        error.tool = this.toolName;
      }
      return error;
    }
    // Handle malformed error objects
    if (typeof error === 'object' && error !== null) {
      const message = error.message || 'Unknown error occurred';
      const code = error.code || 'UNKNOWN_ERROR';
      // Safely check message properties
      const messageStr = String(message || '');
      if (code === 'ENOENT' || messageStr.includes('no such file')) {
        return new ToolError(
          `File or directory not found: ${error.message}`,
          'FILE_NOT_FOUND',
          this.toolName,
          false,
          [
            'Verify the file path is correct',
            'Check if the file exists in the working directory',
            'Ensure proper file permissions'
          ]
        );
      }
      if (error.code === 'EACCES' || messageStr.includes('permission denied')) {
        return new PermissionError(
          `Permission denied: ${error.message}`,
          this.toolName
        );
      }
      if (error.code === 'ETIMEDOUT' || messageStr.includes('timeout')) {
        return new TimeoutError(
          `Operation timed out: ${error.message}`,
          this.toolName,
          context.timeout || 0
        );
      }
      if (error.code === 'ENOTDIR' || messageStr.includes('not a directory')) {
        return new ValidationError(
          `Invalid directory path: ${error.message}`,
          this.toolName
        );
      }
      if (error.code === 'EMFILE' || error.code === 'ENFILE' || messageStr.includes('too many files')) {
        return new ResourceError(
          `Resource limit exceeded: ${error.message}`,
          this.toolName
        );
      }
      if (messageStr.includes('network') || messageStr.includes('connection')) {
        return new NetworkError(
          `Network error: ${error.message}`,
          this.toolName
        );
      }
      return new ToolError(
        error.message || 'Unknown error occurred',
        'UNKNOWN_ERROR',
        this.toolName,
        true,
        [
          'Try the operation again',
          'Check the console for more details',
          'Contact support if the problem persists'
        ]
      )
    }
    // Handle non-object errors (strings, numbers, etc.)
    return new ToolError(
      String(error || 'Unknown error occurred'),
      'UNKNOWN_ERROR',
      this.toolName,
      false,
      ['Try the operation again', 'Check the console for more details']
    );
  }
  async withTimeout(operation, timeoutMs = 30000) {
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        reject(new TimeoutError(
          `Operation timed out after ${timeoutMs}ms`,
          this.toolName,
          timeoutMs
        ));
      }, timeoutMs);
      Promise.resolve(operation())
        .then(result => {
          clearTimeout(timer);
          resolve(result);
        })
        .catch(error => {
          clearTimeout(timer);
          reject(this.handleError(error, { timeout: timeoutMs }));
        });
    });
  }
  async withRetry(operation, maxRetries = 3, delayMs = 1000) {
    let lastError;
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        lastError = this.handleError(error);
        if (!lastError.retryable || attempt === maxRetries) {
          throw lastError;
        }
        await new Promise(resolve => setTimeout(resolve, delayMs * attempt));
        await new Promise(resolve => setTimeout(resolve, delayMs * attempt));
      }
    }
    throw lastError;
  }
}
export function createErrorHandler(toolName) {
  const errorHandler = new ToolErrorHandler(toolName);
  return async (operation, errorMessage = `${toolName} failed`) => {
    try {
      return await operation();
    } catch (error) {
      throw errorHandler.handleError(error);
    }
  };
}
export function withErrorHandling(handler, toolName) {
  const errorHandler = new ToolErrorHandler(toolName);
  return async (args) => {
    try {
      return await handler(args);
    } catch (error) {
      const toolError = errorHandler.handleError(error);
      // Safely log error details without using toJSON which might fail
      try {
        console.error(`Error in ${toolName}:`, {
          code: toolError.code,
          message: toolError.message,
          tool: toolError.tool,
          timestamp: toolError.timestamp,
          retryable: toolError.retryable,
          suggestions: toolError.suggestions
        });
      } catch (logError) {
        console.error(`Error in ${toolName}:`, toolError.message || 'Unknown error');
      }
      
      const errorText = [
        `${toolError.code}: ${toolError.message}`,
        '',
        'Suggestions:',
        ...toolError.suggestions.map(s => `• ${s}`)
      ].join('\n');
      if (toolError.retryable) {
        return {
          content: [{
            type: "text",
            text: `${errorText}\n\nThis error is retryable. You may try the operation again.`
          }],
          isError: true
        };
      }
      return {
        content: [{ type: "text", text: errorText }],
        isError: true
      };
    }
  };
}
export function validateParams(params, schema) {
  const errors = [];
  if (schema.required) {
    if (schema.required) {
      for (const required of schema.required) {
        if (params[required] === undefined || params[required] === null || params[required] === '') {
          errors.push(`Missing required parameter: ${required}`);
        }
      }
    }
    if (schema.properties) {
      if (schema.properties) {
        for (const [key, value] of Object.entries(params)) {
          const propertySchema = schema.properties[key];
          if (propertySchema && value !== undefined) {
            if (propertySchema.type && !validateType(value, propertySchema.type)) {
              errors.push(`Invalid type for parameter ${key}: expected ${propertySchema.type}`);
            }
            if (propertySchema.enum && !propertySchema.enum.includes(value)) {
              errors.push(`Invalid value for parameter ${key}: must be one of ${propertySchema.enum.join(', ')}`);
            }
          }
        }
      }
      if (errors.length > 0) {
        throw new ValidationError(errors.join(', '));
      }
    }
    function validateType(value, expectedType) {
      if (Array.isArray(expectedType)) {
        return expectedType.some(type => validateType(value, type));
      }
      switch (expectedType) {
        case 'string':
          return typeof value === 'string';
        case 'number':
          return typeof value === 'number' && !isNaN(value);
        case 'boolean':
          return typeof value === 'boolean';
        case 'array':
          return Array.isArray(value);
        case 'object':
          return typeof value === 'object' && value !== null && !Array.isArray(value);
        default:
          return true;
      }
    }
  }
}
export function createToolErrorHandler(toolName) {
  return new ToolErrorHandler(toolName);
}
export function createAdvancedToolHandler(handler, toolName, options = {}) {
  const {
    timeout = 30000,
    retries = 1,
    retryDelay = 1000,
    enableTimeout = false,
    enableRetry = false
  } = options;
  const errorHandler = new ToolErrorHandler(toolName);
  return async (args) => {
    let operation = () => handler(args);
    if (enableTimeout) {
      const originalOperation = operation;
      operation = () => errorHandler.withTimeout(originalOperation, timeout);
    }
    if (enableRetry) {
      const originalOperation = operation;
      operation = () => errorHandler.withRetry(originalOperation, retries, retryDelay);
    }
    try {
      return await operation();
    } catch (error) {
      const toolError = errorHandler.handleError(error);
      // Safely log error details without using toJSON which might fail
      try {
        console.error(`Error in ${toolName}:`, {
          code: toolError.code,
          message: toolError.message,
          tool: toolError.tool,
          timestamp: toolError.timestamp,
          retryable: toolError.retryable,
          suggestions: toolError.suggestions
        });
      } catch (logError) {
        console.error(`Error in ${toolName}:`, toolError.message || 'Unknown error');
      }
      const errorText = [
        `${toolError.code}: ${toolError.message}`,
        '',
        'Suggestions:',
        ...toolError.suggestions.map(s => `• ${s}`)
      ].join('\n');
      if (toolError.retryable && !enableRetry) {
        return {
          content: [{
            type: "text",
            text: `${errorText}\n\nThis error is retryable. You may try the operation again.`
          }],
          isError: true
        };
      }
      return {
        content: [{ type: "text", text: errorText }],
        isError: true
      };
    }
  };
}