@ottocode/sdk 0.1.231 → 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 +1 -5
- package/src/core/src/index.ts +0 -2
- package/src/core/src/tools/builtin/fs/write.txt +1 -1
- package/src/core/src/tools/loader.ts +0 -7
- package/src/index.ts +0 -2
- package/src/prompts/src/agents/build.txt +1 -9
- package/src/prompts/src/base.txt +1 -1
- package/src/prompts/src/providers/anthropic.txt +1 -3
- package/src/prompts/src/providers/default.txt +1 -11
- package/src/prompts/src/providers/glm.txt +4 -13
- package/src/prompts/src/providers/google.txt +1 -3
- package/src/prompts/src/providers/moonshot.txt +3 -13
- package/src/prompts/src/providers/openai.txt +1 -3
- package/src/core/src/tools/builtin/edit/replacers.ts +0 -461
- package/src/core/src/tools/builtin/edit.ts +0 -166
- package/src/core/src/tools/builtin/edit.txt +0 -10
- package/src/core/src/tools/builtin/multiedit.ts +0 -190
- package/src/core/src/tools/builtin/multiedit.txt +0 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/sdk",
|
|
3
|
-
"version": "0.1.
|
|
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"
|
package/src/core/src/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
package/src/prompts/src/base.txt
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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,
|
|
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**:
|
|
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.)**:
|
|
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
|
|
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
|
|
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'
|
|
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
|
|
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
|
|
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
|