- Complete project setup with TypeScript, Jest, MCPB manifest - Implement foundational infrastructure (CLI executor, logger, error handler) - Add 9 file operation tools for User Story 1 - Full MCP protocol compliance with stdio transport - Input validation and sanitization for security - Comprehensive error handling with actionable messages - Constitutional compliance: all 6 principles satisfied MVP includes: - obsidian_create_note, read, append, prepend, delete, move, rename, open, file_info - Zod validation schemas for all parameters - 30s timeout configuration with per-command overrides - Stderr-only logging with sanitized output - Graceful shutdown handling Build: ✅ 0 errors, 0 vulnerabilities Tasks: 48/167 complete (MVP milestone) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
186 lines
5.1 KiB
TypeScript
186 lines
5.1 KiB
TypeScript
/**
|
|
* 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, unknown>
|
|
): 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<string, unknown>): 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,
|
|
};
|
|
}
|