bingocode 1.1.176 → 1.1.178

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": "bingocode",
3
- "version": "1.1.176",
3
+ "version": "1.1.178",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude": "bin/claude-win.cjs",
@@ -73,6 +73,7 @@ import {
73
73
  areFileEditsInputsEquivalent,
74
74
  findActualString,
75
75
  findClosestLines,
76
+ findFuzzySuggestion,
76
77
  getPatchForEdit,
77
78
  preserveQuoteStyle,
78
79
  visibleWhitespace,
@@ -317,11 +318,15 @@ export const FileEditTool = buildTool({
317
318
  // Use findActualString to handle quote normalization
318
319
  const actualOldString = findActualString(file, old_string)
319
320
  if (!actualOldString) {
320
- const BASE = 'String to replace not found.'
321
+ const BASE = 'String to replace not found.'
321
322
  const matches = findClosestLines(file, old_string)
323
+ const fuzzy = findFuzzySuggestion(file, old_string)
324
+ const fuzzyBlock = fuzzy
325
+ ? `\n\nDid you mean:\n line ${fuzzy.lineNumber}: ${visibleWhitespace(fuzzy.suggestion)}\n (edit distance ${fuzzy.distance})`
326
+ : ''
322
327
  const msg = matches.length
323
- ? `${BASE}\n→ = tab · = space\nProvided:\n${visibleWhitespace(old_string)}\nClosest matches:\n${matches.map(m => ` line ${m.lineNumber} (${m.diffType})\n ${visibleWhitespace(m.snippet)}`).join('\n')}\n↑ check visible whitespace markers above.`
324
- : `${BASE}.\n→ = tab · = space\nProvided:\n${visibleWhitespace(old_string)}\n↑ check visible whitespace markers above.`
328
+ ? `${BASE}\n→ = tab · = space\nProvided:\n${visibleWhitespace(old_string)}\nClosest matches:\n${matches.map(m => ` line ${m.lineNumber} (${m.diffType})\n ${visibleWhitespace(m.snippet)}`).join('\n')}${fuzzyBlock}\n↑ check visible whitespace markers above.`
329
+ : `${BASE}.\n→ = tab · = space\nProvided:\n${visibleWhitespace(old_string)}${fuzzyBlock}\n↑ check visible whitespace markers above.`
325
330
  return {
326
331
  result: false,
327
332
  behavior: 'ask',
@@ -15,6 +15,108 @@ import {
15
15
  } from '../../utils/file.js'
16
16
  import type { EditInput, FileEdit } from './types.js'
17
17
 
18
+ /**
19
+ * Computes Levenshtein distance between two strings using Wagner-Fischer DP.
20
+ * O(n*m) time, O(min(n,m)) space (two-row optimization).
21
+ */
22
+ export function levenshteinDistance(a: string, b: string): number {
23
+ // Ensure a is the shorter string for memory efficiency
24
+ if (a.length > b.length) {
25
+ ;[a, b] = [b, a]
26
+ }
27
+ const m = a.length
28
+ const n = b.length
29
+
30
+ let prevRow = new Uint16Array(m + 1)
31
+ let currRow = new Uint16Array(m + 1)
32
+
33
+ for (let i = 0; i <= m; i++) prevRow[i] = i
34
+ for (let j = 1; j <= n; j++) {
35
+ currRow[0] = j
36
+ for (let i = 1; i <= m; i++) {
37
+ const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1
38
+ // d[i][j] = min(d[i-1][j]+1, d[i][j-1]+1, d[i-1][j-1]+cost)
39
+ // deletion: remove char from s → d[i-1][j] + 1 → currRow[i - 1] + 1
40
+ // insertion: add char from t → d[i][j-1] + 1 → prevRow[i] + 1
41
+ // substitution: swap or match → d[i-1][j-1] + cost → prevRow[i - 1] + cost
42
+ currRow[i] = Math.min(
43
+ currRow[i - 1]! + 1,
44
+ prevRow[i]! + 1,
45
+ prevRow[i - 1]! + cost,
46
+ }
47
+ ;[prevRow, currRow] = [currRow, prevRow]
48
+ }
49
+ return prevRow[m]!
50
+ }
51
+
52
+ /**
53
+ * Fuzzy matching — finds the best match in fileContent for searchString
54
+ * when the edit distance is within threshold. Used as a secondary pass
55
+ * after exact/normalized matching fails, to suggest "Did you mean: X"
56
+ * when the model's old_string is only a few characters off.
57
+ *
58
+ * For multi-line search strings: compares each contiguous block of lines
59
+ * whose total length is within tolerance. Returns null when no match is
60
+ * within threshold.
61
+ */
62
+ export function findFuzzySuggestion(
63
+ fileContent: string,
64
+ searchString: string,
65
+ threshold: number = 2,
66
+ ): { suggestion: string; lineNumber: number; distance: number } | null {
67
+ const searchLines = searchString.split('\n')
68
+ const fileLines = fileContent.split('\n')
69
+ const searchLen = searchString.length
70
+
71
+ // For single-line searches, scan all lines with similar length
72
+ if (searchLines.length === 1) {
73
+ const lenTolerance = searchLen + threshold * 2
74
+ let bestLine: string | null = null
75
+ let bestDist = Infinity
76
+ let bestIdx = -1
77
+
78
+ for (let i = 0; i < fileLines.length; i++) {
79
+ const line = fileLines[i]!
80
+ if (Math.abs(line.length - searchLen) > lenTolerance) continue
81
+ const dist = levenshteinDistance(line, searchString)
82
+ if (dist < bestDist) {
83
+ bestDist = dist
84
+ bestLine = line
85
+ bestIdx = i
86
+ }
87
+ }
88
+
89
+ if (bestLine && bestDist > 0 && bestDist <= threshold) {
90
+ return {
91
+ suggestion: bestLine.replace(/\s+$/, ''),
92
+ lineNumber: bestIdx + 1,
93
+ distance: bestDist,
94
+ }
95
+ }
96
+ // No match within threshold (or distance 0 = exact match already present)
97
+ return null
98
+ }
99
+
100
+ // Multi-line: compare contiguous blocks of lines
101
+ const maxBlockLen = searchLen + threshold * searchLines.length
102
+ let best: { suggestion: string; lineNumber: number; distance: number } | null = null
103
+
104
+ for (let i = 0; i <= fileLines.length - searchLines.length; i++) {
105
+ const block = fileLines.slice(i, i + searchLines.length).join('\n')
106
+ if (Math.abs(block.length - searchLen) > maxBlockLen) continue
107
+ const dist = levenshteinDistance(block.replace(/\s+$/, ''), searchString.replace(/\s+$/, ''))
108
+ if (dist > 0 && dist <= threshold && dist < (best?.distance ?? Infinity)) {
109
+ best = {
110
+ suggestion: block.replace(/\s+$/, ''),
111
+ lineNumber: i + 1,
112
+ distance: dist,
113
+ }
114
+ }
115
+ }
116
+
117
+ return best
118
+ }
119
+
18
120
  // Claude can't output curly quotes, so we define them as constants here for Claude to use
19
121
  // in the code. We do this because we normalize curly quotes to straight quotes
20
122
  // when applying edits.
@@ -266,7 +368,11 @@ export class EditNotFoundError extends Error {
266
368
  * tab → '→', space → '·'
267
369
  */
268
370
  export function visibleWhitespace(str: string): string {
269
- return str.replace(/\t/g, '→').replace(/ /g, '·')
371
+ return str
372
+ .replace(/\t/g, '→')
373
+ .replace(/ /g, '·')
374
+ .replace(/\n/g, '↵')
375
+ .replace(/\r/g, '␍')
270
376
  }
271
377
 
272
378
  /**
@@ -432,11 +538,15 @@ export function getPatchForEdits({
432
538
  // If this edit didn't change anything, throw an error
433
539
  if (updatedFile === previousContent) {
434
540
  const closest = findClosestLines(fileContents, edit.old_string)
541
+ const fuzzy = findFuzzySuggestion(fileContents, edit.old_string)
542
+ const fuzzyBlock = fuzzy
543
+ ? `\n\nDid you mean:\n line ${fuzzy.lineNumber}: ${visibleWhitespace(fuzzy.suggestion)}\n (edit distance ${fuzzy.distance})`
544
+ : ''
435
545
  throw new EditNotFoundError(
436
546
  closest.length
437
547
  ? `Edit failed — closest match:
438
- ${closest.map(m => ` line ${m.lineNumber}: ${visibleWhitespace(m.snippet)} (${m.diffType})`).join('\n')}`
439
- : 'Edit failed — string not found in file.',
548
+ ${closest.map(m => ` line ${m.lineNumber}: ${visibleWhitespace(m.snippet)} (${m.diffType})`).join('\n')}${fuzzyBlock}`
549
+ : `Edit failed — string not found in file.${fuzzyBlock}`,
440
550
  {
441
551
  searchString: edit.old_string,
442
552
  visibleSearch: visibleWhitespace(edit.old_string),