edit
Perform smart editing operations on files and directories, including window buffering, appending, patching, line-specific changes, and buffer transfers. Supports fuzzy matching, targeted text replacement, and precise content manipulation.
Instructions
Smart editing operations - window (auto-buffers content), append, patch, at_line, from_buffer
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| action | Yes | The specific action to perform | |
| content | No | Content to write or append | |
| fuzzyThreshold | No | Similarity threshold for fuzzy matching (0-1) | |
| lineNumber | No | Line number for at_line action | |
| mode | No | Insert mode for at_line action | |
| newText | No | Text to replace with | |
| oldText | No | Text to search for (supports fuzzy matching) | |
| operation | No | Patch operation: append (add after), prepend (add before), or replace | |
| path | No | Path to the file or directory | |
| target | No | Target identifier - e.g., "Daily Notes::Today" for heading, block ID, or frontmatter field name | |
| targetType | No | What to target: heading (by path like "H1::H2"), block (by ID), or frontmatter (field) |
Input Schema (JSON Schema)
{
"properties": {
"action": {
"description": "The specific action to perform",
"enum": [
"window",
"append",
"patch",
"at_line",
"from_buffer"
],
"type": "string"
},
"content": {
"description": "Content to write or append",
"type": "string"
},
"fuzzyThreshold": {
"default": 0.7,
"description": "Similarity threshold for fuzzy matching (0-1)",
"type": "number"
},
"lineNumber": {
"description": "Line number for at_line action",
"type": "number"
},
"mode": {
"description": "Insert mode for at_line action",
"enum": [
"before",
"after",
"replace"
],
"type": "string"
},
"newText": {
"description": "Text to replace with",
"type": "string"
},
"oldText": {
"description": "Text to search for (supports fuzzy matching)",
"type": "string"
},
"operation": {
"description": "Patch operation: append (add after), prepend (add before), or replace",
"enum": [
"append",
"prepend",
"replace"
],
"type": "string"
},
"path": {
"description": "Path to the file or directory",
"type": "string"
},
"target": {
"description": "Target identifier - e.g., \"Daily Notes::Today\" for heading, block ID, or frontmatter field name",
"type": "string"
},
"targetType": {
"description": "What to target: heading (by path like \"H1::H2\"), block (by ID), or frontmatter (field)",
"enum": [
"heading",
"block",
"frontmatter"
],
"type": "string"
}
},
"required": [
"action"
],
"type": "object"
}
Implementation Reference
- src/semantic/router.ts:503-585 (handler)Core handler for 'edit' tool operations - implements window (fuzzy replace), append, patch, at_line (insert/replace), from_buffer actionsprivate async executeEditOperation(action: string, params: any): Promise<any> { // Import window edit tools dynamically to avoid circular dependencies const { performWindowEdit } = await import('../tools/window-edit.js'); const buffer = ContentBufferManager.getInstance(); switch (action) { case 'window': const result = await performWindowEdit( this.api, params.path, params.oldText, params.newText, params.fuzzyThreshold ); if (result.isError) { throw new Error(result.content[0].text); } return result; case 'append': return await this.api.appendToFile(params.path, params.content); case 'patch': return await this.api.patchVaultFile(params.path, { operation: params.operation, targetType: params.targetType, target: params.target, content: params.content }); case 'at_line': // Get content to insert let insertContent = params.content; if (!insertContent) { const buffered = buffer.retrieve(); if (!buffered) { throw new Error('No content provided and no buffered content found'); } insertContent = buffered.content; } // Get file and perform line-based edit const file = await this.api.getFile(params.path); if (isImageFile(file)) { throw new Error('Cannot perform line-based edits on image files'); } const content = typeof file === 'string' ? file : file.content; const lines = content.split('\n'); if (params.lineNumber < 1 || params.lineNumber > lines.length + 1) { throw new Error(`Invalid line number ${params.lineNumber}. File has ${lines.length} lines.`); } const lineIndex = params.lineNumber - 1; const mode = params.mode || 'replace'; switch (mode) { case 'before': lines.splice(lineIndex, 0, insertContent); break; case 'after': lines.splice(lineIndex + 1, 0, insertContent); break; case 'replace': lines[lineIndex] = insertContent; break; } await this.api.updateFile(params.path, lines.join('\n')); return { success: true, line: params.lineNumber, mode }; case 'from_buffer': const buffered = buffer.retrieve(); if (!buffered) { throw new Error('No buffered content available'); } return await performWindowEdit( this.api, params.path, params.oldText || buffered.searchText || '', buffered.content, params.fuzzyThreshold ); default: throw new Error(`Unknown edit action: ${action}`); } }
- src/tools/semantic-tools.ts:25-75 (handler)MCP tool handler for 'edit' - instantiates SemanticRouter and calls route with operation='edit', action=args.actionhandler: async (api: ObsidianAPI, args: any) => { const router = new SemanticRouter(api); const request: SemanticRequest = { operation, action: args.action, params: args }; const response = await router.route(request); // Format for MCP if (response.error) { return { content: [{ type: 'text', text: JSON.stringify({ error: response.error, workflow: response.workflow, context: response.context }, null, 2) }], isError: true }; } // Check if the result is an image file for vault read operations if (operation === 'vault' && args.action === 'read' && response.result && isImageFile(response.result)) { // Return image content for MCP return { content: [{ type: 'image', data: response.result.base64Data, mimeType: response.result.mimeType }] }; } return { content: [{ type: 'text', text: JSON.stringify({ result: response.result, workflow: response.workflow, context: response.context, efficiency_hints: response.efficiency_hints }, null, 2) }] }; } });
- src/tools/semantic-tools.ts:154-193 (schema)Input schema properties for 'edit' tool parameters (path, content, oldText, newText, fuzzyThreshold, lineNumber, mode, operation, targetType, target)edit: { ...pathParam, ...contentParam, oldText: { type: 'string', description: 'Text to search for (supports fuzzy matching)' }, newText: { type: 'string', description: 'Text to replace with' }, fuzzyThreshold: { type: 'number', description: 'Similarity threshold for fuzzy matching (0-1)', default: 0.7 }, lineNumber: { type: 'number', description: 'Line number for at_line action' }, mode: { type: 'string', enum: ['before', 'after', 'replace'], description: 'Insert mode for at_line action' }, operation: { type: 'string', enum: ['append', 'prepend', 'replace'], description: 'Patch operation: append (add after), prepend (add before), or replace' }, targetType: { type: 'string', enum: ['heading', 'block', 'frontmatter'], description: 'What to target: heading (by path like "H1::H2"), block (by ID), or frontmatter (field)' }, target: { type: 'string', description: 'Target identifier - e.g., "Daily Notes::Today" for heading, block ID, or frontmatter field name' } },
- src/tools/semantic-tools.ts:89-97 (schema)Action enum for 'edit' tool: ['window', 'append', 'patch', 'at_line', 'from_buffer'] used in inputSchemaconst actions: Record<string, string[]> = { vault: ['list', 'read', 'create', 'update', 'delete', 'search', 'fragments'], edit: ['window', 'append', 'patch', 'at_line', 'from_buffer'], view: ['file', 'window', 'active', 'open_in_obsidian'], workflow: ['suggest'], system: ['info', 'commands', 'fetch_web'] }; return actions[operation] || []; }
- src/tools/semantic-tools.ts:227-234 (registration)Registration of 'edit' tool via createSemanticTool('edit') in semanticTools export// Export the 5 semantic tools export const semanticTools = [ createSemanticTool('vault'), createSemanticTool('edit'), createSemanticTool('view'), createSemanticTool('workflow'), createSemanticTool('system') ];
- src/tools/window-edit.ts:7-88 (helper)performWindowEdit helper function used by edit:window and edit:from_buffer for fuzzy text replacement in filesexport async function performWindowEdit( api: ObsidianAPI, path: string, oldText: string, newText: string, fuzzyThreshold: number = 0.7 ) { const buffer = ContentBufferManager.getInstance(); // Get current file content const file = await api.getFile(path); if (isImageFile(file)) { throw new Error('Cannot perform window edits on image files'); } const content = typeof file === 'string' ? file : file.content; // Try exact match first if (content.includes(oldText)) { const newContent = content.replace(oldText, newText); await api.updateFile(path, newContent); return { content: [{ type: 'text', text: `Successfully replaced exact match in ${path}` }] }; } // Buffer the new content for potential recovery buffer.store(newText, undefined, { filePath: path, searchText: oldText }); // Try fuzzy matching const matches = findFuzzyMatches(content, oldText, fuzzyThreshold); if (matches.length === 0) { // No matches found, provide helpful feedback return { content: [{ type: 'text', text: `No matches found for "${oldText}" in ${path}. ` + `Content has been buffered. You can use edit_vault_from_buffer to retry ` + `with different search text or insert_vault_at_line to insert at a specific line.` }], isError: true }; } // If multiple matches, ask for clarification if (matches.length > 1) { const matchList = matches.map(m => `Line ${m.lineNumber} (${Math.round(m.similarity * 100)}% match): "${m.line.trim()}"` ).join('\n'); return { content: [{ type: 'text', text: `Found ${matches.length} potential matches:\n\n${matchList}\n\n` + `Content has been buffered. Use insert_vault_at_line with the specific line number.` }], isError: true }; } // Single match found - replace the entire line const match = matches[0]; const lines = content.split('\n'); lines[match.lineNumber - 1] = newText; const newContent = lines.join('\n'); await api.updateFile(path, newContent); return { content: [{ type: 'text', text: `Successfully replaced line ${match.lineNumber} (${Math.round(match.similarity * 100)}% match) in ${path}` }] }; }