@ottocode/sdk 0.1.232 → 0.1.233

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.232",
3
+ "version": "0.1.233",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -37,10 +37,6 @@
37
37
  "import": "./src/core/src/tools/builtin/bash.ts",
38
38
  "types": "./src/core/src/tools/builtin/bash.ts"
39
39
  },
40
- "./tools/builtin/edit": {
41
- "import": "./src/core/src/tools/builtin/edit.ts",
42
- "types": "./src/core/src/tools/builtin/edit.ts"
43
- },
44
40
  "./tools/builtin/finish": {
45
41
  "import": "./src/core/src/tools/builtin/finish.ts",
46
42
  "types": "./src/core/src/tools/builtin/finish.ts"
@@ -50,8 +50,6 @@ export type {
50
50
  export { buildFsTools } from './tools/builtin/fs/index';
51
51
  export { buildGitTools } from './tools/builtin/git';
52
52
  export { buildTerminalTool } from './tools/builtin/terminal';
53
- export { buildEditTool } from './tools/builtin/edit';
54
- export { buildMultiEditTool } from './tools/builtin/multiedit';
55
53
 
56
54
  // =======================
57
55
  // Terminals
@@ -6,6 +6,6 @@
6
6
  Usage tips:
7
7
  - Use for creating NEW files
8
8
  - Use when replacing >70% of a file's content (almost complete rewrite)
9
- - NEVER use for partial/targeted edits - use apply_patch or edit instead
9
+ - NEVER use for partial/targeted edits - use apply_patch instead
10
10
  - Using write for partial edits wastes output tokens and risks hallucinating unchanged parts
11
11
  - Prefer idempotent writes by providing the full intended content when you do use write
@@ -8,8 +8,6 @@ import { buildBashTool } from './builtin/bash.ts';
8
8
  import { buildRipgrepTool } from './builtin/ripgrep.ts';
9
9
  import { buildGlobTool } from './builtin/glob.ts';
10
10
  import { buildApplyPatchTool } from './builtin/patch.ts';
11
- import { buildEditTool } from './builtin/edit.ts';
12
- import { buildMultiEditTool } from './builtin/multiedit.ts';
13
11
  import { updateTodosTool } from './builtin/todos.ts';
14
12
  import { buildWebSearchTool } from './builtin/websearch.ts';
15
13
  import { buildTerminalTool } from './builtin/terminal.ts';
@@ -136,11 +134,6 @@ export async function discoverProjectTools(
136
134
  // Patch/apply
137
135
  const ap = buildApplyPatchTool(projectRoot);
138
136
  tools.set(ap.name, ap.tool);
139
- // Edit (fuzzy string replacement)
140
- const ed = buildEditTool(projectRoot);
141
- tools.set(ed.name, ed.tool);
142
- const med = buildMultiEditTool(projectRoot);
143
- tools.set(med.name, med.tool);
144
137
  // Todo tracking
145
138
  tools.set('update_todos', updateTodosTool);
146
139
  // Web search
package/src/index.ts CHANGED
@@ -234,8 +234,6 @@ export {
234
234
  OTTOCODE_BOT_EMAIL,
235
235
  OTTOCODE_CO_AUTHOR,
236
236
  } from './core/src/tools/builtin/git-identity.ts';
237
- export { buildEditTool } from './core/src/index.ts';
238
- export { buildMultiEditTool } from './core/src/index.ts';
239
237
 
240
238
  // Terminals
241
239
  export { TerminalManager } from './core/src/index.ts';
@@ -202,15 +202,7 @@ Key points:
202
202
  ## When Patch Fails
203
203
  - Error means context didn't match or file changed
204
204
  - Solution: Read the file AGAIN, copy context character-by-character
205
- - After 2 consecutive failures on the same file: switch to the `edit` tool instead it uses fuzzy matching (oldString/newString) and tolerates whitespace/indentation mismatches
206
- - If `edit` also fails, use `write` tool to rewrite the entire file
207
-
208
- ## Using the `edit` Tool (Recommended Fallback)
209
- - Parameters: `filePath`, `oldString`, `newString`, `replaceAll` (optional)
210
- - Uses 9 fuzzy matching strategies: trims whitespace, normalizes indentation, anchors on first/last lines, etc.
211
- - Much more forgiving than `apply_patch` — handles trailing commas, whitespace differences, indentation mismatches
212
- - For multiple edits to the same file, use `multiedit` with an array of `{oldString, newString}` pairs
213
- - To create a new file: use empty `oldString` with `newString` as the file contents
205
+ - After repeated failures on the same file: use `write` only if a full-file rewrite is the safer option
214
206
 
215
207
  ## Using the `write` Tool (Last Resort)
216
208
  - Use for creating NEW files
@@ -21,4 +21,4 @@ File Editing Best Practices:
21
21
  - When making multiple edits to the same file, use multiple `@@` hunks in a single `apply_patch` call
22
22
  - Never assume file content remains unchanged between separate apply_patch operations
23
23
  - If a patch fails, read the file AGAIN and copy the exact lines
24
- - If `apply_patch` fails 2+ times on the same file, switch to the `edit` tool (oldString/newString with fuzzy matching) or `write` to rewrite the file
24
+ - If `apply_patch` fails repeatedly on the same file, fall back to `write` only when a full-file rewrite is appropriate
@@ -262,9 +262,7 @@ File (10 spaces): - os: linux
262
262
  - Don't retry with same context - read file AGAIN first
263
263
  - Check that context lines exist exactly as written
264
264
  - Verify indentation matches (spaces vs tabs)
265
- - If failing 2+ times, switch to the `edit` tool it uses fuzzy matching (oldString/newString) and tolerates whitespace/indentation mismatches
266
- - For multiple edits to the same file, use `multiedit` with an array of `{oldString, newString}` pairs
267
- - If `edit` also fails, use `write` tool to rewrite the entire file instead
265
+ - If failing repeatedly, use `write` tool to rewrite the entire file only when a full rewrite is the safest option
268
266
 
269
267
  # Code References
270
268
 
@@ -21,8 +21,6 @@ You have access to a rich set of specialized tools optimized for coding tasks:
21
21
  - `read`: Read file contents (supports line ranges)
22
22
  - `write`: Write complete file contents
23
23
  - `apply_patch`: Apply unified diff patches (RECOMMENDED for targeted edits)
24
- - `edit`: Fuzzy string replacement (oldString → newString) — use when `apply_patch` fails
25
- - `multiedit`: Batch multiple `edit` operations on a single file
26
24
 
27
25
  **Version Control**:
28
26
  - `git_status`, `git_diff`
@@ -78,13 +76,6 @@ You have access to a rich set of specialized tools optimized for coding tasks:
78
76
  more context ← More context (space prefix)
79
77
  ```
80
78
 
81
- **Using the `edit` Tool** (Recommended Fallback When Patch Fails):
82
- - Parameters: `filePath`, `oldString`, `newString`, `replaceAll` (optional)
83
- - Uses 9 fuzzy matching strategies: trims whitespace, normalizes indentation, anchors on first/last lines
84
- - Much more forgiving than `apply_patch` — handles trailing commas, whitespace differences, indentation mismatches
85
- - For multiple edits to the same file, use `multiedit` with an array of `{oldString, newString}` pairs
86
- - To create a new file: use empty `oldString` with `newString` as the file contents
87
-
88
79
  **Using the `write` Tool** (Last Resort):
89
80
  - Use for creating NEW files
90
81
  - Use when replacing >70% of a file's content (almost complete rewrite)
@@ -474,8 +465,7 @@ File (10 spaces): - os: linux
474
465
 
475
466
  **If Patch Fails:**
476
467
  - Read file AGAIN, copy context character-by-character
477
- - After 2 failures: switch to `edit` tool it uses fuzzy matching and tolerates whitespace/indentation mismatches
478
- - If `edit` also fails: use `write` tool to rewrite the entire file instead
468
+ - After repeated failures: use `write` tool to rewrite the entire file only if a full-file rewrite is justified
479
469
 
480
470
  ## `update_todos`
481
471
 
@@ -37,8 +37,6 @@ You have access to a rich set of specialized tools optimized for coding tasks:
37
37
  - `read`: Read file contents (supports line ranges)
38
38
  - `write`: Write complete file contents
39
39
  - `apply_patch`: Apply unified diff patches (RECOMMENDED for targeted edits)
40
- - `edit`: Fuzzy string replacement (oldString → newString) — use when `apply_patch` fails
41
- - `multiedit`: Batch multiple `edit` operations on a single file
42
40
 
43
41
  **Version Control**:
44
42
  - `git_status`, `git_diff`
@@ -167,13 +165,6 @@ You have access to a rich set of specialized tools optimized for coding tasks:
167
165
  - Lines starting with ` ` (space) are context (kept unchanged)
168
166
  - Lines starting with `@@` are optional hints/comments (not parsed as context)
169
167
 
170
- **Using the `edit` Tool** (Recommended Fallback When Patch Fails):
171
- - Parameters: `filePath`, `oldString`, `newString`, `replaceAll` (optional)
172
- - Uses 9 fuzzy matching strategies: trims whitespace, normalizes indentation, anchors on first/last lines
173
- - Much more forgiving than `apply_patch` — handles trailing commas, whitespace differences, indentation mismatches
174
- - For multiple edits to the same file, use `multiedit` with an array of `{oldString, newString}` pairs
175
- - To create a new file: use empty `oldString` with `newString` as the file contents
176
-
177
168
  **Using the `write` Tool** (Last Resort):
178
169
  - Use for creating NEW files
179
170
  - Use when replacing >70% of a file's content (almost complete rewrite)
@@ -300,7 +291,7 @@ GLM's reasoning can cause it to "reconstruct" code from training data rather tha
300
291
 
301
292
  When patching files that contain markdown code fences (` ``` `), the fence characters can interfere with the `*** End Patch` marker detection, causing the entire patch to fail.
302
293
 
303
- **Rule: When patching files that contain ` ``` ` (like README.md), prefer patching them in a SEPARATE `apply_patch` call from other files, or use the `edit` tool instead.**
294
+ **Rule: When patching files that contain ` ``` ` (like README.md), prefer patching them in a SEPARATE `apply_patch` call from other files. If patching remains too fragile, consider a full-file `write` only when the rewrite is justified.**
304
295
 
305
296
  ### Pitfall 4: Indentation Mismatch
306
297
 
@@ -338,10 +329,10 @@ Before calling `apply_patch`, verify ALL of these:
338
329
  ### Escalation Strategy When Patch Fails:
339
330
 
340
331
  1. **First failure**: Read the file AGAIN. Copy context character-by-character. Pay special attention to blank lines.
341
- 2. **Second failure**: Switch to the `edit` tool it uses fuzzy matching (oldString/newString) and tolerates whitespace/indentation mismatches. For multiple edits, use `multiedit`.
342
- 3. **Third failure**: Use the `write` tool to rewrite the entire file.
332
+ 2. **Second failure**: Re-read the exact target lines and simplify the patch further.
333
+ 3. **Third failure**: Use the `write` tool to rewrite the entire file only if a full-file rewrite is warranted.
343
334
 
344
- **For Markdown files (README.md, docs, etc.)**: Consider using `edit` as the primary tool instead of `apply_patch`. Markdown files have many blank lines and code fences that make patch context matching fragile.
335
+ **For Markdown files (README.md, docs, etc.)**: Keep patches small and isolated. Markdown files have many blank lines and code fences that make patch context matching fragile.
345
336
 
346
337
  ## YAML Files — Extra Caution Required
347
338
  - YAML uses spaces ONLY (never tabs) — verify exact space count
@@ -269,6 +269,4 @@ File (10 spaces): - os: linux
269
269
 
270
270
  **If Patch Fails:**
271
271
  - Read file AGAIN, copy context character-by-character
272
- - After 2 failures: switch to `edit` tool it uses fuzzy matching (oldString/newString) and tolerates whitespace/indentation mismatches
273
- - For multiple edits to the same file, use `multiedit` with an array of `{oldString, newString}` pairs
274
- - If `edit` also fails: use `write` tool to rewrite the entire file instead
272
+ - After repeated failures: use `write` tool to rewrite the entire file only when a full rewrite is appropriate
@@ -37,8 +37,6 @@ You have access to a rich set of specialized tools optimized for coding tasks:
37
37
  - `read`: Read file contents (supports line ranges)
38
38
  - `write`: Write complete file contents
39
39
  - `apply_patch`: Apply unified diff patches (RECOMMENDED for targeted edits)
40
- - `edit`: Fuzzy string replacement (oldString → newString) — use when `apply_patch` fails
41
- - `multiedit`: Batch multiple `edit` operations on a single file
42
40
 
43
41
  **Version Control**:
44
42
  - `git_status`, `git_diff`
@@ -167,13 +165,6 @@ You have access to a rich set of specialized tools optimized for coding tasks:
167
165
  - Lines starting with ` ` (space) are context (kept unchanged)
168
166
  - Lines starting with `@@` are optional hints/comments (not parsed as context)
169
167
 
170
- **Using the `edit` Tool** (Recommended Fallback When Patch Fails):
171
- - Parameters: `filePath`, `oldString`, `newString`, `replaceAll` (optional)
172
- - Uses 9 fuzzy matching strategies: trims whitespace, normalizes indentation, anchors on first/last lines
173
- - Much more forgiving than `apply_patch` — handles trailing commas, whitespace differences, indentation mismatches
174
- - For multiple edits to the same file, use `multiedit` with an array of `{oldString, newString}` pairs
175
- - To create a new file: use empty `oldString` with `newString` as the file contents
176
-
177
168
  **Using the `write` Tool** (Last Resort):
178
169
  - Use for creating NEW files
179
170
  - Use when replacing >70% of a file's content (almost complete rewrite)
@@ -296,7 +287,7 @@ When a patch fails, do NOT retry the same approach. Instead:
296
287
  2. **Re-read the exact lines** you need (use startLine/endLine)
297
288
  3. **Copy context verbatim** from the fresh read output
298
289
  4. **Patch only one contiguous block** per hunk
299
- 5. If it fails twice more, switch to `edit` tool (fuzzy matching)
290
+ 5. If it still fails repeatedly, only then consider a full-file `write` when the rewrite is justified
300
291
 
301
292
  **Pre-Flight Checklist (EVERY call):**
302
293
  Before calling `apply_patch`, verify ALL of these:
@@ -364,7 +355,7 @@ Small, surgical, one contiguous region. This pattern succeeds reliably.
364
355
  *** Begin Patch
365
356
  *** Update File: src/capture.ts
366
357
  @@ after MUTATING_TOOLS constant
367
- const MUTATING_TOOLS = new Set(['write', 'apply_patch', 'edit']);
358
+ const MUTATING_TOOLS = new Set(['write', 'apply_patch']);
368
359
 
369
360
  +const RETRYABLE_ERRORS = new Set([
370
361
  + 'ECONNREFUSED',
@@ -402,8 +393,7 @@ Two non-adjacent edits, one patch call, tabs preserved.
402
393
  - Step 1: Read file AGAIN with `read` tool (use startLine/endLine to get just the relevant section)
403
394
  - Step 2: Copy context lines VERBATIM from fresh read — do NOT retype from memory
404
395
  - Step 3: Reduce the patch to the MINIMUM change (2-3 context lines, one contiguous block)
405
- - After 2+ failures: switch to `edit` tool it uses fuzzy matching and tolerates whitespace/indentation mismatches
406
- - If `edit` also fails: use `write` tool to rewrite the entire file instead
396
+ - After repeated failures: use `write` tool to rewrite the entire file only when a full rewrite is warranted
407
397
 
408
398
  ## Validating Your Work
409
399
 
@@ -419,9 +419,7 @@ File (10 spaces): - os: linux
419
419
 
420
420
  **If Your Patch Fails:**
421
421
  - Read file AGAIN, copy context character-by-character
422
- - After 2 failures: switch to `edit` tool it uses fuzzy matching (oldString/newString) and tolerates whitespace/indentation mismatches
423
- - For multiple edits to the same file, use `multiedit` with an array of `{oldString, newString}` pairs
424
- - If `edit` also fails: use `write` to rewrite the entire file instead
422
+ - After repeated failures: use `write` only if a full-file rewrite is the safer option
425
423
 
426
424
  ## `update_todos`
427
425
 
@@ -1,461 +0,0 @@
1
- export type Replacer = (
2
- content: string,
3
- find: string,
4
- ) => Generator<string, void, unknown>;
5
-
6
- const SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.0;
7
- const MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.3;
8
-
9
- function levenshtein(a: string, b: string): number {
10
- if (a === '' || b === '') {
11
- return Math.max(a.length, b.length);
12
- }
13
- const matrix = Array.from({ length: a.length + 1 }, (_, i) =>
14
- Array.from({ length: b.length + 1 }, (_, j) =>
15
- i === 0 ? j : j === 0 ? i : 0,
16
- ),
17
- );
18
-
19
- for (let i = 1; i <= a.length; i++) {
20
- for (let j = 1; j <= b.length; j++) {
21
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
22
- matrix[i][j] = Math.min(
23
- matrix[i - 1][j] + 1,
24
- matrix[i][j - 1] + 1,
25
- matrix[i - 1][j - 1] + cost,
26
- );
27
- }
28
- }
29
- return matrix[a.length][b.length];
30
- }
31
-
32
- export const SimpleReplacer: Replacer = function* (_content, find) {
33
- yield find;
34
- };
35
-
36
- export const LineTrimmedReplacer: Replacer = function* (content, find) {
37
- const originalLines = content.split('\n');
38
- const searchLines = find.split('\n');
39
-
40
- if (searchLines[searchLines.length - 1] === '') {
41
- searchLines.pop();
42
- }
43
-
44
- for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
45
- let matches = true;
46
-
47
- for (let j = 0; j < searchLines.length; j++) {
48
- const originalTrimmed = originalLines[i + j].trim();
49
- const searchTrimmed = searchLines[j].trim();
50
-
51
- if (originalTrimmed !== searchTrimmed) {
52
- matches = false;
53
- break;
54
- }
55
- }
56
-
57
- if (matches) {
58
- let matchStartIndex = 0;
59
- for (let k = 0; k < i; k++) {
60
- matchStartIndex += originalLines[k].length + 1;
61
- }
62
-
63
- let matchEndIndex = matchStartIndex;
64
- for (let k = 0; k < searchLines.length; k++) {
65
- matchEndIndex += originalLines[i + k].length;
66
- if (k < searchLines.length - 1) {
67
- matchEndIndex += 1;
68
- }
69
- }
70
-
71
- yield content.substring(matchStartIndex, matchEndIndex);
72
- }
73
- }
74
- };
75
-
76
- export const BlockAnchorReplacer: Replacer = function* (content, find) {
77
- const originalLines = content.split('\n');
78
- const searchLines = find.split('\n');
79
-
80
- if (searchLines.length < 3) {
81
- return;
82
- }
83
-
84
- if (searchLines[searchLines.length - 1] === '') {
85
- searchLines.pop();
86
- }
87
-
88
- const firstLineSearch = searchLines[0].trim();
89
- const lastLineSearch = searchLines[searchLines.length - 1].trim();
90
-
91
- const candidates: Array<{ startLine: number; endLine: number }> = [];
92
- for (let i = 0; i < originalLines.length; i++) {
93
- if (originalLines[i].trim() !== firstLineSearch) {
94
- continue;
95
- }
96
-
97
- for (let j = i + 2; j < originalLines.length; j++) {
98
- if (originalLines[j].trim() === lastLineSearch) {
99
- candidates.push({ startLine: i, endLine: j });
100
- break;
101
- }
102
- }
103
- }
104
-
105
- if (candidates.length === 0) {
106
- return;
107
- }
108
-
109
- if (candidates.length === 1) {
110
- const { startLine, endLine } = candidates[0];
111
- const actualBlockSize = endLine - startLine + 1;
112
- const searchBlockSize = searchLines.length;
113
-
114
- let similarity = 0;
115
- const linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2);
116
-
117
- if (linesToCheck > 0) {
118
- for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
119
- const originalLine = originalLines[startLine + j].trim();
120
- const searchLine = searchLines[j].trim();
121
- const maxLen = Math.max(originalLine.length, searchLine.length);
122
- if (maxLen === 0) {
123
- continue;
124
- }
125
- const distance = levenshtein(originalLine, searchLine);
126
- similarity += (1 - distance / maxLen) / linesToCheck;
127
-
128
- if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
129
- break;
130
- }
131
- }
132
- } else {
133
- similarity = 1.0;
134
- }
135
-
136
- if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
137
- let matchStartIndex = 0;
138
- for (let k = 0; k < startLine; k++) {
139
- matchStartIndex += originalLines[k].length + 1;
140
- }
141
- let matchEndIndex = matchStartIndex;
142
- for (let k = startLine; k <= endLine; k++) {
143
- matchEndIndex += originalLines[k].length;
144
- if (k < endLine) {
145
- matchEndIndex += 1;
146
- }
147
- }
148
- yield content.substring(matchStartIndex, matchEndIndex);
149
- }
150
- return;
151
- }
152
-
153
- let bestMatch: { startLine: number; endLine: number } | null = null;
154
- let maxSimilarity = -1;
155
-
156
- for (const candidate of candidates) {
157
- const { startLine, endLine } = candidate;
158
- const actualBlockSize = endLine - startLine + 1;
159
- const searchBlockSize = searchLines.length;
160
-
161
- let similarity = 0;
162
- const linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2);
163
-
164
- if (linesToCheck > 0) {
165
- for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
166
- const originalLine = originalLines[startLine + j].trim();
167
- const searchLine = searchLines[j].trim();
168
- const maxLen = Math.max(originalLine.length, searchLine.length);
169
- if (maxLen === 0) {
170
- continue;
171
- }
172
- const distance = levenshtein(originalLine, searchLine);
173
- similarity += 1 - distance / maxLen;
174
- }
175
- similarity /= linesToCheck;
176
- } else {
177
- similarity = 1.0;
178
- }
179
-
180
- if (similarity > maxSimilarity) {
181
- maxSimilarity = similarity;
182
- bestMatch = candidate;
183
- }
184
- }
185
-
186
- if (maxSimilarity >= MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD && bestMatch) {
187
- const { startLine, endLine } = bestMatch;
188
- let matchStartIndex = 0;
189
- for (let k = 0; k < startLine; k++) {
190
- matchStartIndex += originalLines[k].length + 1;
191
- }
192
- let matchEndIndex = matchStartIndex;
193
- for (let k = startLine; k <= endLine; k++) {
194
- matchEndIndex += originalLines[k].length;
195
- if (k < endLine) {
196
- matchEndIndex += 1;
197
- }
198
- }
199
- yield content.substring(matchStartIndex, matchEndIndex);
200
- }
201
- };
202
-
203
- export const WhitespaceNormalizedReplacer: Replacer = function* (
204
- content,
205
- find,
206
- ) {
207
- const normalizeWhitespace = (text: string) =>
208
- text.replace(/\s+/g, ' ').trim();
209
- const normalizedFind = normalizeWhitespace(find);
210
-
211
- const lines = content.split('\n');
212
- for (let i = 0; i < lines.length; i++) {
213
- const line = lines[i];
214
- if (normalizeWhitespace(line) === normalizedFind) {
215
- yield line;
216
- } else {
217
- const normalizedLine = normalizeWhitespace(line);
218
- if (normalizedLine.includes(normalizedFind)) {
219
- const words = find.trim().split(/\s+/);
220
- if (words.length > 0) {
221
- const pattern = words
222
- .map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
223
- .join('\\s+');
224
- try {
225
- const regex = new RegExp(pattern);
226
- const match = line.match(regex);
227
- if (match) {
228
- yield match[0];
229
- }
230
- } catch {
231
- // skip invalid regex
232
- }
233
- }
234
- }
235
- }
236
- }
237
-
238
- const findLines = find.split('\n');
239
- if (findLines.length > 1) {
240
- for (let i = 0; i <= lines.length - findLines.length; i++) {
241
- const block = lines.slice(i, i + findLines.length);
242
- if (normalizeWhitespace(block.join('\n')) === normalizedFind) {
243
- yield block.join('\n');
244
- }
245
- }
246
- }
247
- };
248
-
249
- export const IndentationFlexibleReplacer: Replacer = function* (content, find) {
250
- const removeIndentation = (text: string) => {
251
- const lines = text.split('\n');
252
- const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
253
- if (nonEmptyLines.length === 0) return text;
254
-
255
- const minIndent = Math.min(
256
- ...nonEmptyLines.map((line) => {
257
- const match = line.match(/^(\s*)/);
258
- return match ? match[1].length : 0;
259
- }),
260
- );
261
-
262
- return lines
263
- .map((line) => (line.trim().length === 0 ? line : line.slice(minIndent)))
264
- .join('\n');
265
- };
266
-
267
- const normalizedFind = removeIndentation(find);
268
- const contentLines = content.split('\n');
269
- const findLines = find.split('\n');
270
-
271
- for (let i = 0; i <= contentLines.length - findLines.length; i++) {
272
- const block = contentLines.slice(i, i + findLines.length).join('\n');
273
- if (removeIndentation(block) === normalizedFind) {
274
- yield block;
275
- }
276
- }
277
- };
278
-
279
- export const EscapeNormalizedReplacer: Replacer = function* (content, find) {
280
- const unescapeString = (str: string): string => {
281
- return str.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (match, capturedChar) => {
282
- switch (capturedChar) {
283
- case 'n':
284
- return '\n';
285
- case 't':
286
- return '\t';
287
- case 'r':
288
- return '\r';
289
- case "'":
290
- return "'";
291
- case '"':
292
- return '"';
293
- case '`':
294
- return '`';
295
- case '\\':
296
- return '\\';
297
- case '\n':
298
- return '\n';
299
- case '$':
300
- return '$';
301
- default:
302
- return match;
303
- }
304
- });
305
- };
306
-
307
- const unescapedFind = unescapeString(find);
308
-
309
- if (content.includes(unescapedFind)) {
310
- yield unescapedFind;
311
- }
312
-
313
- const lines = content.split('\n');
314
- const findLines = unescapedFind.split('\n');
315
-
316
- for (let i = 0; i <= lines.length - findLines.length; i++) {
317
- const block = lines.slice(i, i + findLines.length).join('\n');
318
- const unescapedBlock = unescapeString(block);
319
-
320
- if (unescapedBlock === unescapedFind) {
321
- yield block;
322
- }
323
- }
324
- };
325
-
326
- export const TrimmedBoundaryReplacer: Replacer = function* (content, find) {
327
- const trimmedFind = find.trim();
328
-
329
- if (trimmedFind === find) {
330
- return;
331
- }
332
-
333
- if (content.includes(trimmedFind)) {
334
- yield trimmedFind;
335
- }
336
-
337
- const lines = content.split('\n');
338
- const findLines = find.split('\n');
339
-
340
- for (let i = 0; i <= lines.length - findLines.length; i++) {
341
- const block = lines.slice(i, i + findLines.length).join('\n');
342
-
343
- if (block.trim() === trimmedFind) {
344
- yield block;
345
- }
346
- }
347
- };
348
-
349
- export const ContextAwareReplacer: Replacer = function* (content, find) {
350
- const findLines = find.split('\n');
351
- if (findLines.length < 3) {
352
- return;
353
- }
354
-
355
- if (findLines[findLines.length - 1] === '') {
356
- findLines.pop();
357
- }
358
-
359
- const contentLines = content.split('\n');
360
-
361
- const firstLine = findLines[0].trim();
362
- const lastLine = findLines[findLines.length - 1].trim();
363
-
364
- for (let i = 0; i < contentLines.length; i++) {
365
- if (contentLines[i].trim() !== firstLine) continue;
366
-
367
- for (let j = i + 2; j < contentLines.length; j++) {
368
- if (contentLines[j].trim() === lastLine) {
369
- const blockLines = contentLines.slice(i, j + 1);
370
-
371
- if (blockLines.length === findLines.length) {
372
- let matchingLines = 0;
373
- let totalNonEmptyLines = 0;
374
-
375
- for (let k = 1; k < blockLines.length - 1; k++) {
376
- const blockLine = blockLines[k].trim();
377
- const findLine = findLines[k].trim();
378
-
379
- if (blockLine.length > 0 || findLine.length > 0) {
380
- totalNonEmptyLines++;
381
- if (blockLine === findLine) {
382
- matchingLines++;
383
- }
384
- }
385
- }
386
-
387
- if (
388
- totalNonEmptyLines === 0 ||
389
- matchingLines / totalNonEmptyLines >= 0.5
390
- ) {
391
- yield blockLines.join('\n');
392
- break;
393
- }
394
- }
395
- break;
396
- }
397
- }
398
- }
399
- };
400
-
401
- export const MultiOccurrenceReplacer: Replacer = function* (content, find) {
402
- let startIndex = 0;
403
-
404
- while (true) {
405
- const index = content.indexOf(find, startIndex);
406
- if (index === -1) break;
407
-
408
- yield find;
409
- startIndex = index + find.length;
410
- }
411
- };
412
-
413
- export const ALL_REPLACERS: Replacer[] = [
414
- SimpleReplacer,
415
- LineTrimmedReplacer,
416
- BlockAnchorReplacer,
417
- WhitespaceNormalizedReplacer,
418
- IndentationFlexibleReplacer,
419
- EscapeNormalizedReplacer,
420
- TrimmedBoundaryReplacer,
421
- ContextAwareReplacer,
422
- MultiOccurrenceReplacer,
423
- ];
424
-
425
- export function replace(
426
- content: string,
427
- oldString: string,
428
- newString: string,
429
- replaceAll = false,
430
- ): string {
431
- if (oldString === newString) {
432
- throw new Error('oldString and newString must be different');
433
- }
434
-
435
- let notFound = true;
436
-
437
- for (const replacer of ALL_REPLACERS) {
438
- for (const search of replacer(content, oldString)) {
439
- const index = content.indexOf(search);
440
- if (index === -1) continue;
441
- notFound = false;
442
- if (replaceAll) {
443
- return content.replaceAll(search, newString);
444
- }
445
- const lastIndex = content.lastIndexOf(search);
446
- if (index !== lastIndex) continue;
447
- return (
448
- content.substring(0, index) +
449
- newString +
450
- content.substring(index + search.length)
451
- );
452
- }
453
- }
454
-
455
- if (notFound) {
456
- throw new Error('oldString not found in content');
457
- }
458
- throw new Error(
459
- 'Found multiple matches for oldString. Provide more surrounding lines in oldString to identify the correct match.',
460
- );
461
- }
@@ -1,166 +0,0 @@
1
- import { tool, type Tool } from 'ai';
2
- import { z } from 'zod/v3';
3
- import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
4
- import { dirname, isAbsolute, join, relative } from 'node:path';
5
- import DESCRIPTION from './edit.txt' with { type: 'text' };
6
- import { createToolError, type ToolResponse } from '../error.ts';
7
- import { replace } from './edit/replacers.ts';
8
- import { buildWriteArtifact } from './fs/util.ts';
9
-
10
- export function buildEditTool(projectRoot: string): {
11
- name: string;
12
- tool: Tool;
13
- } {
14
- const editTool = tool({
15
- description: DESCRIPTION,
16
- inputSchema: z.object({
17
- filePath: z
18
- .string()
19
- .describe(
20
- 'The path to the file to modify (relative to project root or absolute)',
21
- ),
22
- oldString: z.string().describe('The text to replace'),
23
- newString: z
24
- .string()
25
- .describe(
26
- 'The text to replace it with (must be different from oldString)',
27
- ),
28
- replaceAll: z
29
- .boolean()
30
- .optional()
31
- .default(false)
32
- .describe('Replace all occurrences of oldString (default false)'),
33
- }),
34
- async execute({
35
- filePath,
36
- oldString,
37
- newString,
38
- replaceAll: replaceAllFlag = false,
39
- }: {
40
- filePath: string;
41
- oldString: string;
42
- newString: string;
43
- replaceAll?: boolean;
44
- }): Promise<
45
- ToolResponse<{
46
- output: string;
47
- filePath: string;
48
- artifact: unknown;
49
- }>
50
- > {
51
- if (!filePath || filePath.trim().length === 0) {
52
- return createToolError(
53
- 'Missing required parameter: filePath',
54
- 'validation',
55
- {
56
- parameter: 'filePath',
57
- value: filePath,
58
- suggestion: 'Provide a file path to edit',
59
- },
60
- );
61
- }
62
-
63
- if (oldString === newString) {
64
- return createToolError(
65
- 'oldString and newString must be different',
66
- 'validation',
67
- {
68
- parameter: 'oldString',
69
- suggestion: 'Provide different values for oldString and newString',
70
- },
71
- );
72
- }
73
-
74
- const absPath = isAbsolute(filePath)
75
- ? filePath
76
- : join(projectRoot, filePath);
77
- const relPath = relative(projectRoot, absPath);
78
-
79
- try {
80
- if (oldString === '') {
81
- await mkdir(dirname(absPath), { recursive: true });
82
- await writeFile(absPath, newString, 'utf-8');
83
- const artifact = await buildWriteArtifact(
84
- relPath,
85
- false,
86
- '',
87
- newString,
88
- );
89
- return {
90
- ok: true,
91
- output: 'File created successfully.',
92
- filePath: relPath,
93
- artifact,
94
- };
95
- }
96
-
97
- const fileStat = await stat(absPath).catch(() => null);
98
- if (!fileStat) {
99
- return createToolError(`File ${relPath} not found`, 'not_found', {
100
- parameter: 'filePath',
101
- value: relPath,
102
- suggestion: 'Check the file path exists',
103
- });
104
- }
105
- if (fileStat.isDirectory()) {
106
- return createToolError(
107
- `Path is a directory, not a file: ${relPath}`,
108
- 'validation',
109
- {
110
- parameter: 'filePath',
111
- value: relPath,
112
- suggestion: 'Provide a path to a file, not a directory',
113
- },
114
- );
115
- }
116
-
117
- const contentOld = await readFile(absPath, 'utf-8');
118
- let contentNew: string;
119
-
120
- try {
121
- contentNew = replace(
122
- contentOld,
123
- oldString,
124
- newString,
125
- replaceAllFlag,
126
- );
127
- } catch (error) {
128
- const message =
129
- error instanceof Error ? error.message : String(error);
130
- return createToolError(message, 'execution', {
131
- suggestion: message.includes('multiple matches')
132
- ? 'Provide more surrounding context in oldString to uniquely identify the match, or use replaceAll: true'
133
- : 'Verify the oldString matches the file content exactly',
134
- });
135
- }
136
-
137
- await writeFile(absPath, contentNew, 'utf-8');
138
-
139
- const artifact = await buildWriteArtifact(
140
- relPath,
141
- true,
142
- contentOld,
143
- contentNew,
144
- );
145
-
146
- return {
147
- ok: true,
148
- output: 'Edit applied successfully.',
149
- filePath: relPath,
150
- artifact,
151
- };
152
- } catch (error: unknown) {
153
- return createToolError(
154
- `Failed to edit file: ${error instanceof Error ? error.message : String(error)}`,
155
- 'execution',
156
- {
157
- parameter: 'filePath',
158
- value: relPath,
159
- },
160
- );
161
- }
162
- },
163
- });
164
-
165
- return { name: 'edit', tool: editTool };
166
- }
@@ -1,10 +0,0 @@
1
- Performs exact string replacements in files.
2
-
3
- Usage:
4
- - You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
5
- - When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears in the file.
6
- - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
7
- - The edit will FAIL if `oldString` is not found in the file with an error "oldString not found in content".
8
- - The edit will FAIL if `oldString` is found multiple times in the file with an error about multiple matches. Either provide a larger string with more surrounding context to make it unique or use `replaceAll` to change every instance.
9
- - Use `replaceAll` for replacing and renaming strings across the file. This parameter is useful for renaming a variable.
10
- - To create a new file, use an empty `oldString` and set `newString` to the file contents.
@@ -1,190 +0,0 @@
1
- import { tool, type Tool } from 'ai';
2
- import { z } from 'zod/v3';
3
- import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
4
- import { dirname, isAbsolute, join, relative } from 'node:path';
5
- import DESCRIPTION from './multiedit.txt' with { type: 'text' };
6
- import { createToolError, type ToolResponse } from '../error.ts';
7
- import { replace } from './edit/replacers.ts';
8
- import { buildWriteArtifact } from './fs/util.ts';
9
-
10
- export function buildMultiEditTool(projectRoot: string): {
11
- name: string;
12
- tool: Tool;
13
- } {
14
- const multiEditTool = tool({
15
- description: DESCRIPTION,
16
- inputSchema: z.object({
17
- filePath: z
18
- .string()
19
- .describe(
20
- 'The path to the file to modify (relative to project root or absolute)',
21
- ),
22
- edits: z
23
- .array(
24
- z.object({
25
- oldString: z.string().describe('The text to replace'),
26
- newString: z.string().describe('The text to replace it with'),
27
- replaceAll: z
28
- .boolean()
29
- .optional()
30
- .default(false)
31
- .describe('Replace all occurrences of oldString (default false)'),
32
- }),
33
- )
34
- .describe(
35
- 'Array of edit operations to perform sequentially on the file',
36
- ),
37
- }),
38
- async execute({
39
- filePath,
40
- edits,
41
- }: {
42
- filePath: string;
43
- edits: Array<{
44
- oldString: string;
45
- newString: string;
46
- replaceAll?: boolean;
47
- }>;
48
- }): Promise<
49
- ToolResponse<{
50
- output: string;
51
- filePath: string;
52
- editsApplied: number;
53
- artifact: unknown;
54
- }>
55
- > {
56
- if (!filePath || filePath.trim().length === 0) {
57
- return createToolError(
58
- 'Missing required parameter: filePath',
59
- 'validation',
60
- {
61
- parameter: 'filePath',
62
- value: filePath,
63
- suggestion: 'Provide a file path to edit',
64
- },
65
- );
66
- }
67
-
68
- if (!edits || edits.length === 0) {
69
- return createToolError(
70
- 'Missing required parameter: edits',
71
- 'validation',
72
- {
73
- parameter: 'edits',
74
- suggestion: 'Provide at least one edit operation',
75
- },
76
- );
77
- }
78
-
79
- const absPath = isAbsolute(filePath)
80
- ? filePath
81
- : join(projectRoot, filePath);
82
- const relPath = relative(projectRoot, absPath);
83
-
84
- try {
85
- let contentOld: string;
86
- let isNew = false;
87
-
88
- if (edits[0].oldString === '') {
89
- await mkdir(dirname(absPath), { recursive: true });
90
- contentOld = '';
91
- isNew = true;
92
- } else {
93
- const fileStat = await stat(absPath).catch(() => null);
94
- if (!fileStat) {
95
- return createToolError(`File ${relPath} not found`, 'not_found', {
96
- parameter: 'filePath',
97
- value: relPath,
98
- suggestion: 'Check the file path exists',
99
- });
100
- }
101
- if (fileStat.isDirectory()) {
102
- return createToolError(
103
- `Path is a directory, not a file: ${relPath}`,
104
- 'validation',
105
- {
106
- parameter: 'filePath',
107
- value: relPath,
108
- suggestion: 'Provide a path to a file, not a directory',
109
- },
110
- );
111
- }
112
- contentOld = await readFile(absPath, 'utf-8');
113
- }
114
-
115
- const originalContent = contentOld;
116
- let current = contentOld;
117
-
118
- for (let i = 0; i < edits.length; i++) {
119
- const edit = edits[i];
120
-
121
- if (edit.oldString === '' && i === 0 && isNew) {
122
- current = edit.newString;
123
- continue;
124
- }
125
-
126
- if (edit.oldString === edit.newString) {
127
- return createToolError(
128
- `Edit ${i + 1}: oldString and newString must be different`,
129
- 'validation',
130
- {
131
- parameter: `edits[${i}]`,
132
- suggestion:
133
- 'Provide different values for oldString and newString',
134
- },
135
- );
136
- }
137
-
138
- try {
139
- current = replace(
140
- current,
141
- edit.oldString,
142
- edit.newString,
143
- edit.replaceAll ?? false,
144
- );
145
- } catch (error) {
146
- const message =
147
- error instanceof Error ? error.message : String(error);
148
- return createToolError(
149
- `Edit ${i + 1} failed: ${message}`,
150
- 'execution',
151
- {
152
- parameter: `edits[${i}]`,
153
- suggestion:
154
- 'Check that earlier edits did not change the text this edit targets',
155
- },
156
- );
157
- }
158
- }
159
-
160
- await writeFile(absPath, current, 'utf-8');
161
-
162
- const artifact = await buildWriteArtifact(
163
- relPath,
164
- !isNew,
165
- originalContent,
166
- current,
167
- );
168
-
169
- return {
170
- ok: true,
171
- output: `Applied ${edits.length} edit${edits.length === 1 ? '' : 's'} successfully.`,
172
- filePath: relPath,
173
- editsApplied: edits.length,
174
- artifact,
175
- };
176
- } catch (error: unknown) {
177
- return createToolError(
178
- `Failed to edit file: ${error instanceof Error ? error.message : String(error)}`,
179
- 'execution',
180
- {
181
- parameter: 'filePath',
182
- value: relPath,
183
- },
184
- );
185
- }
186
- },
187
- });
188
-
189
- return { name: 'multiedit', tool: multiEditTool };
190
- }
@@ -1,15 +0,0 @@
1
- Makes multiple edits to a single file in one operation. Built on top of the Edit tool.
2
-
3
- Prefer this tool over Edit when you need to make multiple changes to the same file.
4
-
5
- Parameters:
6
- 1. filePath: The path to the file to modify (relative or absolute)
7
- 2. edits: An array of edit operations, each with:
8
- - oldString: The text to replace (must match file contents)
9
- - newString: The replacement text
10
- - replaceAll: Optional, replace all occurrences (default false)
11
-
12
- IMPORTANT:
13
- - Edits are applied in sequence, each operating on the result of the previous
14
- - All edits must succeed or none are applied
15
- - Plan edits carefully to avoid conflicts between sequential operations