fix: properly quote CLI parameters to handle filenames with spaces

Fixed parameter quoting in file operations and search tools to handle
filenames and values containing spaces correctly.

Root Cause:
- Obsidian CLI requires quoting values with spaces: name="My Note"
- Previous implementation used unquoted format: name=My Note
- Shell would split on spaces, breaking multi-word filenames

Solution:
1. Created formatParam() helper in src/utils/cli-helpers.ts
   - Always quotes parameter values: param="value"
   - Handles spaces and special characters safely

2. Updated file-operations.ts (8 tools):
   - All file/path/content/name parameters now quoted
   - create, read, append, prepend, delete, move, rename, open

3. Updated search.ts (1 tool):
   - query, path, format, limit parameters now quoted
   - Fixes searches with multi-word queries

Changes:
- Before: cmdArgs.push(\`file=${name}\`)
- After: cmdArgs.push(formatParam('file', name))

Files changed:
- src/utils/cli-helpers.ts (new): formatParam() and buildCmdArgs() helpers
- src/tools/file-operations.ts: Use formatParam() for all parameters
- src/tools/search.ts: Use formatParam() for all parameters

Impact:
- File operations now work with multi-word filenames
- Search queries with spaces now work correctly
- Content parameters with newlines/special chars handled safely

Known Issue:
- links.ts, tags-aliases.ts, properties.ts still need similar fixes
- These tools have additional structural issues (wrong command names)
- Will be addressed in follow-up commit

Build: 0 errors

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-03-22 13:09:08 -05:00
parent d556183388
commit 8e2e8f858c
3 changed files with 73 additions and 27 deletions

View File

@@ -17,6 +17,7 @@ import {
fileIdentifierSchema,
} from '../validation/schemas.js';
import { sanitizeParameters } from '../validation/sanitizer.js';
import { formatParam } from '../utils/cli-helpers.js';
/**
* Register all file operation tools
@@ -103,10 +104,10 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
const cmdArgs: string[] = [];
// Add name or path parameter
if (sanitized.name) cmdArgs.push(`name=${sanitized.name as string}`);
if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`);
if (sanitized.content) cmdArgs.push(`content=${sanitized.content as string}`);
if (sanitized.template) cmdArgs.push(`template=${sanitized.template as string}`);
if (sanitized.name) cmdArgs.push(formatParam('name', sanitized.name as string));
if (sanitized.path) cmdArgs.push(formatParam('path', sanitized.path as string));
if (sanitized.content) cmdArgs.push(formatParam('content', sanitized.content as string));
if (sanitized.template) cmdArgs.push(formatParam('template', sanitized.template as string));
if (sanitized.overwrite) cmdArgs.push('overwrite');
if (sanitized.open) cmdArgs.push('open');
if (sanitized.newtab) cmdArgs.push('newtab');
@@ -163,8 +164,8 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
const sanitized = sanitizeParameters(validated) as any;
const cmdArgs: string[] = [];
if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`);
if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`);
if (sanitized.file) cmdArgs.push(formatParam('file', sanitized.file as string));
if (sanitized.path) cmdArgs.push(formatParam('path', sanitized.path as string));
const result = await executeObsidianCommand('read', cmdArgs);
handleCLIResult(result, { operation: 'read_note', identifier: sanitized.file || sanitized.path });
@@ -236,9 +237,9 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
const sanitized = sanitizeParameters(validated) as any;
const cmdArgs: string[] = [];
if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`);
if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`);
cmdArgs.push(`content=${sanitized.content as string}`);
if (sanitized.file) cmdArgs.push(formatParam('file', sanitized.file as string));
if (sanitized.path) cmdArgs.push(formatParam('path', sanitized.path as string));
cmdArgs.push(formatParam('content', sanitized.content as string));
if (sanitized.inline) cmdArgs.push('inline');
const result = await executeObsidianCommand('append', cmdArgs);
@@ -288,9 +289,9 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
const sanitized = sanitizeParameters(validated) as any;
const cmdArgs: string[] = [];
if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`);
if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`);
cmdArgs.push(`content=${sanitized.content as string}`);
if (sanitized.file) cmdArgs.push(formatParam('file', sanitized.file as string));
if (sanitized.path) cmdArgs.push(formatParam('path', sanitized.path as string));
cmdArgs.push(formatParam('content', sanitized.content as string));
if (sanitized.inline) cmdArgs.push('inline');
const result = await executeObsidianCommand('prepend', cmdArgs);
@@ -335,8 +336,8 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
const sanitized = sanitizeParameters(validated) as any;
const cmdArgs: string[] = [];
if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`);
if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`);
if (sanitized.file) cmdArgs.push(formatParam('file', sanitized.file as string));
if (sanitized.path) cmdArgs.push(formatParam('path', sanitized.path as string));
if (sanitized.permanent) cmdArgs.push('permanent');
const result = await executeObsidianCommand('delete', cmdArgs);
@@ -383,9 +384,9 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
const sanitized = sanitizeParameters(validated) as any;
const cmdArgs: string[] = [];
if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`);
if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`);
cmdArgs.push(`to=${(sanitized.to || sanitized.newPath) as string}`);
if (sanitized.file) cmdArgs.push(formatParam('file', sanitized.file as string));
if (sanitized.path) cmdArgs.push(formatParam('path', sanitized.path as string));
cmdArgs.push(formatParam('to', (sanitized.to || sanitized.newPath) as string));
const result = await executeObsidianCommand('move', cmdArgs);
handleCLIResult(result, { operation: 'move', identifier: sanitized.file || sanitized.path, to: sanitized.to });
@@ -431,9 +432,9 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
const sanitized = sanitizeParameters(validated) as any;
const cmdArgs: string[] = [];
if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`);
if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`);
cmdArgs.push(`name=${(sanitized.name || sanitized.newName) as string}`);
if (sanitized.file) cmdArgs.push(formatParam('file', sanitized.file as string));
if (sanitized.path) cmdArgs.push(formatParam('path', sanitized.path as string));
cmdArgs.push(formatParam('name', (sanitized.name || sanitized.newName) as string));
const result = await executeObsidianCommand('rename', cmdArgs);
handleCLIResult(result, { operation: 'rename', identifier: sanitized.file || sanitized.path, name: sanitized.name });
@@ -477,8 +478,8 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
const sanitized = sanitizeParameters(validated) as any;
const cmdArgs: string[] = [];
if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`);
if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`);
if (sanitized.file) cmdArgs.push(formatParam('file', sanitized.file as string));
if (sanitized.path) cmdArgs.push(formatParam('path', sanitized.path as string));
if ((args as any).newtab) cmdArgs.push('newtab');
const result = await executeObsidianCommand('open', cmdArgs);