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:
@@ -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);
|
||||
|
||||
@@ -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<vo
|
||||
const validated = searchSchema.parse(args) as any;
|
||||
const sanitized = sanitizeParameters(validated) as any;
|
||||
|
||||
const cmdArgs: string[] = ['search'];
|
||||
const cmdArgs: string[] = [];
|
||||
|
||||
// Add query parameter
|
||||
cmdArgs.push(`query=${sanitized.query as string}`);
|
||||
cmdArgs.push(formatParam('query', sanitized.query as string));
|
||||
|
||||
// Add optional parameters
|
||||
if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`);
|
||||
if (sanitized.limit) cmdArgs.push(`limit=${sanitized.limit}`);
|
||||
if (sanitized.path) cmdArgs.push(formatParam('path', sanitized.path as string));
|
||||
if (sanitized.limit) cmdArgs.push(formatParam('limit', sanitized.limit));
|
||||
if (sanitized.total) cmdArgs.push('total');
|
||||
if (sanitized.case) cmdArgs.push('case');
|
||||
if (sanitized.format) cmdArgs.push(`format=${sanitized.format as string}`);
|
||||
if (sanitized.format) cmdArgs.push(formatParam('format', sanitized.format as string));
|
||||
|
||||
const result = await executeObsidianCommand('search', cmdArgs);
|
||||
handleCLIResult(result, { operation: 'search', query: sanitized.query });
|
||||
|
||||
44
src/utils/cli-helpers.ts
Normal file
44
src/utils/cli-helpers.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* CLI Helper Utilities
|
||||
* Functions for building properly formatted Obsidian CLI commands
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format a parameter for Obsidian CLI
|
||||
* Always quotes values to handle spaces and special characters
|
||||
* @param key - Parameter name (e.g., 'file', 'query', 'content')
|
||||
* @param value - Parameter value (will be quoted)
|
||||
* @returns Formatted parameter string (e.g., 'query="my search"')
|
||||
*/
|
||||
export function formatParam(key: string, value: string | number): string {
|
||||
// Always quote string values to handle spaces and special characters safely
|
||||
// Note: Obsidian CLI docs say: "Quote values with spaces: name="My Note""
|
||||
return `${key}="${value}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build command arguments array with proper quoting
|
||||
* @param params - Object with parameter key-value pairs
|
||||
* @param flags - Array of boolean flag names (no values)
|
||||
* @returns Array of command arguments
|
||||
*/
|
||||
export function buildCmdArgs(
|
||||
params: Record<string, string | number | undefined>,
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user