/** * Error handler for Obsidian CLI operations * Maps CLI errors to MCP error codes with actionable messages * Constitutional Principle IV: Defensive Programming */ import { CLIResult, ErrorResponse, MCPErrorCode, ObsidianErrorType, } from './types.js'; import { logger } from './logger.js'; /** * Error class for Obsidian operations */ export class ObsidianError extends Error { constructor( public type: ObsidianErrorType, message: string, public details?: unknown ) { super(message); this.name = 'ObsidianError'; } } /** * Map CLI result to error type */ export function detectErrorType(result: CLIResult): ObsidianErrorType | null { const stderr = result.stderr.toLowerCase(); if (result.timedOut) { return ObsidianErrorType.CLI_TIMEOUT; } if (stderr.includes('not found') || stderr.includes('does not exist')) { if (stderr.includes('vault')) { return ObsidianErrorType.VAULT_NOT_FOUND; } return ObsidianErrorType.FILE_NOT_FOUND; } if ( stderr.includes('obsidian is not running') || stderr.includes('not running') || stderr.includes('cannot connect') ) { return ObsidianErrorType.OBSIDIAN_NOT_RUNNING; } if ( stderr.includes('multiple') && (stderr.includes('found') || stderr.includes('match')) ) { return ObsidianErrorType.AMBIGUOUS_NAME; } if (result.exitCode !== 0) { return ObsidianErrorType.CLI_ERROR; } return null; } /** * Create actionable error message based on error type */ export function createErrorMessage( type: ObsidianErrorType, originalError: string, context?: Record ): string { switch (type) { case ObsidianErrorType.FILE_NOT_FOUND: return `Note not found. ${originalError}\n\nTip: Use the exact note name or full path. Check for typos or case sensitivity.`; case ObsidianErrorType.VAULT_NOT_FOUND: return `Vault not found. ${originalError}\n\nTip: Ensure the vault name in your configuration matches exactly (case-sensitive). Use 'obsidian vault list' to see available vaults.`; case ObsidianErrorType.OBSIDIAN_NOT_RUNNING: return `Obsidian is not running. Please start the Obsidian application and try again.\n\nOriginal error: ${originalError}`; case ObsidianErrorType.AMBIGUOUS_NAME: return `Multiple notes found with the same name. ${originalError}\n\nTip: Specify the exact path to the note to avoid ambiguity.`; case ObsidianErrorType.CLI_TIMEOUT: return `Operation timed out after ${context?.timeout || 30} seconds.\n\nTip: The operation may still be running. Check Obsidian, or try again with a larger timeout if the vault is very large.`; case ObsidianErrorType.CLI_ERROR: return `Obsidian CLI error: ${originalError}\n\nTip: Check that the Obsidian CLI is properly installed and configured.`; case ObsidianErrorType.VALIDATION_ERROR: return `Invalid parameters: ${originalError}\n\nTip: Check that all required parameters are provided and in the correct format.`; default: return `An unexpected error occurred: ${originalError}`; } } /** * Map error type to MCP error code */ export function getMCPErrorCode(type: ObsidianErrorType): MCPErrorCode { switch (type) { case ObsidianErrorType.VALIDATION_ERROR: return MCPErrorCode.InvalidParams; case ObsidianErrorType.FILE_NOT_FOUND: case ObsidianErrorType.VAULT_NOT_FOUND: case ObsidianErrorType.OBSIDIAN_NOT_RUNNING: case ObsidianErrorType.AMBIGUOUS_NAME: return MCPErrorCode.InvalidParams; case ObsidianErrorType.CLI_TIMEOUT: case ObsidianErrorType.CLI_ERROR: default: return MCPErrorCode.InternalError; } } /** * Handle CLI result and throw error if failed */ export function handleCLIResult(result: CLIResult, context?: Record): void { const errorType = detectErrorType(result); if (errorType) { const errorMessage = createErrorMessage(errorType, result.stderr, context); logger.error('CLI operation failed', { type: errorType, exitCode: result.exitCode, timedOut: result.timedOut, context, }); throw new ObsidianError(errorType, errorMessage, { result, context }); } } /** * Create MCP error response from ObsidianError */ export function createErrorResponse(error: unknown): ErrorResponse { if (error instanceof ObsidianError) { return { content: [ { type: 'text', text: error.message, }, ], isError: true, }; } // Handle validation errors from Zod if (error instanceof Error && error.name === 'ZodError') { return { content: [ { type: 'text', text: createErrorMessage( ObsidianErrorType.VALIDATION_ERROR, error.message ), }, ], isError: true, }; } // Generic error fallback const message = error instanceof Error ? error.message : String(error); logger.error('Unexpected error', { error }); return { content: [ { type: 'text', text: `An unexpected error occurred: ${message}`, }, ], isError: true, }; }