@oh-my-pi/pi-coding-agent 6.2.0 → 6.7.67

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.
Files changed (93) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/docs/sdk.md +1 -1
  3. package/package.json +5 -5
  4. package/scripts/generate-template.ts +6 -6
  5. package/src/cli/args.ts +3 -0
  6. package/src/core/agent-session.ts +39 -0
  7. package/src/core/bash-executor.ts +3 -3
  8. package/src/core/cursor/exec-bridge.ts +95 -88
  9. package/src/core/custom-commands/bundled/review/index.ts +142 -145
  10. package/src/core/custom-commands/bundled/wt/index.ts +68 -66
  11. package/src/core/custom-commands/loader.ts +4 -6
  12. package/src/core/custom-tools/index.ts +2 -2
  13. package/src/core/custom-tools/loader.ts +66 -61
  14. package/src/core/custom-tools/types.ts +4 -4
  15. package/src/core/custom-tools/wrapper.ts +61 -25
  16. package/src/core/event-bus.ts +19 -47
  17. package/src/core/extensions/index.ts +8 -4
  18. package/src/core/extensions/loader.ts +160 -120
  19. package/src/core/extensions/types.ts +4 -4
  20. package/src/core/extensions/wrapper.ts +149 -100
  21. package/src/core/hooks/index.ts +1 -1
  22. package/src/core/hooks/tool-wrapper.ts +96 -70
  23. package/src/core/hooks/types.ts +1 -2
  24. package/src/core/index.ts +1 -0
  25. package/src/core/mcp/index.ts +6 -2
  26. package/src/core/mcp/json-rpc.ts +88 -0
  27. package/src/core/mcp/loader.ts +22 -4
  28. package/src/core/mcp/manager.ts +202 -48
  29. package/src/core/mcp/tool-bridge.ts +143 -55
  30. package/src/core/mcp/tool-cache.ts +122 -0
  31. package/src/core/python-executor.ts +3 -9
  32. package/src/core/sdk.ts +33 -32
  33. package/src/core/session-manager.ts +30 -0
  34. package/src/core/settings-manager.ts +54 -1
  35. package/src/core/ssh/ssh-executor.ts +6 -84
  36. package/src/core/streaming-output.ts +107 -53
  37. package/src/core/tools/ask.ts +92 -93
  38. package/src/core/tools/bash.ts +103 -94
  39. package/src/core/tools/calculator.ts +41 -26
  40. package/src/core/tools/complete.ts +76 -66
  41. package/src/core/tools/context.ts +22 -24
  42. package/src/core/tools/exa/index.ts +1 -1
  43. package/src/core/tools/exa/mcp-client.ts +56 -101
  44. package/src/core/tools/find.ts +250 -253
  45. package/src/core/tools/git.ts +39 -33
  46. package/src/core/tools/grep.ts +440 -427
  47. package/src/core/tools/index.ts +63 -61
  48. package/src/core/tools/ls.ts +119 -114
  49. package/src/core/tools/lsp/clients/biome-client.ts +5 -7
  50. package/src/core/tools/lsp/clients/index.ts +4 -4
  51. package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
  52. package/src/core/tools/lsp/config.ts +2 -2
  53. package/src/core/tools/lsp/index.ts +604 -578
  54. package/src/core/tools/notebook.ts +121 -119
  55. package/src/core/tools/output.ts +163 -147
  56. package/src/core/tools/patch/applicator.ts +1100 -0
  57. package/src/core/tools/patch/diff.ts +362 -0
  58. package/src/core/tools/patch/fuzzy.ts +647 -0
  59. package/src/core/tools/patch/index.ts +430 -0
  60. package/src/core/tools/patch/normalize.ts +220 -0
  61. package/src/core/tools/patch/normative.ts +73 -0
  62. package/src/core/tools/patch/parser.ts +528 -0
  63. package/src/core/tools/patch/shared.ts +257 -0
  64. package/src/core/tools/patch/types.ts +244 -0
  65. package/src/core/tools/python.ts +139 -136
  66. package/src/core/tools/read.ts +239 -216
  67. package/src/core/tools/render-utils.ts +196 -77
  68. package/src/core/tools/renderers.ts +6 -2
  69. package/src/core/tools/ssh.ts +99 -80
  70. package/src/core/tools/task/executor.ts +11 -7
  71. package/src/core/tools/task/index.ts +352 -343
  72. package/src/core/tools/task/worker.ts +13 -23
  73. package/src/core/tools/todo-write.ts +74 -59
  74. package/src/core/tools/web-fetch.ts +54 -47
  75. package/src/core/tools/web-search/index.ts +27 -16
  76. package/src/core/tools/write.ts +108 -47
  77. package/src/core/ttsr.ts +106 -152
  78. package/src/core/voice.ts +49 -39
  79. package/src/index.ts +16 -12
  80. package/src/lib/worktree/index.ts +1 -9
  81. package/src/modes/interactive/components/diff.ts +15 -8
  82. package/src/modes/interactive/components/settings-defs.ts +42 -0
  83. package/src/modes/interactive/components/tool-execution.ts +46 -8
  84. package/src/modes/interactive/controllers/event-controller.ts +6 -19
  85. package/src/modes/interactive/controllers/input-controller.ts +1 -1
  86. package/src/modes/interactive/utils/ui-helpers.ts +5 -1
  87. package/src/modes/rpc/rpc-mode.ts +99 -81
  88. package/src/prompts/tools/patch.md +76 -0
  89. package/src/prompts/tools/read.md +1 -1
  90. package/src/prompts/tools/{edit.md → replace.md} +1 -0
  91. package/src/utils/shell.ts +0 -40
  92. package/src/core/tools/edit-diff.ts +0 -574
  93. package/src/core/tools/edit.ts +0 -345
@@ -1,574 +0,0 @@
1
- /**
2
- * Shared diff computation utilities for the edit tool.
3
- * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).
4
- */
5
-
6
- import * as Diff from "diff";
7
- import { resolveToCwd } from "./path-utils";
8
-
9
- export function detectLineEnding(content: string): "\r\n" | "\n" {
10
- const crlfIdx = content.indexOf("\r\n");
11
- const lfIdx = content.indexOf("\n");
12
- if (lfIdx === -1) return "\n";
13
- if (crlfIdx === -1) return "\n";
14
- return crlfIdx < lfIdx ? "\r\n" : "\n";
15
- }
16
-
17
- export function normalizeToLF(text: string): string {
18
- return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
19
- }
20
-
21
- export function restoreLineEndings(text: string, ending: "\r\n" | "\n"): string {
22
- return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
23
- }
24
-
25
- /** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */
26
- export function stripBom(content: string): { bom: string; text: string } {
27
- return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
28
- }
29
-
30
- export const DEFAULT_FUZZY_THRESHOLD = 0.95;
31
-
32
- export interface EditMatch {
33
- actualText: string;
34
- startIndex: number;
35
- startLine: number;
36
- confidence: number;
37
- }
38
-
39
- export interface EditMatchOutcome {
40
- match?: EditMatch;
41
- closest?: EditMatch;
42
- occurrences?: number;
43
- fuzzyMatches?: number;
44
- }
45
-
46
- function countLeadingWhitespace(line: string): number {
47
- let count = 0;
48
- for (let i = 0; i < line.length; i++) {
49
- const char = line[i];
50
- if (char === " " || char === "\t") {
51
- count++;
52
- } else {
53
- break;
54
- }
55
- }
56
- return count;
57
- }
58
-
59
- function computeRelativeIndentDepths(lines: string[]): number[] {
60
- const indents = lines.map(countLeadingWhitespace);
61
- const nonEmptyIndents: number[] = [];
62
- for (let i = 0; i < lines.length; i++) {
63
- if (lines[i].trim().length > 0) {
64
- nonEmptyIndents.push(indents[i]);
65
- }
66
- }
67
- const minIndent = nonEmptyIndents.length > 0 ? Math.min(...nonEmptyIndents) : 0;
68
- const indentSteps = nonEmptyIndents.map((indent) => indent - minIndent).filter((step) => step > 0);
69
- const indentUnit = indentSteps.length > 0 ? Math.min(...indentSteps) : 1;
70
-
71
- return lines.map((line, index) => {
72
- if (line.trim().length === 0) {
73
- return 0;
74
- }
75
- if (indentUnit <= 0) {
76
- return 0;
77
- }
78
- const relativeIndent = indents[index] - minIndent;
79
- return Math.round(relativeIndent / indentUnit);
80
- });
81
- }
82
-
83
- function normalizeFuzzyText(text: string): string {
84
- return text
85
- .replace(/[“”„‟«»]/g, '"')
86
- .replace(/[‘’‚‛`´]/g, "'")
87
- .replace(/[‐‑‒–—−]/g, "-");
88
- }
89
-
90
- function normalizeLinesForMatch(lines: string[], includeDepth = true): string[] {
91
- const indentDepths = includeDepth ? computeRelativeIndentDepths(lines) : null;
92
- return lines.map((line, index) => {
93
- const trimmed = line.trim();
94
- const prefix = indentDepths ? `${indentDepths[index]}|` : "|";
95
- if (trimmed.length === 0) {
96
- return prefix;
97
- }
98
- const normalized = normalizeFuzzyText(trimmed);
99
- const collapsed = normalized.replace(/[ \t]+/g, " ");
100
- return `${prefix}${collapsed}`;
101
- });
102
- }
103
-
104
- function levenshteinDistance(a: string, b: string): number {
105
- if (a === b) return 0;
106
- const aLen = a.length;
107
- const bLen = b.length;
108
- if (aLen === 0) return bLen;
109
- if (bLen === 0) return aLen;
110
-
111
- let prev = new Array<number>(bLen + 1);
112
- let curr = new Array<number>(bLen + 1);
113
- for (let j = 0; j <= bLen; j++) {
114
- prev[j] = j;
115
- }
116
-
117
- for (let i = 1; i <= aLen; i++) {
118
- curr[0] = i;
119
- const aCode = a.charCodeAt(i - 1);
120
- for (let j = 1; j <= bLen; j++) {
121
- const cost = aCode === b.charCodeAt(j - 1) ? 0 : 1;
122
- const deletion = prev[j] + 1;
123
- const insertion = curr[j - 1] + 1;
124
- const substitution = prev[j - 1] + cost;
125
- curr[j] = Math.min(deletion, insertion, substitution);
126
- }
127
- const tmp = prev;
128
- prev = curr;
129
- curr = tmp;
130
- }
131
-
132
- return prev[bLen];
133
- }
134
-
135
- function similarityScore(a: string, b: string): number {
136
- if (a.length === 0 && b.length === 0) {
137
- return 1;
138
- }
139
- const maxLen = Math.max(a.length, b.length);
140
- if (maxLen === 0) {
141
- return 1;
142
- }
143
- const distance = levenshteinDistance(a, b);
144
- return 1 - distance / maxLen;
145
- }
146
-
147
- function computeLineOffsets(lines: string[]): number[] {
148
- const offsets: number[] = [];
149
- let offset = 0;
150
- for (let i = 0; i < lines.length; i++) {
151
- offsets.push(offset);
152
- offset += lines[i].length;
153
- if (i < lines.length - 1) {
154
- offset += 1;
155
- }
156
- }
157
- return offsets;
158
- }
159
-
160
- function findBestFuzzyMatchCore(
161
- contentLines: string[],
162
- targetLines: string[],
163
- offsets: number[],
164
- threshold: number,
165
- includeDepth: boolean,
166
- ): { best?: EditMatch; aboveThresholdCount: number } {
167
- const targetNormalized = normalizeLinesForMatch(targetLines, includeDepth);
168
-
169
- let best: EditMatch | undefined;
170
- let bestScore = -1;
171
- let aboveThresholdCount = 0;
172
-
173
- for (let start = 0; start <= contentLines.length - targetLines.length; start++) {
174
- const windowLines = contentLines.slice(start, start + targetLines.length);
175
- const windowNormalized = normalizeLinesForMatch(windowLines, includeDepth);
176
- let score = 0;
177
- for (let i = 0; i < targetLines.length; i++) {
178
- score += similarityScore(targetNormalized[i], windowNormalized[i]);
179
- }
180
- score = score / targetLines.length;
181
-
182
- if (score >= threshold) {
183
- aboveThresholdCount++;
184
- }
185
-
186
- if (score > bestScore) {
187
- bestScore = score;
188
- best = {
189
- actualText: windowLines.join("\n"),
190
- startIndex: offsets[start],
191
- startLine: start + 1,
192
- confidence: score,
193
- };
194
- }
195
- }
196
-
197
- return { best, aboveThresholdCount };
198
- }
199
-
200
- const FALLBACK_THRESHOLD = 0.8;
201
-
202
- function findBestFuzzyMatch(
203
- content: string,
204
- target: string,
205
- threshold: number,
206
- ): { best?: EditMatch; aboveThresholdCount: number } {
207
- const contentLines = content.split("\n");
208
- const targetLines = target.split("\n");
209
- if (targetLines.length === 0 || target.length === 0) {
210
- return { aboveThresholdCount: 0 };
211
- }
212
- if (targetLines.length > contentLines.length) {
213
- return { aboveThresholdCount: 0 };
214
- }
215
-
216
- const offsets = computeLineOffsets(contentLines);
217
-
218
- let result = findBestFuzzyMatchCore(contentLines, targetLines, offsets, threshold, true);
219
-
220
- if (result.best && result.best.confidence < threshold && result.best.confidence >= FALLBACK_THRESHOLD) {
221
- const noDepthResult = findBestFuzzyMatchCore(contentLines, targetLines, offsets, threshold, false);
222
- if (noDepthResult.best && noDepthResult.best.confidence > result.best.confidence) {
223
- result = noDepthResult;
224
- }
225
- }
226
-
227
- return result;
228
- }
229
-
230
- export function findEditMatch(
231
- content: string,
232
- target: string,
233
- options: { allowFuzzy: boolean; similarityThreshold?: number },
234
- ): EditMatchOutcome {
235
- if (target.length === 0) {
236
- return {};
237
- }
238
-
239
- const exactIndex = content.indexOf(target);
240
- if (exactIndex !== -1) {
241
- const occurrences = content.split(target).length - 1;
242
- if (occurrences > 1) {
243
- return { occurrences };
244
- }
245
- const startLine = content.slice(0, exactIndex).split("\n").length;
246
- return {
247
- match: {
248
- actualText: target,
249
- startIndex: exactIndex,
250
- startLine,
251
- confidence: 1,
252
- },
253
- };
254
- }
255
-
256
- const threshold = options.similarityThreshold ?? DEFAULT_FUZZY_THRESHOLD;
257
- const { best, aboveThresholdCount } = findBestFuzzyMatch(content, target, threshold);
258
- if (!best) {
259
- return {};
260
- }
261
-
262
- if (options.allowFuzzy && best.confidence >= threshold && aboveThresholdCount === 1) {
263
- return { match: best, closest: best };
264
- }
265
-
266
- return { closest: best, fuzzyMatches: aboveThresholdCount };
267
- }
268
-
269
- function findFirstDifferentLine(oldLines: string[], newLines: string[]): { oldLine: string; newLine: string } {
270
- const max = Math.max(oldLines.length, newLines.length);
271
- for (let i = 0; i < max; i++) {
272
- const oldLine = oldLines[i] ?? "";
273
- const newLine = newLines[i] ?? "";
274
- if (oldLine !== newLine) {
275
- return { oldLine, newLine };
276
- }
277
- }
278
- return { oldLine: oldLines[0] ?? "", newLine: newLines[0] ?? "" };
279
- }
280
-
281
- export class EditMatchError extends Error {
282
- constructor(
283
- public readonly path: string,
284
- public readonly normalizedOldText: string,
285
- public readonly closest: EditMatch | undefined,
286
- public readonly options: { allowFuzzy: boolean; similarityThreshold: number; fuzzyMatches?: number },
287
- ) {
288
- super(EditMatchError.formatMessage(path, normalizedOldText, closest, options));
289
- this.name = "EditMatchError";
290
- }
291
-
292
- static formatMessage(
293
- path: string,
294
- normalizedOldText: string,
295
- closest: EditMatch | undefined,
296
- options: { allowFuzzy: boolean; similarityThreshold: number; fuzzyMatches?: number },
297
- ): string {
298
- if (!closest) {
299
- return options.allowFuzzy
300
- ? `Could not find a close enough match in ${path}.`
301
- : `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`;
302
- }
303
-
304
- const similarity = Math.round(closest.confidence * 100);
305
- const oldLines = normalizedOldText.split("\n");
306
- const actualLines = closest.actualText.split("\n");
307
- const { oldLine, newLine } = findFirstDifferentLine(oldLines, actualLines);
308
- const thresholdPercent = Math.round(options.similarityThreshold * 100);
309
-
310
- const hint = options.allowFuzzy
311
- ? options.fuzzyMatches && options.fuzzyMatches > 1
312
- ? `Found ${options.fuzzyMatches} high-confidence matches. Provide more context to make it unique.`
313
- : `Closest match was below the ${thresholdPercent}% similarity threshold.`
314
- : "Fuzzy matching is disabled. Enable 'Edit fuzzy match' in settings to accept high-confidence matches.";
315
-
316
- return [
317
- options.allowFuzzy
318
- ? `Could not find a close enough match in ${path}.`
319
- : `Could not find the exact text in ${path}.`,
320
- ``,
321
- `Closest match (${similarity}% similar) at line ${closest.startLine}:`,
322
- ` - ${oldLine}`,
323
- ` + ${newLine}`,
324
- hint,
325
- ].join("\n");
326
- }
327
- }
328
-
329
- /**
330
- * Generate a unified diff string with line numbers and context.
331
- * Returns both the diff string and the first changed line number (in the new file).
332
- */
333
- export function generateDiffString(
334
- oldContent: string,
335
- newContent: string,
336
- contextLines = 4,
337
- ): { diff: string; firstChangedLine: number | undefined } {
338
- const parts = Diff.diffLines(oldContent, newContent);
339
- const output: string[] = [];
340
-
341
- const oldLines = oldContent.split("\n");
342
- const newLines = newContent.split("\n");
343
- const maxLineNum = Math.max(oldLines.length, newLines.length);
344
- const lineNumWidth = String(maxLineNum).length;
345
-
346
- let oldLineNum = 1;
347
- let newLineNum = 1;
348
- let lastWasChange = false;
349
- let firstChangedLine: number | undefined;
350
-
351
- for (let i = 0; i < parts.length; i++) {
352
- const part = parts[i];
353
- const raw = part.value.split("\n");
354
- if (raw[raw.length - 1] === "") {
355
- raw.pop();
356
- }
357
-
358
- if (part.added || part.removed) {
359
- // Capture the first changed line (in the new file)
360
- if (firstChangedLine === undefined) {
361
- firstChangedLine = newLineNum;
362
- }
363
-
364
- // Show the change
365
- for (const line of raw) {
366
- if (part.added) {
367
- const lineNum = String(newLineNum).padStart(lineNumWidth, " ");
368
- output.push(`+${lineNum} ${line}`);
369
- newLineNum++;
370
- } else {
371
- // removed
372
- const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
373
- output.push(`-${lineNum} ${line}`);
374
- oldLineNum++;
375
- }
376
- }
377
- lastWasChange = true;
378
- } else {
379
- // Context lines - only show a few before/after changes
380
- const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
381
-
382
- if (lastWasChange || nextPartIsChange) {
383
- // Show context
384
- let linesToShow = raw;
385
- let skipStart = 0;
386
- let skipEnd = 0;
387
-
388
- if (!lastWasChange) {
389
- // Show only last N lines as leading context
390
- skipStart = Math.max(0, raw.length - contextLines);
391
- linesToShow = raw.slice(skipStart);
392
- }
393
-
394
- if (!nextPartIsChange && linesToShow.length > contextLines) {
395
- // Show only first N lines as trailing context
396
- skipEnd = linesToShow.length - contextLines;
397
- linesToShow = linesToShow.slice(0, contextLines);
398
- }
399
-
400
- // Add ellipsis if we skipped lines at start
401
- if (skipStart > 0) {
402
- output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
403
- // Update line numbers for the skipped leading context
404
- oldLineNum += skipStart;
405
- newLineNum += skipStart;
406
- }
407
-
408
- for (const line of linesToShow) {
409
- const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
410
- output.push(` ${lineNum} ${line}`);
411
- oldLineNum++;
412
- newLineNum++;
413
- }
414
-
415
- // Add ellipsis if we skipped lines at end
416
- if (skipEnd > 0) {
417
- output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
418
- // Update line numbers for the skipped trailing context
419
- oldLineNum += skipEnd;
420
- newLineNum += skipEnd;
421
- }
422
- } else {
423
- // Skip these context lines entirely
424
- oldLineNum += raw.length;
425
- newLineNum += raw.length;
426
- }
427
-
428
- lastWasChange = false;
429
- }
430
- }
431
-
432
- return { diff: output.join("\n"), firstChangedLine };
433
- }
434
-
435
- export interface EditDiffResult {
436
- diff: string;
437
- firstChangedLine: number | undefined;
438
- }
439
-
440
- export interface EditDiffError {
441
- error: string;
442
- }
443
-
444
- /**
445
- * Compute the diff for an edit operation without applying it.
446
- * Used for preview rendering in the TUI before the tool executes.
447
- */
448
- export async function computeEditDiff(
449
- path: string,
450
- oldText: string,
451
- newText: string,
452
- cwd: string,
453
- fuzzy = true,
454
- all = false,
455
- ): Promise<EditDiffResult | EditDiffError> {
456
- const absolutePath = resolveToCwd(path, cwd);
457
-
458
- try {
459
- // Check if file exists and is readable
460
- const file = Bun.file(absolutePath);
461
- try {
462
- if (!(await file.exists())) {
463
- return { error: `File not found: ${path}` };
464
- }
465
- } catch {
466
- return { error: `File not found: ${path}` };
467
- }
468
-
469
- // Read the file
470
- let rawContent: string;
471
- try {
472
- rawContent = await file.text();
473
- } catch (error) {
474
- const message = error instanceof Error ? error.message : String(error);
475
- return { error: message || `Unable to read ${path}` };
476
- }
477
-
478
- // Strip BOM before matching (LLM won't include invisible BOM in oldText)
479
- const { text: content } = stripBom(rawContent);
480
-
481
- const normalizedContent = normalizeToLF(content);
482
- const normalizedOldText = normalizeToLF(oldText);
483
- const normalizedNewText = normalizeToLF(newText);
484
-
485
- let normalizedNewContent: string;
486
-
487
- if (all) {
488
- // Replace all occurrences mode with fuzzy matching
489
- normalizedNewContent = normalizedContent;
490
- let replacementCount = 0;
491
-
492
- // First check: if exact matches exist, use simple replaceAll
493
- const exactCount = normalizedContent.split(normalizedOldText).length - 1;
494
- if (exactCount > 0) {
495
- normalizedNewContent = normalizedContent.split(normalizedOldText).join(normalizedNewText);
496
- replacementCount = exactCount;
497
- } else {
498
- // No exact matches - try fuzzy matching iteratively
499
- while (true) {
500
- const matchOutcome = findEditMatch(normalizedNewContent, normalizedOldText, {
501
- allowFuzzy: fuzzy,
502
- similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
503
- });
504
-
505
- // In all mode, use closest match if it passes threshold (even with multiple matches)
506
- const match =
507
- matchOutcome.match ||
508
- (fuzzy && matchOutcome.closest && matchOutcome.closest.confidence >= DEFAULT_FUZZY_THRESHOLD
509
- ? matchOutcome.closest
510
- : undefined);
511
-
512
- if (!match) {
513
- if (replacementCount === 0) {
514
- return {
515
- error: EditMatchError.formatMessage(path, normalizedOldText, matchOutcome.closest, {
516
- allowFuzzy: fuzzy,
517
- similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
518
- fuzzyMatches: matchOutcome.fuzzyMatches,
519
- }),
520
- };
521
- }
522
- break;
523
- }
524
-
525
- normalizedNewContent =
526
- normalizedNewContent.substring(0, match.startIndex) +
527
- normalizedNewText +
528
- normalizedNewContent.substring(match.startIndex + match.actualText.length);
529
- replacementCount++;
530
- }
531
- }
532
- } else {
533
- // Single replacement mode with fuzzy matching
534
- const matchOutcome = findEditMatch(normalizedContent, normalizedOldText, {
535
- allowFuzzy: fuzzy,
536
- similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
537
- });
538
-
539
- if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
540
- return {
541
- error: `Found ${matchOutcome.occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique, or use all: true to replace all.`,
542
- };
543
- }
544
-
545
- if (!matchOutcome.match) {
546
- return {
547
- error: EditMatchError.formatMessage(path, normalizedOldText, matchOutcome.closest, {
548
- allowFuzzy: fuzzy,
549
- similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
550
- fuzzyMatches: matchOutcome.fuzzyMatches,
551
- }),
552
- };
553
- }
554
-
555
- const match = matchOutcome.match;
556
- normalizedNewContent =
557
- normalizedContent.substring(0, match.startIndex) +
558
- normalizedNewText +
559
- normalizedContent.substring(match.startIndex + match.actualText.length);
560
- }
561
-
562
- // Check if it would actually change anything
563
- if (normalizedContent === normalizedNewContent) {
564
- return {
565
- error: `No changes would be made to ${path}. The replacement produces identical content.`,
566
- };
567
- }
568
-
569
- // Generate the diff
570
- return generateDiffString(normalizedContent, normalizedNewContent);
571
- } catch (err) {
572
- return { error: err instanceof Error ? err.message : String(err) };
573
- }
574
- }