bingocode 1.1.177 → 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.177",
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.
@@ -436,11 +538,15 @@ export function getPatchForEdits({
436
538
  // If this edit didn't change anything, throw an error
437
539
  if (updatedFile === previousContent) {
438
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
+ : ''
439
545
  throw new EditNotFoundError(
440
546
  closest.length
441
547
  ? `Edit failed — closest match:
442
- ${closest.map(m => ` line ${m.lineNumber}: ${visibleWhitespace(m.snippet)} (${m.diffType})`).join('\n')}`
443
- : '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}`,
444
550
  {
445
551
  searchString: edit.old_string,
446
552
  visibleSearch: visibleWhitespace(edit.old_string),