/** * Edit tool — surgical string replacement in an existing file. * * Behavior matches Claude SDK's Edit semantics: refuses if `old_string` isn't * unique (and `replace_all` is false), so the model can't accidentally edit * the wrong occurrence. */ import fs from 'fs'; import type { PiTool } from './types.js'; import { safeResolve, displayPath } from './path-safety.js'; export const editTool: PiTool = { name: 'Edit', description: 'Replace a unique substring in a file. Fails if `old_string` is not found, or if it appears more than once unless `replace_all` is true.', inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: 'File to edit (relative to workspace).' }, old_string: { type: 'string', description: 'The exact text to find. Include enough surrounding context to make it unique.' }, new_string: { type: 'string', description: 'Replacement text.' }, replace_all: { type: 'boolean', description: 'If true, replace every occurrence instead of requiring uniqueness.' }, }, required: ['file_path', 'old_string', 'new_string'], }, async run(input, ctx) { let abs: string; try { abs = safeResolve(ctx.cwd, input?.file_path); } catch (err: any) { return { output: err.message, isError: true }; } if (!fs.existsSync(abs)) { return { output: `File not found: ${displayPath(ctx.cwd, abs)}`, isError: true }; } const oldStr = typeof input?.old_string === 'string' ? input.old_string : ''; const newStr = typeof input?.new_string === 'string' ? input.new_string : ''; if (!oldStr) return { output: 'old_string is required and cannot be empty.', isError: true }; if (oldStr === newStr) return { output: 'old_string and new_string are identical — nothing to change.', isError: true }; const original = fs.readFileSync(abs, 'utf-8'); const occurrences = original.split(oldStr).length - 1; if (occurrences === 0) { return { output: `Did not find old_string in ${displayPath(ctx.cwd, abs)}. Check whitespace/quoting and re-read the file.`, isError: true, }; } if (occurrences > 1 && !input?.replace_all) { return { output: `Found ${occurrences} matches for old_string in ${displayPath(ctx.cwd, abs)}. Add more surrounding context to make it unique, or set replace_all: true.`, isError: true, }; } const updated = input?.replace_all ? original.split(oldStr).join(newStr) : original.replace(oldStr, newStr); try { fs.writeFileSync(abs, updated, 'utf-8'); } catch (err: any) { return { output: `Write failed: ${err.message}`, isError: true }; } return { output: `Edited ${displayPath(ctx.cwd, abs)} (${occurrences} ${occurrences === 1 ? 'match' : 'matches'} replaced).` }; }, };