@namzu/sdk 52.5.0 → 52.6.0
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/CHANGELOG.md +6 -0
- package/dist/tools/builtins/edit-apply.d.ts.map +1 -1
- package/dist/tools/builtins/edit-apply.js +30 -12
- package/dist/tools/builtins/edit-apply.js.map +1 -1
- package/dist/tools/builtins/edit-match.d.ts +47 -0
- package/dist/tools/builtins/edit-match.d.ts.map +1 -0
- package/dist/tools/builtins/edit-match.js +262 -0
- package/dist/tools/builtins/edit-match.js.map +1 -0
- package/package.json +1 -1
- package/src/tools/builtins/edit-apply.ts +31 -12
- package/src/tools/builtins/edit-match.ts +274 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 52.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c230860: Make `edit` recover a few `old_string` near-misses instead of failing, matching the reference terminal's resolver and order: a straight quote against the file's curly one (or the reverse), and a `\uXXXX` escape where the file holds the character (or a character where the file holds the escape). The matched span is a real substring of the file, so the uniqueness rule still applies and the same transformation is carried onto `new_string`. A whitespace or indentation difference is still not recovered — the error continues to tell the model to match those exactly. A deletion (empty `new_string`) now takes the matched line's trailing newline with it, so removing a whole line no longer leaves a blank one behind. The CRLF/LF reconciliation this kernel already did is unchanged.
|
|
8
|
+
|
|
3
9
|
## 52.5.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-apply.d.ts","sourceRoot":"","sources":["../../../src/tools/builtins/edit-apply.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"edit-apply.d.ts","sourceRoot":"","sources":["../../../src/tools/builtins/edit-apply.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAE1C;;;;;;;;;;GAUG;AAEH,MAAM,MAAM,mBAAmB,GAC5B;IACA,SAAS,EAAE,SAAS,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,OAAO,CAAA;CACnB,GACD;IACA,SAAS,EAAE,QAAQ,CAAA;IACnB,UAAU,EAAE,MAAM,GAAG,KAAK,CAAA;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,OAAO,CAAA;CACnB,CAAA;AAEJ;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CACjC,KAAK,EAAE,SAAS,GACd;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,mBAAmB,EAAE,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAuD1F;AAoCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CACxB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,SAAS,mBAAmB,EAAE,GACxC;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAiB9F;AA6ID;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GACtB;IACA,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;CAC5B,GACD;IAAE,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACzD;IACA,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACrB,CAAA;AAEJ;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,oBAAoB,CACnC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,OAAO,EACrB,SAAS,EAAE,MAAM,GACf,aAAa,CAOf;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACrC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,SAAS,mBAAmB,EAAE,EAC1C,SAAS,EAAE,MAAM,GACf,aAAa,CAyBf"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reconcileNewString, resolveOldString } from './edit-match.js';
|
|
1
2
|
/**
|
|
2
3
|
* Turn one call into the ordered list of operations it stands for.
|
|
3
4
|
*
|
|
@@ -131,24 +132,43 @@ function applyOne(content, input) {
|
|
|
131
132
|
return applyLineInsert(content, input);
|
|
132
133
|
}
|
|
133
134
|
const replacement = normalizeLineEndings(content, input);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
// Exact first — which, after the CRLF reconciliation above, is what nearly
|
|
136
|
+
// every call is. On a miss, recover the span across a difference the model
|
|
137
|
+
// did not mean: a curly quote for a straight one, a `\uXXXX` escape for the
|
|
138
|
+
// character or the reverse. `resolveOldString` returns the real span in the
|
|
139
|
+
// file, and `reconcileNewString` carries the same change onto the
|
|
140
|
+
// replacement so it fits where it lands. See `edit-match.ts`.
|
|
141
|
+
let oldString = replacement.oldString;
|
|
142
|
+
let newString = replacement.newString;
|
|
143
|
+
if (!content.includes(oldString)) {
|
|
144
|
+
const resolved = resolveOldString(content, oldString);
|
|
145
|
+
if (resolved === null) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
error: 'old_string/oldStr not found in file. Make sure the string matches exactly, including whitespace and indentation.',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
newString = reconcileNewString(oldString, resolved, newString);
|
|
152
|
+
oldString = resolved;
|
|
153
|
+
}
|
|
154
|
+
// A deletion takes the line's trailing newline with it, so removing a whole
|
|
155
|
+
// line does not leave a blank one behind. Only when the anchor is not
|
|
156
|
+
// already newline-terminated and the file has it followed by one.
|
|
157
|
+
if (newString === '' && !oldString.endsWith('\n') && content.includes(`${oldString}\n`)) {
|
|
158
|
+
oldString = `${oldString}\n`;
|
|
139
159
|
}
|
|
140
160
|
if (replacement.replace_all) {
|
|
141
|
-
const parts = content.split(
|
|
161
|
+
const parts = content.split(oldString);
|
|
142
162
|
const replacements = parts.length - 1;
|
|
143
163
|
return {
|
|
144
164
|
success: true,
|
|
145
|
-
content: parts.join(
|
|
165
|
+
content: parts.join(newString),
|
|
146
166
|
replacements,
|
|
147
167
|
};
|
|
148
168
|
}
|
|
149
169
|
// Uniqueness check: old_string/oldStr must appear exactly once
|
|
150
|
-
const firstIndex = content.indexOf(
|
|
151
|
-
const secondIndex = content.indexOf(
|
|
170
|
+
const firstIndex = content.indexOf(oldString);
|
|
171
|
+
const secondIndex = content.indexOf(oldString, firstIndex + 1);
|
|
152
172
|
if (secondIndex !== -1) {
|
|
153
173
|
const lineNumber = content.slice(0, firstIndex).split('\n').length;
|
|
154
174
|
const secondLine = content.slice(0, secondIndex).split('\n').length;
|
|
@@ -159,9 +179,7 @@ function applyOne(content, input) {
|
|
|
159
179
|
}
|
|
160
180
|
return {
|
|
161
181
|
success: true,
|
|
162
|
-
content: content.slice(0, firstIndex) +
|
|
163
|
-
replacement.newString +
|
|
164
|
-
content.slice(firstIndex + replacement.oldString.length),
|
|
182
|
+
content: content.slice(0, firstIndex) + newString + content.slice(firstIndex + oldString.length),
|
|
165
183
|
replacements: 1,
|
|
166
184
|
};
|
|
167
185
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-apply.js","sourceRoot":"","sources":["../../../src/tools/builtins/edit-apply.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"edit-apply.js","sourceRoot":"","sources":["../../../src/tools/builtins/edit-apply.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flexible `old_string` matching for the edit tool.
|
|
3
|
+
*
|
|
4
|
+
* When an exact match of `old_string` against the file fails, a handful of
|
|
5
|
+
* near-misses are not the model being wrong about the text, only about one
|
|
6
|
+
* invisible or interchangeable detail: a curly quote where the file has a
|
|
7
|
+
* straight one, or a `\uXXXX` escape where the file has the character itself
|
|
8
|
+
* (or the reverse). This module recovers the real span the model meant, and
|
|
9
|
+
* carries the same transformation across to `new_string` so the replacement
|
|
10
|
+
* stays consistent with what it replaced.
|
|
11
|
+
*
|
|
12
|
+
* The strategies and their order are a faithful port of the reference client's
|
|
13
|
+
* file-edit resolver (the `lje`/`b`/`_`/`L`/`k`/`R`/`C`/`cDs`/`ipt`/`H`/`Q`/`N`
|
|
14
|
+
* functions in its shipped bundle), so an edit that lands there lands here:
|
|
15
|
+
*
|
|
16
|
+
* 1. exact
|
|
17
|
+
* 2. curly vs straight quotes, both directions
|
|
18
|
+
* 3. `\uXXXX` escape in `old_string`, character in the file
|
|
19
|
+
* 4. character in `old_string`, `\uXXXX` escape in the file
|
|
20
|
+
*
|
|
21
|
+
* The reference also expands a map of its own prompt delimiters (`<fnr>` →
|
|
22
|
+
* `<function_results>`, `\n\nH:` → `\n\nHuman:`, …). That is deliberately NOT
|
|
23
|
+
* ported: those tokens are artifacts of that client's own transcript format,
|
|
24
|
+
* not a mismatch a Namzu model produces against a real source file.
|
|
25
|
+
*
|
|
26
|
+
* Pure by construction, like `edit-apply`: every function here is from strings
|
|
27
|
+
* to strings, with no filesystem access.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* The span of `content` that `oldString` refers to, recovering it across a
|
|
31
|
+
* quote- or escape-only difference, or null when nothing matches even then.
|
|
32
|
+
*
|
|
33
|
+
* The returned string is a real substring of `content` (or its escaped form),
|
|
34
|
+
* so the caller can run the ordinary uniqueness check and splice against it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveOldString(content: string, oldString: string): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Carry the transformation that matched `oldString` across to `newString`, so
|
|
39
|
+
* the replacement matches the span it lands in rather than the literal the
|
|
40
|
+
* model typed.
|
|
41
|
+
*
|
|
42
|
+
* `resolved` is what {@link resolveOldString} returned — equal to `oldString`
|
|
43
|
+
* for an exact match (then `newString` is returned unchanged), and otherwise
|
|
44
|
+
* the real span, which tells us which strategy fired.
|
|
45
|
+
*/
|
|
46
|
+
export declare function reconcileNewString(oldString: string, resolved: string, newString: string): string;
|
|
47
|
+
//# sourceMappingURL=edit-match.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edit-match.d.ts","sourceRoot":"","sources":["../../../src/tools/builtins/edit-match.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAkHH;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBlF;AAiDD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAiDjG"}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flexible `old_string` matching for the edit tool.
|
|
3
|
+
*
|
|
4
|
+
* When an exact match of `old_string` against the file fails, a handful of
|
|
5
|
+
* near-misses are not the model being wrong about the text, only about one
|
|
6
|
+
* invisible or interchangeable detail: a curly quote where the file has a
|
|
7
|
+
* straight one, or a `\uXXXX` escape where the file has the character itself
|
|
8
|
+
* (or the reverse). This module recovers the real span the model meant, and
|
|
9
|
+
* carries the same transformation across to `new_string` so the replacement
|
|
10
|
+
* stays consistent with what it replaced.
|
|
11
|
+
*
|
|
12
|
+
* The strategies and their order are a faithful port of the reference client's
|
|
13
|
+
* file-edit resolver (the `lje`/`b`/`_`/`L`/`k`/`R`/`C`/`cDs`/`ipt`/`H`/`Q`/`N`
|
|
14
|
+
* functions in its shipped bundle), so an edit that lands there lands here:
|
|
15
|
+
*
|
|
16
|
+
* 1. exact
|
|
17
|
+
* 2. curly vs straight quotes, both directions
|
|
18
|
+
* 3. `\uXXXX` escape in `old_string`, character in the file
|
|
19
|
+
* 4. character in `old_string`, `\uXXXX` escape in the file
|
|
20
|
+
*
|
|
21
|
+
* The reference also expands a map of its own prompt delimiters (`<fnr>` →
|
|
22
|
+
* `<function_results>`, `\n\nH:` → `\n\nHuman:`, …). That is deliberately NOT
|
|
23
|
+
* ported: those tokens are artifacts of that client's own transcript format,
|
|
24
|
+
* not a mismatch a Namzu model produces against a real source file.
|
|
25
|
+
*
|
|
26
|
+
* Pure by construction, like `edit-apply`: every function here is from strings
|
|
27
|
+
* to strings, with no filesystem access.
|
|
28
|
+
*/
|
|
29
|
+
const LEFT_SINGLE_QUOTE = '‘';
|
|
30
|
+
const RIGHT_SINGLE_QUOTE = '’';
|
|
31
|
+
const LEFT_DOUBLE_QUOTE = '“';
|
|
32
|
+
const RIGHT_DOUBLE_QUOTE = '”';
|
|
33
|
+
/** `old_string` carries a `\uXXXX` escape the file may hold as a character. */
|
|
34
|
+
const HAS_UNICODE_ESCAPE = /\\u[0-9a-fA-F]{4}/;
|
|
35
|
+
/** `old_string` carries a non-ASCII character the file may hold as an escape. */
|
|
36
|
+
const HAS_NON_ASCII = /[\u0080-]/;
|
|
37
|
+
/** Curly quotes to straight. Length-preserving: one character for one. */
|
|
38
|
+
function straightenQuotes(text) {
|
|
39
|
+
return text
|
|
40
|
+
.replaceAll(LEFT_SINGLE_QUOTE, "'")
|
|
41
|
+
.replaceAll(RIGHT_SINGLE_QUOTE, "'")
|
|
42
|
+
.replaceAll(LEFT_DOUBLE_QUOTE, '"')
|
|
43
|
+
.replaceAll(RIGHT_DOUBLE_QUOTE, '"');
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Replace each `\uXXXX` escape with its character, leaving an escaped
|
|
47
|
+
* backslash (`\\`) untouched so `\\u0041` stays the five characters it is
|
|
48
|
+
* rather than becoming a stray `A`.
|
|
49
|
+
*/
|
|
50
|
+
function decodeUnicodeEscapes(text) {
|
|
51
|
+
return text.replace(/(\\\\)|\\u([0-9a-fA-F]{4})/g, (whole, backslashes, hex) => backslashes !== undefined ? whole : String.fromCharCode(Number.parseInt(hex, 16)));
|
|
52
|
+
}
|
|
53
|
+
/** The length `text` would have if every non-ASCII character were a `\uXXXX`. */
|
|
54
|
+
function escapedLength(text) {
|
|
55
|
+
let nonAscii = 0;
|
|
56
|
+
for (let i = 0; i < text.length; i++)
|
|
57
|
+
if (text.charCodeAt(i) >= 128)
|
|
58
|
+
nonAscii++;
|
|
59
|
+
return text.length + 5 * nonAscii;
|
|
60
|
+
}
|
|
61
|
+
/** Consecutive backslashes immediately before index `at`. */
|
|
62
|
+
function trailingBackslashes(text, at) {
|
|
63
|
+
let count = 0;
|
|
64
|
+
while (text[at - 1 - count] === '\\')
|
|
65
|
+
count++;
|
|
66
|
+
return count;
|
|
67
|
+
}
|
|
68
|
+
/** Escape a literal string so it matches itself inside a `RegExp`. */
|
|
69
|
+
function escapeRegExp(text) {
|
|
70
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A pattern that matches `model` where every non-ASCII character appears as its
|
|
74
|
+
* `\uXXXX` escape (case-insensitive in the hex letters) and every ASCII
|
|
75
|
+
* character appears literally. Used to find, in a file written with escapes,
|
|
76
|
+
* the span a model wrote with the characters themselves.
|
|
77
|
+
*/
|
|
78
|
+
function buildEscapeTolerantPattern(model) {
|
|
79
|
+
let pattern = '';
|
|
80
|
+
for (let i = 0; i < model.length; i++) {
|
|
81
|
+
const code = model.charCodeAt(i);
|
|
82
|
+
if (code >= 128) {
|
|
83
|
+
pattern += '\\\\u';
|
|
84
|
+
for (const digit of code.toString(16).padStart(4, '0'))
|
|
85
|
+
pattern += digit >= 'a' ? `[${digit}${digit.toUpperCase()}]` : digit;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// biome-ignore lint/style/noNonNullAssertion: index < length
|
|
89
|
+
pattern += escapeRegExp(model[i]);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return pattern;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Find, in `file`, the escaped form of `model` — where `model` is written with
|
|
96
|
+
* real non-ASCII characters and `file` holds them as `\uXXXX`. Returns the
|
|
97
|
+
* matched (escaped) substring, or null when there is no such span, when the
|
|
98
|
+
* pattern would be ambiguous, or when a matched `\u` is itself escaped by an
|
|
99
|
+
* odd run of backslashes (a literal `\u`, not an escape).
|
|
100
|
+
*/
|
|
101
|
+
function findEscapedForm(model, file) {
|
|
102
|
+
// A model ending in an odd number of backslashes cannot be the clean prefix
|
|
103
|
+
// of an escape sequence; bail rather than build a pattern that misreads it.
|
|
104
|
+
if (trailingBackslashes(model, model.length) % 2 === 1)
|
|
105
|
+
return null;
|
|
106
|
+
// Offsets, within the escaped match, whose backslash parity must be checked
|
|
107
|
+
// so a literal `\u` in the file is not mistaken for an escape.
|
|
108
|
+
const sensitiveOffsets = model[0] === '\\' ? [0] : [];
|
|
109
|
+
let offset = 0;
|
|
110
|
+
for (let i = 0; i < model.length; i++) {
|
|
111
|
+
if (model.charCodeAt(i) >= 128) {
|
|
112
|
+
sensitiveOffsets.push(offset);
|
|
113
|
+
offset += 6;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
offset += 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
let regex;
|
|
120
|
+
try {
|
|
121
|
+
regex = new RegExp(buildEscapeTolerantPattern(model), 'g');
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
for (let match = regex.exec(file); match !== null; match = regex.exec(file)) {
|
|
127
|
+
const at = match.index;
|
|
128
|
+
if (sensitiveOffsets.every((o) => trailingBackslashes(file, at + o) % 2 === 0)) {
|
|
129
|
+
return file.indexOf(match[0]) === at ? match[0] : null;
|
|
130
|
+
}
|
|
131
|
+
regex.lastIndex = at + 1;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The span of `content` that `oldString` refers to, recovering it across a
|
|
137
|
+
* quote- or escape-only difference, or null when nothing matches even then.
|
|
138
|
+
*
|
|
139
|
+
* The returned string is a real substring of `content` (or its escaped form),
|
|
140
|
+
* so the caller can run the ordinary uniqueness check and splice against it.
|
|
141
|
+
*/
|
|
142
|
+
export function resolveOldString(content, oldString) {
|
|
143
|
+
if (content.includes(oldString))
|
|
144
|
+
return oldString;
|
|
145
|
+
const straight = straightenQuotes(oldString);
|
|
146
|
+
const quoteIndex = straightenQuotes(content).indexOf(straight);
|
|
147
|
+
if (quoteIndex !== -1)
|
|
148
|
+
return content.substring(quoteIndex, quoteIndex + oldString.length);
|
|
149
|
+
if (HAS_UNICODE_ESCAPE.test(oldString)) {
|
|
150
|
+
const decoded = decodeUnicodeEscapes(oldString);
|
|
151
|
+
if (decoded !== oldString && content.includes(decoded))
|
|
152
|
+
return decoded;
|
|
153
|
+
}
|
|
154
|
+
if (HAS_NON_ASCII.test(oldString)) {
|
|
155
|
+
if (escapedLength(oldString) > content.length || !content.includes('\\u'))
|
|
156
|
+
return null;
|
|
157
|
+
return findEscapedForm(oldString, content);
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
/** Is `char` at `index` opening a quote (preceded by a boundary) rather than closing one? */
|
|
162
|
+
function isOpeningPosition(chars, index) {
|
|
163
|
+
if (index === 0)
|
|
164
|
+
return true;
|
|
165
|
+
const before = chars[index - 1];
|
|
166
|
+
return (before === ' ' ||
|
|
167
|
+
before === '\t' ||
|
|
168
|
+
before === '\n' ||
|
|
169
|
+
before === '\r' ||
|
|
170
|
+
before === '(' ||
|
|
171
|
+
before === '[' ||
|
|
172
|
+
before === '{' ||
|
|
173
|
+
before === '—' ||
|
|
174
|
+
before === '–');
|
|
175
|
+
}
|
|
176
|
+
/** Apply curly double quotes to `text`, choosing opening vs closing by context. */
|
|
177
|
+
function curlifyDoubleQuotes(text) {
|
|
178
|
+
const chars = [...text];
|
|
179
|
+
return chars
|
|
180
|
+
.map((char, index) => char === '"'
|
|
181
|
+
? isOpeningPosition(chars, index)
|
|
182
|
+
? LEFT_DOUBLE_QUOTE
|
|
183
|
+
: RIGHT_DOUBLE_QUOTE
|
|
184
|
+
: char)
|
|
185
|
+
.join('');
|
|
186
|
+
}
|
|
187
|
+
/** Apply curly single quotes to `text`; an apostrophe between letters is always closing. */
|
|
188
|
+
function curlifySingleQuotes(text) {
|
|
189
|
+
const chars = [...text];
|
|
190
|
+
return chars
|
|
191
|
+
.map((char, index) => {
|
|
192
|
+
if (char !== "'")
|
|
193
|
+
return char;
|
|
194
|
+
const before = index > 0 ? chars[index - 1] : undefined;
|
|
195
|
+
const after = index < chars.length - 1 ? chars[index + 1] : undefined;
|
|
196
|
+
const betweenLetters = before !== undefined && /\p{L}/u.test(before) && after !== undefined && /\p{L}/u.test(after);
|
|
197
|
+
if (betweenLetters)
|
|
198
|
+
return RIGHT_SINGLE_QUOTE;
|
|
199
|
+
return isOpeningPosition(chars, index) ? LEFT_SINGLE_QUOTE : RIGHT_SINGLE_QUOTE;
|
|
200
|
+
})
|
|
201
|
+
.join('');
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Carry the transformation that matched `oldString` across to `newString`, so
|
|
205
|
+
* the replacement matches the span it lands in rather than the literal the
|
|
206
|
+
* model typed.
|
|
207
|
+
*
|
|
208
|
+
* `resolved` is what {@link resolveOldString} returned — equal to `oldString`
|
|
209
|
+
* for an exact match (then `newString` is returned unchanged), and otherwise
|
|
210
|
+
* the real span, which tells us which strategy fired.
|
|
211
|
+
*/
|
|
212
|
+
export function reconcileNewString(oldString, resolved, newString) {
|
|
213
|
+
if (resolved === oldString)
|
|
214
|
+
return newString;
|
|
215
|
+
// character in old, escape in file: re-escape new's non-ASCII to the file's
|
|
216
|
+
// hex spelling, matching the letter-case the file used.
|
|
217
|
+
if (HAS_NON_ASCII.test(oldString) &&
|
|
218
|
+
escapedLength(oldString) === resolved.length &&
|
|
219
|
+
findEscapedForm(oldString, resolved) !== null) {
|
|
220
|
+
const perChar = new Map();
|
|
221
|
+
let upper = 0;
|
|
222
|
+
let lower = 0;
|
|
223
|
+
for (let oldIndex = 0, resolvedIndex = 0; oldIndex < oldString.length; oldIndex++) {
|
|
224
|
+
const code = oldString.charCodeAt(oldIndex);
|
|
225
|
+
if (code >= 128) {
|
|
226
|
+
const hex = resolved.slice(resolvedIndex + 2, resolvedIndex + 6);
|
|
227
|
+
perChar.set(code, hex);
|
|
228
|
+
for (const digit of hex)
|
|
229
|
+
if (digit >= 'a' && digit <= 'f')
|
|
230
|
+
lower++;
|
|
231
|
+
else if (digit >= 'A' && digit <= 'F')
|
|
232
|
+
upper++;
|
|
233
|
+
resolvedIndex += 6;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
resolvedIndex += 1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return newString.replace(/[\u0080-]/g, (char) => {
|
|
240
|
+
const code = char.charCodeAt(0);
|
|
241
|
+
const known = perChar.get(code);
|
|
242
|
+
if (known !== undefined)
|
|
243
|
+
return `\\u${known}`;
|
|
244
|
+
const hex = code.toString(16).padStart(4, '0');
|
|
245
|
+
return `\\u${upper > lower ? hex.toUpperCase() : hex}`;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
// escape in old, character in file: decode new's escapes to match.
|
|
249
|
+
if (HAS_UNICODE_ESCAPE.test(oldString) && decodeUnicodeEscapes(oldString) === resolved) {
|
|
250
|
+
return decodeUnicodeEscapes(newString);
|
|
251
|
+
}
|
|
252
|
+
// quote difference: re-apply the file's curly quotes to new, by kind.
|
|
253
|
+
const hasCurlyDouble = resolved.includes(LEFT_DOUBLE_QUOTE) || resolved.includes(RIGHT_DOUBLE_QUOTE);
|
|
254
|
+
const hasCurlySingle = resolved.includes(LEFT_SINGLE_QUOTE) || resolved.includes(RIGHT_SINGLE_QUOTE);
|
|
255
|
+
let reconciled = newString;
|
|
256
|
+
if (hasCurlyDouble)
|
|
257
|
+
reconciled = curlifyDoubleQuotes(reconciled);
|
|
258
|
+
if (hasCurlySingle)
|
|
259
|
+
reconciled = curlifySingleQuotes(reconciled);
|
|
260
|
+
return reconciled;
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=edit-match.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edit-match.js","sourceRoot":"","sources":["../../../src/tools/builtins/edit-match.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,iBAAiB,GAAG,GAAG,CAAA;AAC7B,MAAM,kBAAkB,GAAG,GAAG,CAAA;AAC9B,MAAM,iBAAiB,GAAG,GAAG,CAAA;AAC7B,MAAM,kBAAkB,GAAG,GAAG,CAAA;AAE9B,+EAA+E;AAC/E,MAAM,kBAAkB,GAAG,mBAAmB,CAAA;AAC9C,iFAAiF;AACjF,MAAM,aAAa,GAAG,YAAY,CAAA;AAElC,0EAA0E;AAC1E,SAAS,gBAAgB,CAAC,IAAY;IACrC,OAAO,IAAI;SACT,UAAU,CAAC,iBAAiB,EAAE,GAAG,CAAC;SAClC,UAAU,CAAC,kBAAkB,EAAE,GAAG,CAAC;SACnC,UAAU,CAAC,iBAAiB,EAAE,GAAG,CAAC;SAClC,UAAU,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAA;AACtC,CAAC;AAED;;;;GAIG;AACH,SAAS,oBAAoB,CAAC,IAAY;IACzC,OAAO,IAAI,CAAC,OAAO,CAAC,6BAA6B,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,EAAE,CAC9E,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CACjF,CAAA;AACF,CAAC;AAED,iFAAiF;AACjF,SAAS,aAAa,CAAC,IAAY;IAClC,IAAI,QAAQ,GAAG,CAAC,CAAA;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,GAAG;YAAE,QAAQ,EAAE,CAAA;IAC/E,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,QAAQ,CAAA;AAClC,CAAC;AAED,6DAA6D;AAC7D,SAAS,mBAAmB,CAAC,IAAY,EAAE,EAAU;IACpD,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,OAAO,IAAI,CAAC,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,IAAI;QAAE,KAAK,EAAE,CAAA;IAC7C,OAAO,KAAK,CAAA;AACb,CAAC;AAED,sEAAsE;AACtE,SAAS,YAAY,CAAC,IAAY;IACjC,OAAO,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAA;AACnD,CAAC;AAED;;;;;GAKG;AACH,SAAS,0BAA0B,CAAC,KAAa;IAChD,IAAI,OAAO,GAAG,EAAE,CAAA;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;QAChC,IAAI,IAAI,IAAI,GAAG,EAAE,CAAC;YACjB,OAAO,IAAI,OAAO,CAAA;YAClB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC;gBACrD,OAAO,IAAI,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,KAAK,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,CAAA;QACtE,CAAC;aAAM,CAAC;YACP,6DAA6D;YAC7D,OAAO,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAA;QACnC,CAAC;IACF,CAAC;IACD,OAAO,OAAO,CAAA;AACf,CAAC;AAED;;;;;;GAMG;AACH,SAAS,eAAe,CAAC,KAAa,EAAE,IAAY;IACnD,4EAA4E;IAC5E,4EAA4E;IAC5E,IAAI,mBAAmB,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAEnE,4EAA4E;IAC5E,+DAA+D;IAC/D,MAAM,gBAAgB,GAAa,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC/D,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;YAChC,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YAC7B,MAAM,IAAI,CAAC,CAAA;QACZ,CAAC;aAAM,CAAC;YACP,MAAM,IAAI,CAAC,CAAA;QACZ,CAAC;IACF,CAAC;IAED,IAAI,KAAa,CAAA;IACjB,IAAI,CAAC;QACJ,KAAK,GAAG,IAAI,MAAM,CAAC,0BAA0B,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAA;IAC3D,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAA;IACZ,CAAC;IACD,KAAK,IAAI,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,KAAK,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7E,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,CAAA;QACtB,IAAI,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,mBAAmB,CAAC,IAAI,EAAE,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YAChF,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QACvD,CAAC;QACD,KAAK,CAAC,SAAS,GAAG,EAAE,GAAG,CAAC,CAAA;IACzB,CAAC;IACD,OAAO,IAAI,CAAA;AACZ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,SAAiB;IAClE,IAAI,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAA;IAEjD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAA;IAC5C,MAAM,UAAU,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC9D,IAAI,UAAU,KAAK,CAAC,CAAC;QAAE,OAAO,OAAO,CAAC,SAAS,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,CAAC,MAAM,CAAC,CAAA;IAE1F,IAAI,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QACxC,MAAM,OAAO,GAAG,oBAAoB,CAAC,SAAS,CAAC,CAAA;QAC/C,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,OAAO,CAAA;IACvE,CAAC;IAED,IAAI,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QACnC,IAAI,aAAa,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA;QACtF,OAAO,eAAe,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IAC3C,CAAC;IAED,OAAO,IAAI,CAAA;AACZ,CAAC;AAED,6FAA6F;AAC7F,SAAS,iBAAiB,CAAC,KAAwB,EAAE,KAAa;IACjE,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAC5B,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAA;IAC/B,OAAO,CACN,MAAM,KAAK,GAAG;QACd,MAAM,KAAK,IAAI;QACf,MAAM,KAAK,IAAI;QACf,MAAM,KAAK,IAAI;QACf,MAAM,KAAK,GAAG;QACd,MAAM,KAAK,GAAG;QACd,MAAM,KAAK,GAAG;QACd,MAAM,KAAK,GAAG;QACd,MAAM,KAAK,GAAG,CACd,CAAA;AACF,CAAC;AAED,mFAAmF;AACnF,SAAS,mBAAmB,CAAC,IAAY;IACxC,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,CAAA;IACvB,OAAO,KAAK;SACV,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CACpB,IAAI,KAAK,GAAG;QACX,CAAC,CAAC,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC;YAChC,CAAC,CAAC,iBAAiB;YACnB,CAAC,CAAC,kBAAkB;QACrB,CAAC,CAAC,IAAI,CACP;SACA,IAAI,CAAC,EAAE,CAAC,CAAA;AACX,CAAC;AAED,4FAA4F;AAC5F,SAAS,mBAAmB,CAAC,IAAY;IACxC,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,CAAA;IACvB,OAAO,KAAK;SACV,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QACpB,IAAI,IAAI,KAAK,GAAG;YAAE,OAAO,IAAI,CAAA;QAC7B,MAAM,MAAM,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QACvD,MAAM,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QACrE,MAAM,cAAc,GACnB,MAAM,KAAK,SAAS,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,KAAK,SAAS,IAAI,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC7F,IAAI,cAAc;YAAE,OAAO,kBAAkB,CAAA;QAC7C,OAAO,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,kBAAkB,CAAA;IAChF,CAAC,CAAC;SACD,IAAI,CAAC,EAAE,CAAC,CAAA;AACX,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,SAAiB,EAAE,QAAgB,EAAE,SAAiB;IACxF,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAE5C,4EAA4E;IAC5E,wDAAwD;IACxD,IACC,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC;QAC7B,aAAa,CAAC,SAAS,CAAC,KAAK,QAAQ,CAAC,MAAM;QAC5C,eAAe,CAAC,SAAS,EAAE,QAAQ,CAAC,KAAK,IAAI,EAC5C,CAAC;QACF,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAA;QACzC,IAAI,KAAK,GAAG,CAAC,CAAA;QACb,IAAI,KAAK,GAAG,CAAC,CAAA;QACb,KAAK,IAAI,QAAQ,GAAG,CAAC,EAAE,aAAa,GAAG,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,CAAC;YACnF,MAAM,IAAI,GAAG,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAA;YAC3C,IAAI,IAAI,IAAI,GAAG,EAAE,CAAC;gBACjB,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,aAAa,GAAG,CAAC,EAAE,aAAa,GAAG,CAAC,CAAC,CAAA;gBAChE,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;gBACtB,KAAK,MAAM,KAAK,IAAI,GAAG;oBACtB,IAAI,KAAK,IAAI,GAAG,IAAI,KAAK,IAAI,GAAG;wBAAE,KAAK,EAAE,CAAA;yBACpC,IAAI,KAAK,IAAI,GAAG,IAAI,KAAK,IAAI,GAAG;wBAAE,KAAK,EAAE,CAAA;gBAC/C,aAAa,IAAI,CAAC,CAAA;YACnB,CAAC;iBAAM,CAAC;gBACP,aAAa,IAAI,CAAC,CAAA;YACnB,CAAC;QACF,CAAC;QACD,OAAO,SAAS,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC,IAAI,EAAE,EAAE;YAChD,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;YAC/B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;YAC/B,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,MAAM,KAAK,EAAE,CAAA;YAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;YAC9C,OAAO,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAA;QACvD,CAAC,CAAC,CAAA;IACH,CAAC;IAED,mEAAmE;IACnE,IAAI,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,oBAAoB,CAAC,SAAS,CAAC,KAAK,QAAQ,EAAE,CAAC;QACxF,OAAO,oBAAoB,CAAC,SAAS,CAAC,CAAA;IACvC,CAAC;IAED,sEAAsE;IACtE,MAAM,cAAc,GACnB,QAAQ,CAAC,QAAQ,CAAC,iBAAiB,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAA;IAC9E,MAAM,cAAc,GACnB,QAAQ,CAAC,QAAQ,CAAC,iBAAiB,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAA;IAC9E,IAAI,UAAU,GAAG,SAAS,CAAA;IAC1B,IAAI,cAAc;QAAE,UAAU,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAA;IAChE,IAAI,cAAc;QAAE,UAAU,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAA;IAChE,OAAO,UAAU,CAAA;AAClB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@namzu/sdk",
|
|
3
|
-
"version": "52.
|
|
3
|
+
"version": "52.6.0",
|
|
4
4
|
"description": "Open-source AI kernel for TypeScript: runs an agent the way an operating system runs a process, with an identity and a budget, scheduled tool calls and checkpointed turns.",
|
|
5
5
|
"license": "FSL-1.1-MIT",
|
|
6
6
|
"engines": {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reconcileNewString, resolveOldString } from './edit-match.js'
|
|
1
2
|
import type { EditInput } from './edit.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -177,27 +178,47 @@ function applyOne(
|
|
|
177
178
|
|
|
178
179
|
const replacement = normalizeLineEndings(content, input)
|
|
179
180
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
// Exact first — which, after the CRLF reconciliation above, is what nearly
|
|
182
|
+
// every call is. On a miss, recover the span across a difference the model
|
|
183
|
+
// did not mean: a curly quote for a straight one, a `\uXXXX` escape for the
|
|
184
|
+
// character or the reverse. `resolveOldString` returns the real span in the
|
|
185
|
+
// file, and `reconcileNewString` carries the same change onto the
|
|
186
|
+
// replacement so it fits where it lands. See `edit-match.ts`.
|
|
187
|
+
let oldString = replacement.oldString
|
|
188
|
+
let newString = replacement.newString
|
|
189
|
+
if (!content.includes(oldString)) {
|
|
190
|
+
const resolved = resolveOldString(content, oldString)
|
|
191
|
+
if (resolved === null) {
|
|
192
|
+
return {
|
|
193
|
+
success: false,
|
|
194
|
+
error:
|
|
195
|
+
'old_string/oldStr not found in file. Make sure the string matches exactly, including whitespace and indentation.',
|
|
196
|
+
}
|
|
185
197
|
}
|
|
198
|
+
newString = reconcileNewString(oldString, resolved, newString)
|
|
199
|
+
oldString = resolved
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// A deletion takes the line's trailing newline with it, so removing a whole
|
|
203
|
+
// line does not leave a blank one behind. Only when the anchor is not
|
|
204
|
+
// already newline-terminated and the file has it followed by one.
|
|
205
|
+
if (newString === '' && !oldString.endsWith('\n') && content.includes(`${oldString}\n`)) {
|
|
206
|
+
oldString = `${oldString}\n`
|
|
186
207
|
}
|
|
187
208
|
|
|
188
209
|
if (replacement.replace_all) {
|
|
189
|
-
const parts = content.split(
|
|
210
|
+
const parts = content.split(oldString)
|
|
190
211
|
const replacements = parts.length - 1
|
|
191
212
|
return {
|
|
192
213
|
success: true,
|
|
193
|
-
content: parts.join(
|
|
214
|
+
content: parts.join(newString),
|
|
194
215
|
replacements,
|
|
195
216
|
}
|
|
196
217
|
}
|
|
197
218
|
|
|
198
219
|
// Uniqueness check: old_string/oldStr must appear exactly once
|
|
199
|
-
const firstIndex = content.indexOf(
|
|
200
|
-
const secondIndex = content.indexOf(
|
|
220
|
+
const firstIndex = content.indexOf(oldString)
|
|
221
|
+
const secondIndex = content.indexOf(oldString, firstIndex + 1)
|
|
201
222
|
|
|
202
223
|
if (secondIndex !== -1) {
|
|
203
224
|
const lineNumber = content.slice(0, firstIndex).split('\n').length
|
|
@@ -211,9 +232,7 @@ function applyOne(
|
|
|
211
232
|
return {
|
|
212
233
|
success: true,
|
|
213
234
|
content:
|
|
214
|
-
content.slice(0, firstIndex) +
|
|
215
|
-
replacement.newString +
|
|
216
|
-
content.slice(firstIndex + replacement.oldString.length),
|
|
235
|
+
content.slice(0, firstIndex) + newString + content.slice(firstIndex + oldString.length),
|
|
217
236
|
replacements: 1,
|
|
218
237
|
}
|
|
219
238
|
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flexible `old_string` matching for the edit tool.
|
|
3
|
+
*
|
|
4
|
+
* When an exact match of `old_string` against the file fails, a handful of
|
|
5
|
+
* near-misses are not the model being wrong about the text, only about one
|
|
6
|
+
* invisible or interchangeable detail: a curly quote where the file has a
|
|
7
|
+
* straight one, or a `\uXXXX` escape where the file has the character itself
|
|
8
|
+
* (or the reverse). This module recovers the real span the model meant, and
|
|
9
|
+
* carries the same transformation across to `new_string` so the replacement
|
|
10
|
+
* stays consistent with what it replaced.
|
|
11
|
+
*
|
|
12
|
+
* The strategies and their order are a faithful port of the reference client's
|
|
13
|
+
* file-edit resolver (the `lje`/`b`/`_`/`L`/`k`/`R`/`C`/`cDs`/`ipt`/`H`/`Q`/`N`
|
|
14
|
+
* functions in its shipped bundle), so an edit that lands there lands here:
|
|
15
|
+
*
|
|
16
|
+
* 1. exact
|
|
17
|
+
* 2. curly vs straight quotes, both directions
|
|
18
|
+
* 3. `\uXXXX` escape in `old_string`, character in the file
|
|
19
|
+
* 4. character in `old_string`, `\uXXXX` escape in the file
|
|
20
|
+
*
|
|
21
|
+
* The reference also expands a map of its own prompt delimiters (`<fnr>` →
|
|
22
|
+
* `<function_results>`, `\n\nH:` → `\n\nHuman:`, …). That is deliberately NOT
|
|
23
|
+
* ported: those tokens are artifacts of that client's own transcript format,
|
|
24
|
+
* not a mismatch a Namzu model produces against a real source file.
|
|
25
|
+
*
|
|
26
|
+
* Pure by construction, like `edit-apply`: every function here is from strings
|
|
27
|
+
* to strings, with no filesystem access.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const LEFT_SINGLE_QUOTE = '‘'
|
|
31
|
+
const RIGHT_SINGLE_QUOTE = '’'
|
|
32
|
+
const LEFT_DOUBLE_QUOTE = '“'
|
|
33
|
+
const RIGHT_DOUBLE_QUOTE = '”'
|
|
34
|
+
|
|
35
|
+
/** `old_string` carries a `\uXXXX` escape the file may hold as a character. */
|
|
36
|
+
const HAS_UNICODE_ESCAPE = /\\u[0-9a-fA-F]{4}/
|
|
37
|
+
/** `old_string` carries a non-ASCII character the file may hold as an escape. */
|
|
38
|
+
const HAS_NON_ASCII = /[\u0080-]/
|
|
39
|
+
|
|
40
|
+
/** Curly quotes to straight. Length-preserving: one character for one. */
|
|
41
|
+
function straightenQuotes(text: string): string {
|
|
42
|
+
return text
|
|
43
|
+
.replaceAll(LEFT_SINGLE_QUOTE, "'")
|
|
44
|
+
.replaceAll(RIGHT_SINGLE_QUOTE, "'")
|
|
45
|
+
.replaceAll(LEFT_DOUBLE_QUOTE, '"')
|
|
46
|
+
.replaceAll(RIGHT_DOUBLE_QUOTE, '"')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Replace each `\uXXXX` escape with its character, leaving an escaped
|
|
51
|
+
* backslash (`\\`) untouched so `\\u0041` stays the five characters it is
|
|
52
|
+
* rather than becoming a stray `A`.
|
|
53
|
+
*/
|
|
54
|
+
function decodeUnicodeEscapes(text: string): string {
|
|
55
|
+
return text.replace(/(\\\\)|\\u([0-9a-fA-F]{4})/g, (whole, backslashes, hex) =>
|
|
56
|
+
backslashes !== undefined ? whole : String.fromCharCode(Number.parseInt(hex, 16)),
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The length `text` would have if every non-ASCII character were a `\uXXXX`. */
|
|
61
|
+
function escapedLength(text: string): number {
|
|
62
|
+
let nonAscii = 0
|
|
63
|
+
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) >= 128) nonAscii++
|
|
64
|
+
return text.length + 5 * nonAscii
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Consecutive backslashes immediately before index `at`. */
|
|
68
|
+
function trailingBackslashes(text: string, at: number): number {
|
|
69
|
+
let count = 0
|
|
70
|
+
while (text[at - 1 - count] === '\\') count++
|
|
71
|
+
return count
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Escape a literal string so it matches itself inside a `RegExp`. */
|
|
75
|
+
function escapeRegExp(text: string): string {
|
|
76
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A pattern that matches `model` where every non-ASCII character appears as its
|
|
81
|
+
* `\uXXXX` escape (case-insensitive in the hex letters) and every ASCII
|
|
82
|
+
* character appears literally. Used to find, in a file written with escapes,
|
|
83
|
+
* the span a model wrote with the characters themselves.
|
|
84
|
+
*/
|
|
85
|
+
function buildEscapeTolerantPattern(model: string): string {
|
|
86
|
+
let pattern = ''
|
|
87
|
+
for (let i = 0; i < model.length; i++) {
|
|
88
|
+
const code = model.charCodeAt(i)
|
|
89
|
+
if (code >= 128) {
|
|
90
|
+
pattern += '\\\\u'
|
|
91
|
+
for (const digit of code.toString(16).padStart(4, '0'))
|
|
92
|
+
pattern += digit >= 'a' ? `[${digit}${digit.toUpperCase()}]` : digit
|
|
93
|
+
} else {
|
|
94
|
+
// biome-ignore lint/style/noNonNullAssertion: index < length
|
|
95
|
+
pattern += escapeRegExp(model[i]!)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return pattern
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Find, in `file`, the escaped form of `model` — where `model` is written with
|
|
103
|
+
* real non-ASCII characters and `file` holds them as `\uXXXX`. Returns the
|
|
104
|
+
* matched (escaped) substring, or null when there is no such span, when the
|
|
105
|
+
* pattern would be ambiguous, or when a matched `\u` is itself escaped by an
|
|
106
|
+
* odd run of backslashes (a literal `\u`, not an escape).
|
|
107
|
+
*/
|
|
108
|
+
function findEscapedForm(model: string, file: string): string | null {
|
|
109
|
+
// A model ending in an odd number of backslashes cannot be the clean prefix
|
|
110
|
+
// of an escape sequence; bail rather than build a pattern that misreads it.
|
|
111
|
+
if (trailingBackslashes(model, model.length) % 2 === 1) return null
|
|
112
|
+
|
|
113
|
+
// Offsets, within the escaped match, whose backslash parity must be checked
|
|
114
|
+
// so a literal `\u` in the file is not mistaken for an escape.
|
|
115
|
+
const sensitiveOffsets: number[] = model[0] === '\\' ? [0] : []
|
|
116
|
+
let offset = 0
|
|
117
|
+
for (let i = 0; i < model.length; i++) {
|
|
118
|
+
if (model.charCodeAt(i) >= 128) {
|
|
119
|
+
sensitiveOffsets.push(offset)
|
|
120
|
+
offset += 6
|
|
121
|
+
} else {
|
|
122
|
+
offset += 1
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let regex: RegExp
|
|
127
|
+
try {
|
|
128
|
+
regex = new RegExp(buildEscapeTolerantPattern(model), 'g')
|
|
129
|
+
} catch {
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
for (let match = regex.exec(file); match !== null; match = regex.exec(file)) {
|
|
133
|
+
const at = match.index
|
|
134
|
+
if (sensitiveOffsets.every((o) => trailingBackslashes(file, at + o) % 2 === 0)) {
|
|
135
|
+
return file.indexOf(match[0]) === at ? match[0] : null
|
|
136
|
+
}
|
|
137
|
+
regex.lastIndex = at + 1
|
|
138
|
+
}
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The span of `content` that `oldString` refers to, recovering it across a
|
|
144
|
+
* quote- or escape-only difference, or null when nothing matches even then.
|
|
145
|
+
*
|
|
146
|
+
* The returned string is a real substring of `content` (or its escaped form),
|
|
147
|
+
* so the caller can run the ordinary uniqueness check and splice against it.
|
|
148
|
+
*/
|
|
149
|
+
export function resolveOldString(content: string, oldString: string): string | null {
|
|
150
|
+
if (content.includes(oldString)) return oldString
|
|
151
|
+
|
|
152
|
+
const straight = straightenQuotes(oldString)
|
|
153
|
+
const quoteIndex = straightenQuotes(content).indexOf(straight)
|
|
154
|
+
if (quoteIndex !== -1) return content.substring(quoteIndex, quoteIndex + oldString.length)
|
|
155
|
+
|
|
156
|
+
if (HAS_UNICODE_ESCAPE.test(oldString)) {
|
|
157
|
+
const decoded = decodeUnicodeEscapes(oldString)
|
|
158
|
+
if (decoded !== oldString && content.includes(decoded)) return decoded
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (HAS_NON_ASCII.test(oldString)) {
|
|
162
|
+
if (escapedLength(oldString) > content.length || !content.includes('\\u')) return null
|
|
163
|
+
return findEscapedForm(oldString, content)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Is `char` at `index` opening a quote (preceded by a boundary) rather than closing one? */
|
|
170
|
+
function isOpeningPosition(chars: readonly string[], index: number): boolean {
|
|
171
|
+
if (index === 0) return true
|
|
172
|
+
const before = chars[index - 1]
|
|
173
|
+
return (
|
|
174
|
+
before === ' ' ||
|
|
175
|
+
before === '\t' ||
|
|
176
|
+
before === '\n' ||
|
|
177
|
+
before === '\r' ||
|
|
178
|
+
before === '(' ||
|
|
179
|
+
before === '[' ||
|
|
180
|
+
before === '{' ||
|
|
181
|
+
before === '—' ||
|
|
182
|
+
before === '–'
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Apply curly double quotes to `text`, choosing opening vs closing by context. */
|
|
187
|
+
function curlifyDoubleQuotes(text: string): string {
|
|
188
|
+
const chars = [...text]
|
|
189
|
+
return chars
|
|
190
|
+
.map((char, index) =>
|
|
191
|
+
char === '"'
|
|
192
|
+
? isOpeningPosition(chars, index)
|
|
193
|
+
? LEFT_DOUBLE_QUOTE
|
|
194
|
+
: RIGHT_DOUBLE_QUOTE
|
|
195
|
+
: char,
|
|
196
|
+
)
|
|
197
|
+
.join('')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Apply curly single quotes to `text`; an apostrophe between letters is always closing. */
|
|
201
|
+
function curlifySingleQuotes(text: string): string {
|
|
202
|
+
const chars = [...text]
|
|
203
|
+
return chars
|
|
204
|
+
.map((char, index) => {
|
|
205
|
+
if (char !== "'") return char
|
|
206
|
+
const before = index > 0 ? chars[index - 1] : undefined
|
|
207
|
+
const after = index < chars.length - 1 ? chars[index + 1] : undefined
|
|
208
|
+
const betweenLetters =
|
|
209
|
+
before !== undefined && /\p{L}/u.test(before) && after !== undefined && /\p{L}/u.test(after)
|
|
210
|
+
if (betweenLetters) return RIGHT_SINGLE_QUOTE
|
|
211
|
+
return isOpeningPosition(chars, index) ? LEFT_SINGLE_QUOTE : RIGHT_SINGLE_QUOTE
|
|
212
|
+
})
|
|
213
|
+
.join('')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Carry the transformation that matched `oldString` across to `newString`, so
|
|
218
|
+
* the replacement matches the span it lands in rather than the literal the
|
|
219
|
+
* model typed.
|
|
220
|
+
*
|
|
221
|
+
* `resolved` is what {@link resolveOldString} returned — equal to `oldString`
|
|
222
|
+
* for an exact match (then `newString` is returned unchanged), and otherwise
|
|
223
|
+
* the real span, which tells us which strategy fired.
|
|
224
|
+
*/
|
|
225
|
+
export function reconcileNewString(oldString: string, resolved: string, newString: string): string {
|
|
226
|
+
if (resolved === oldString) return newString
|
|
227
|
+
|
|
228
|
+
// character in old, escape in file: re-escape new's non-ASCII to the file's
|
|
229
|
+
// hex spelling, matching the letter-case the file used.
|
|
230
|
+
if (
|
|
231
|
+
HAS_NON_ASCII.test(oldString) &&
|
|
232
|
+
escapedLength(oldString) === resolved.length &&
|
|
233
|
+
findEscapedForm(oldString, resolved) !== null
|
|
234
|
+
) {
|
|
235
|
+
const perChar = new Map<number, string>()
|
|
236
|
+
let upper = 0
|
|
237
|
+
let lower = 0
|
|
238
|
+
for (let oldIndex = 0, resolvedIndex = 0; oldIndex < oldString.length; oldIndex++) {
|
|
239
|
+
const code = oldString.charCodeAt(oldIndex)
|
|
240
|
+
if (code >= 128) {
|
|
241
|
+
const hex = resolved.slice(resolvedIndex + 2, resolvedIndex + 6)
|
|
242
|
+
perChar.set(code, hex)
|
|
243
|
+
for (const digit of hex)
|
|
244
|
+
if (digit >= 'a' && digit <= 'f') lower++
|
|
245
|
+
else if (digit >= 'A' && digit <= 'F') upper++
|
|
246
|
+
resolvedIndex += 6
|
|
247
|
+
} else {
|
|
248
|
+
resolvedIndex += 1
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return newString.replace(/[\u0080-]/g, (char) => {
|
|
252
|
+
const code = char.charCodeAt(0)
|
|
253
|
+
const known = perChar.get(code)
|
|
254
|
+
if (known !== undefined) return `\\u${known}`
|
|
255
|
+
const hex = code.toString(16).padStart(4, '0')
|
|
256
|
+
return `\\u${upper > lower ? hex.toUpperCase() : hex}`
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// escape in old, character in file: decode new's escapes to match.
|
|
261
|
+
if (HAS_UNICODE_ESCAPE.test(oldString) && decodeUnicodeEscapes(oldString) === resolved) {
|
|
262
|
+
return decodeUnicodeEscapes(newString)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// quote difference: re-apply the file's curly quotes to new, by kind.
|
|
266
|
+
const hasCurlyDouble =
|
|
267
|
+
resolved.includes(LEFT_DOUBLE_QUOTE) || resolved.includes(RIGHT_DOUBLE_QUOTE)
|
|
268
|
+
const hasCurlySingle =
|
|
269
|
+
resolved.includes(LEFT_SINGLE_QUOTE) || resolved.includes(RIGHT_SINGLE_QUOTE)
|
|
270
|
+
let reconciled = newString
|
|
271
|
+
if (hasCurlyDouble) reconciled = curlifyDoubleQuotes(reconciled)
|
|
272
|
+
if (hasCurlySingle) reconciled = curlifySingleQuotes(reconciled)
|
|
273
|
+
return reconciled
|
|
274
|
+
}
|