feat: implement Obsidian MCP Bundle MVP (Phase 1-3)
- 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>
This commit is contained in:
117
src/cli/executor.ts
Normal file
117
src/cli/executor.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* CLI Executor for Obsidian commands
|
||||
* Constitutional Principle IV: Defensive Programming
|
||||
* Uses spawn for better streaming and timeout control
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { CLICommand, CLIResult } from '../utils/types.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { getCommandTimeout } from '../config/timeouts.js';
|
||||
|
||||
/**
|
||||
* Execute an Obsidian CLI command with timeout
|
||||
*/
|
||||
export async function executeCommand(cmd: CLICommand): Promise<CLIResult> {
|
||||
const timeout = cmd.timeout || getCommandTimeout(cmd.command);
|
||||
logger.debug('Executing CLI command', {
|
||||
command: cmd.command,
|
||||
argCount: cmd.args.length,
|
||||
timeout,
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(cmd.command, cmd.args, {
|
||||
cwd: cmd.cwd || process.cwd(),
|
||||
shell: true,
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let timedOut = false;
|
||||
|
||||
// Set timeout
|
||||
const timeoutId = setTimeout(() => {
|
||||
timedOut = true;
|
||||
child.kill('SIGTERM');
|
||||
logger.warn('CLI command timed out', {
|
||||
command: cmd.command,
|
||||
timeout,
|
||||
});
|
||||
}, timeout);
|
||||
|
||||
// Collect stdout
|
||||
child.stdout?.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
// Collect stderr
|
||||
child.stderr?.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
// Handle completion
|
||||
child.on('close', (code) => {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const result: CLIResult = {
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
exitCode: code || 0,
|
||||
timedOut,
|
||||
};
|
||||
|
||||
if (result.exitCode === 0) {
|
||||
logger.debug('CLI command succeeded', {
|
||||
command: cmd.command,
|
||||
outputLength: result.stdout.length,
|
||||
});
|
||||
} else {
|
||||
logger.warn('CLI command failed', {
|
||||
command: cmd.command,
|
||||
exitCode: result.exitCode,
|
||||
timedOut: result.timedOut,
|
||||
});
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
});
|
||||
|
||||
// Handle spawn errors
|
||||
child.on('error', (error) => {
|
||||
clearTimeout(timeoutId);
|
||||
logger.error('CLI command spawn error', { error: error.message });
|
||||
|
||||
resolve({
|
||||
stdout: '',
|
||||
stderr: error.message,
|
||||
exitCode: 1,
|
||||
timedOut: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Obsidian CLI command with vault context
|
||||
*/
|
||||
export async function executeObsidianCommand(
|
||||
subcommand: string,
|
||||
args: string[] = [],
|
||||
options?: { timeout?: number }
|
||||
): Promise<CLIResult> {
|
||||
const vaultName = process.env.OBSIDIAN_VAULT;
|
||||
|
||||
if (!vaultName) {
|
||||
throw new Error('OBSIDIAN_VAULT environment variable not set');
|
||||
}
|
||||
|
||||
// Build full command: obsidian <subcommand> --vault <vault_name> <args>
|
||||
const fullArgs = [subcommand, '--vault', vaultName, ...args];
|
||||
|
||||
return executeCommand({
|
||||
command: 'obsidian',
|
||||
args: fullArgs,
|
||||
timeout: options?.timeout,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user