(options: EditToolOptions)
| 91 | } |
| 92 | |
| 93 | export function createEditTool(options: EditToolOptions): Tool<z.infer<typeof inputSchema>> { |
| 94 | const { store } = options; |
| 95 | const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; |
| 96 | |
| 97 | return tool({ |
| 98 | description: |
| 99 | "Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes touch the same block, merge them into one edit.", |
| 100 | inputSchema, |
| 101 | execute: async (rawInput: z.infer<typeof inputSchema>) => { |
| 102 | const { path, edits } = prepareArguments(rawInput); |
| 103 | |
| 104 | if (!Array.isArray(edits) || edits.length === 0) { |
| 105 | return { error: "edits must contain at least one replacement." }; |
| 106 | } |
| 107 | |
| 108 | return withFileLock(path, async () => { |
| 109 | try { |
| 110 | const stat = await store.stat(path); |
| 111 | if (!stat) return { error: `File not found: ${path}` }; |
| 112 | if (stat.size > maxBytes) { |
| 113 | return { |
| 114 | error: `File too large to edit: ${stat.size} bytes exceeds the ${maxBytes}-byte cap. Use the write tool to rewrite the file from scratch.`, |
| 115 | }; |
| 116 | } |
| 117 | |
| 118 | const bytes = await store.readAll(path); |
| 119 | if (!bytes) return { error: `File not found: ${path}` }; |
| 120 | |
| 121 | const rawContent = new TextDecoder("utf-8", { fatal: false, ignoreBOM: true }).decode( |
| 122 | bytes, |
| 123 | ); |
| 124 | const { bom, text } = stripBom(rawContent); |
| 125 | const ending = detectLineEnding(text); |
| 126 | const normalized = normalizeToLF(text); |
| 127 | |
| 128 | let baseContent: string; |
| 129 | let newContent: string; |
| 130 | try { |
| 131 | ({ baseContent, newContent } = applyEditsToNormalizedContent(normalized, edits, path)); |
| 132 | } catch (err) { |
| 133 | return { error: err instanceof Error ? err.message : String(err) }; |
| 134 | } |
| 135 | |
| 136 | const finalContent = bom + restoreLineEndings(newContent, ending); |
| 137 | // Round-trip the file's mode so editing an executable script (or any |
| 138 | // file with a non-default mode) doesn't silently drop bits. `stat.mode` |
| 139 | // is undefined for stores that don't track modes; pass `undefined` in |
| 140 | // that case so the store applies its own default. |
| 141 | await store.write(path, new TextEncoder().encode(finalContent), { mode: stat.mode }); |
| 142 | |
| 143 | const diffResult = generateDiffString(baseContent, newContent); |
| 144 | const patch = generateUnifiedPatch(path, baseContent, newContent); |
| 145 | |
| 146 | return { |
| 147 | path, |
| 148 | editsApplied: edits.length, |
| 149 | diff: diffResult.diff, |
| 150 | patch, |
no test coverage detected