(args: z.infer<typeof ArgsSchema>, ctx: ToolContext)
| 19 | argsSchema = ArgsSchema; |
| 20 | |
| 21 | async execute(args: z.infer<typeof ArgsSchema>, ctx: ToolContext): Promise<ToolResult> { |
| 22 | const abs = path.isAbsolute(args.path) ? args.path : path.resolve(ctx.cwd, args.path); |
| 23 | const rel = path.relative(ctx.cwd, abs); |
| 24 | |
| 25 | let content: string; |
| 26 | try { |
| 27 | content = await fs.readFile(abs, 'utf-8'); |
| 28 | } catch (e: any) { |
| 29 | if (e.code === 'ENOENT') { |
| 30 | const { notFoundWithSuggestions } = await import('./suggest-paths.js'); |
| 31 | const msg = await notFoundWithSuggestions( |
| 32 | ctx.cwd, args.path, |
| 33 | `[FILE_NOT_FOUND] ${args.path} doesn't exist. Use write_file to create it, or check the path with ls.`, |
| 34 | ); |
| 35 | return { content: msg, isError: true }; |
| 36 | } |
| 37 | return { content: `[ERROR] Cannot read ${args.path}: ${e.message}`, isError: true }; |
| 38 | } |
| 39 | |
| 40 | if (args.old_string === args.new_string) { |
| 41 | return { content: `[ERROR] old_string and new_string are identical — nothing to do.`, isError: true }; |
| 42 | } |
| 43 | |
| 44 | const { findMatch, reindentReplacement } = await import('./fuzzy-match.js'); |
| 45 | |
| 46 | let updated: string; |
| 47 | let occurrences: number; |
| 48 | let tierNote = ''; |
| 49 | |
| 50 | // replace_all keeps exact semantics (it's an explicit "every occurrence" op); |
| 51 | // fuzzy only applies to the single-match path where drift is the usual cause |
| 52 | // of failure. |
| 53 | if (args.replace_all) { |
| 54 | occurrences = content.split(args.old_string).length - 1; |
| 55 | if (occurrences === 0) { |
| 56 | return { |
| 57 | content: `[STRING_NOT_FOUND] old_string was not found in ${rel} (replace_all). ` + |
| 58 | `For a single fuzzy-tolerant edit, omit replace_all.`, |
| 59 | isError: true, |
| 60 | }; |
| 61 | } |
| 62 | updated = content.split(args.old_string).join(args.new_string); |
| 63 | } else { |
| 64 | const match = findMatch(content, args.old_string, { allowFuzzy: true }); |
| 65 | if (!match) { |
| 66 | // Helpful approximate-line hints, same as before. |
| 67 | const lines = content.split('\n'); |
| 68 | const firstLine = args.old_string.split('\n')[0]?.trim(); |
| 69 | const hints: string[] = []; |
| 70 | if (firstLine && firstLine.length > 3) { |
| 71 | for (let i = 0; i < lines.length; i++) { |
| 72 | if (lines[i]?.trim().startsWith(firstLine.slice(0, Math.min(40, firstLine.length)))) { |
| 73 | hints.push(` Line ${i + 1}: ${lines[i]?.trim().slice(0, 80)}`); |
| 74 | if (hints.length >= 3) break; |
| 75 | } |
| 76 | } |
| 77 | } |
| 78 | return { |
nothing calls this directly
no test coverage detected