From 8e2e8f858cf2f3cb2c8ca6a04bfada9305a4e53f Mon Sep 17 00:00:00 2001 From: "Peter.Morton" Date: Sun, 22 Mar 2026 13:09:08 -0500 Subject: [PATCH] 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> --- src/tools/file-operations.ts | 45 ++++++++++++++++++------------------ src/tools/search.ts | 11 +++++---- src/utils/cli-helpers.ts | 44 +++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 27 deletions(-) create mode 100644 src/utils/cli-helpers.ts diff --git a/src/tools/file-operations.ts b/src/tools/file-operations.ts index 41821b0..52df704 100644 --- a/src/tools/file-operations.ts +++ b/src/tools/file-operations.ts @@ -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); diff --git a/src/tools/search.ts b/src/tools/search.ts index f9be64d..e16686d 100644 --- a/src/tools/search.ts +++ b/src/tools/search.ts @@ -10,6 +10,7 @@ import { handleCLIResult } from '../utils/error-handler.js'; import { logger } from '../utils/logger.js'; import { searchSchema } from '../validation/schemas.js'; import { sanitizeParameters } from '../validation/sanitizer.js'; +import { formatParam } from '../utils/cli-helpers.js'; /** * Register all search tools @@ -89,17 +90,17 @@ export async function registerSearchTools(server: ObsidianMCPServer): Promise, + flags: string[] = [] +): string[] { + const args: string[] = []; + + // Add parameters with values + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null) { + args.push(formatParam(key, value)); + } + } + + // Add boolean flags + for (const flag of flags) { + args.push(flag); + } + + return args; +}