(args: z.infer<typeof ArgsSchema>, ctx: ToolContext)
| 23 | argsSchema = ArgsSchema; |
| 24 | |
| 25 | async execute(args: z.infer<typeof ArgsSchema>, ctx: ToolContext): Promise<ToolResult> { |
| 26 | const abs = path.isAbsolute(args.path) ? args.path : path.resolve(ctx.cwd, args.path); |
| 27 | const rel = path.relative(ctx.cwd, abs); |
| 28 | |
| 29 | let content: string; |
| 30 | try { |
| 31 | content = await fs.readFile(abs, 'utf-8'); |
| 32 | } catch (e: any) { |
| 33 | return { content: `[ERROR] Cannot read ${args.path}: ${e.message}`, isError: true }; |
| 34 | } |
| 35 | |
| 36 | const original = content; |
| 37 | let applied = 0; |
| 38 | for (let i = 0; i < args.edits.length; i++) { |
| 39 | const edit = args.edits[i]!; |
| 40 | if (!content.includes(edit.old_string)) { |
| 41 | return { |
| 42 | content: `[MULTI_EDIT_FAILED] Edit #${i + 1} failed: old_string not found. ` + |
| 43 | `${applied} previous edits would have been applied — rolling back. Re-read the file and provide accurate old_strings.`, |
| 44 | isError: true, |
| 45 | }; |
| 46 | } |
| 47 | const occ = content.split(edit.old_string).length - 1; |
| 48 | if (occ > 1 && !edit.replace_all) { |
| 49 | return { |
| 50 | content: `[MULTI_EDIT_FAILED] Edit #${i + 1}: old_string appears ${occ} times. Make it unique or set replace_all=true. Rolled back.`, |
| 51 | isError: true, |
| 52 | }; |
| 53 | } |
| 54 | content = edit.replace_all |
| 55 | ? content.split(edit.old_string).join(edit.new_string) |
| 56 | : content.replace(edit.old_string, edit.new_string); |
| 57 | applied++; |
| 58 | } |
| 59 | |
| 60 | if (content === original) { |
| 61 | return { content: `[NO_OP] All edits resulted in no change to ${rel}.`, isError: true }; |
| 62 | } |
| 63 | |
| 64 | // Permission check |
| 65 | const permReq = { tool: 'multi_edit', operation: rel, description: `Multi-edit ${rel} (${args.edits.length} changes)` }; |
| 66 | const decision = ctx.permissions.evaluate(permReq); |
| 67 | if (decision === 'deny') return { content: `[PERMISSION_DENIED]`, isError: true }; |
| 68 | if (decision === 'ask') { |
| 69 | const dec = await confirmEdit(ctx, { |
| 70 | rel, before: original, after: content, absPath: abs, permReq, |
| 71 | label: `Apply ${args.edits.length} edits to ${rel}?`, |
| 72 | }); |
| 73 | if (dec.kind === 'reject') return { content: `[USER_REJECTED]`, isError: true }; |
| 74 | if (dec.kind === 'revise') return reviseResult(rel); |
| 75 | content = dec.content; // may be the user-edited version from [E] Edit |
| 76 | } else { |
| 77 | emitEditDiff(ctx, rel, original, content); |
| 78 | } |
| 79 | |
| 80 | await ctx.transaction.write(abs, content, { base: original, label: rel }); |
| 81 | |
| 82 | return { content: `Applied ${args.edits.length} edits to ${rel}` }; |
nothing calls this directly
no test coverage detected