@bubblebrain-ai/bubble 0.0.13 → 0.0.14
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/dist/agent/execution-governor.js +1 -1
- package/dist/agent/tool-intent.js +1 -0
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +589 -316
- package/dist/approval/controller.d.ts +1 -0
- package/dist/approval/controller.js +20 -3
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +14 -1
- package/dist/context/compact.js +9 -3
- package/dist/context/projector.js +27 -12
- package/dist/debug-trace.d.ts +27 -0
- package/dist/debug-trace.js +385 -0
- package/dist/feishu/agent-host/approval-card.js +9 -0
- package/dist/feishu/serve.js +7 -1
- package/dist/main.js +28 -0
- package/dist/model-catalog.js +1 -0
- package/dist/orchestrator/default-hooks.js +19 -8
- package/dist/orchestrator/hooks.d.ts +1 -0
- package/dist/prompt/environment.js +2 -0
- package/dist/prompt/reminders.d.ts +5 -6
- package/dist/prompt/reminders.js +8 -9
- package/dist/prompt/runtime.js +2 -2
- package/dist/provider-openai-codex.d.ts +7 -0
- package/dist/provider-openai-codex.js +265 -124
- package/dist/provider-registry.d.ts +2 -0
- package/dist/provider-registry.js +58 -9
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +5 -1
- package/dist/session-log.js +13 -1
- package/dist/slash-commands/commands.js +12 -0
- package/dist/slash-commands/types.d.ts +2 -0
- package/dist/stats/usage.d.ts +52 -0
- package/dist/stats/usage.js +414 -0
- package/dist/tools/apply-patch.d.ts +9 -0
- package/dist/tools/apply-patch.js +330 -0
- package/dist/tools/bash.js +205 -44
- package/dist/tools/edit-apply.d.ts +5 -2
- package/dist/tools/edit-apply.js +221 -31
- package/dist/tools/edit.js +12 -3
- package/dist/tools/file-mutation-queue.d.ts +1 -0
- package/dist/tools/file-mutation-queue.js +12 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +7 -1
- package/dist/tools/patch-apply.d.ts +41 -0
- package/dist/tools/patch-apply.js +312 -0
- package/dist/tools/server-manager.d.ts +36 -0
- package/dist/tools/server-manager.js +234 -0
- package/dist/tools/server.d.ts +6 -0
- package/dist/tools/server.js +245 -0
- package/dist/tools/write.d.ts +3 -6
- package/dist/tools/write.js +26 -46
- package/dist/tui/display-history.d.ts +1 -0
- package/dist/tui/display-history.js +5 -4
- package/dist/tui/edit-diff.js +6 -1
- package/dist/tui/model-picker-data.d.ts +10 -0
- package/dist/tui/model-picker-data.js +32 -0
- package/dist/tui/run.js +632 -89
- package/dist/tui/tool-renderers/fallback.js +1 -1
- package/dist/tui/tool-renderers/write-preview.js +2 -0
- package/dist/tui/trace-groups.js +10 -3
- package/dist/tui-ink/app.js +1 -4
- package/dist/tui-ink/approval/approval-dialog.js +7 -1
- package/dist/tui-ink/display-history.d.ts +1 -0
- package/dist/tui-ink/display-history.js +5 -4
- package/dist/tui-ink/message-list.js +14 -8
- package/dist/tui-ink/trace-groups.js +1 -1
- package/dist/tui-opentui/app.js +2 -0
- package/dist/tui-opentui/approval/approval-dialog.js +7 -1
- package/dist/tui-opentui/display-history.d.ts +1 -0
- package/dist/tui-opentui/display-history.js +5 -4
- package/dist/tui-opentui/edit-diff.js +6 -1
- package/dist/tui-opentui/message-list.js +6 -3
- package/dist/tui-opentui/trace-groups.js +10 -3
- package/dist/types.d.ts +12 -2
- package/package.json +1 -1
package/dist/tools/edit-apply.js
CHANGED
|
@@ -17,6 +17,36 @@ function stripBom(content) {
|
|
|
17
17
|
function normalizeToLF(text) {
|
|
18
18
|
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
19
19
|
}
|
|
20
|
+
function unescapeOverEscaped(text) {
|
|
21
|
+
return text
|
|
22
|
+
.replace(/\\u([0-9a-fA-F]{4})/g, (_match, hex) => String.fromCodePoint(Number.parseInt(hex, 16)))
|
|
23
|
+
.replace(/\\n/g, "\n")
|
|
24
|
+
.replace(/\\t/g, "\t")
|
|
25
|
+
.replace(/\\r/g, "\r")
|
|
26
|
+
.replace(/\\f/g, "\f")
|
|
27
|
+
.replace(/\\b/g, "\b")
|
|
28
|
+
.replace(/\\v/g, "\v")
|
|
29
|
+
.replace(/\\"/g, '"')
|
|
30
|
+
.replace(/\\\\/g, "\\");
|
|
31
|
+
}
|
|
32
|
+
function addTextCandidate(candidates, seen, text, mode) {
|
|
33
|
+
if (text.length === 0 || seen.has(text))
|
|
34
|
+
return;
|
|
35
|
+
seen.add(text);
|
|
36
|
+
candidates.push({ text, mode });
|
|
37
|
+
}
|
|
38
|
+
function generateTextCandidates(oldText) {
|
|
39
|
+
const candidates = [];
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const trimmed = oldText.trim();
|
|
42
|
+
const unescaped = normalizeToLF(unescapeOverEscaped(oldText));
|
|
43
|
+
const unescapedTrimmed = normalizeToLF(unescapeOverEscaped(trimmed));
|
|
44
|
+
addTextCandidate(candidates, seen, oldText, "exact");
|
|
45
|
+
addTextCandidate(candidates, seen, trimmed, "trimmed");
|
|
46
|
+
addTextCandidate(candidates, seen, unescaped, "unescaped");
|
|
47
|
+
addTextCandidate(candidates, seen, unescapedTrimmed, "unescaped");
|
|
48
|
+
return candidates;
|
|
49
|
+
}
|
|
20
50
|
function restoreLineEndings(text, lineEnding) {
|
|
21
51
|
return lineEnding === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
|
|
22
52
|
}
|
|
@@ -29,6 +59,9 @@ function normalizeLineForMatch(line) {
|
|
|
29
59
|
.replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, "-")
|
|
30
60
|
.replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ");
|
|
31
61
|
}
|
|
62
|
+
function normalizeLeadingWhitespaceForMatch(line) {
|
|
63
|
+
return normalizeLineForMatch(line).replace(/^\s+/, " ");
|
|
64
|
+
}
|
|
32
65
|
function findAllOccurrences(content, needle) {
|
|
33
66
|
const indexes = [];
|
|
34
67
|
if (needle.length === 0)
|
|
@@ -67,6 +100,10 @@ function normalizedOldNonBlankLines(oldText) {
|
|
|
67
100
|
.map((line) => normalizeLineForMatch(line.text))
|
|
68
101
|
.filter((line) => line.trim().length > 0);
|
|
69
102
|
}
|
|
103
|
+
function singleNonBlankOldLine(oldText) {
|
|
104
|
+
const lines = normalizedOldNonBlankLines(oldText);
|
|
105
|
+
return lines.length === 1 ? lines[0] : undefined;
|
|
106
|
+
}
|
|
70
107
|
function findNormalizedLineMatches(content, oldText) {
|
|
71
108
|
const contentLines = splitLines(content);
|
|
72
109
|
const searchable = nonBlankLines(contentLines);
|
|
@@ -90,6 +127,103 @@ function findNormalizedLineMatches(content, oldText) {
|
|
|
90
127
|
}
|
|
91
128
|
return matches;
|
|
92
129
|
}
|
|
130
|
+
function findSmartLineMatches(content, oldText) {
|
|
131
|
+
const contentLines = splitLines(content);
|
|
132
|
+
const oldLines = splitLines(oldText).map((line) => normalizeLeadingWhitespaceForMatch(line.text));
|
|
133
|
+
if (oldLines.length === 0 || oldLines.every((line) => line.length === 0))
|
|
134
|
+
return [];
|
|
135
|
+
const matches = [];
|
|
136
|
+
for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
|
|
137
|
+
let matched = true;
|
|
138
|
+
for (let j = 0; j < oldLines.length; j++) {
|
|
139
|
+
const actual = normalizeLeadingWhitespaceForMatch(contentLines[i + j].text);
|
|
140
|
+
if (actual !== oldLines[j]) {
|
|
141
|
+
matched = false;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (matched) {
|
|
146
|
+
const first = contentLines[i];
|
|
147
|
+
const last = contentLines[i + oldLines.length - 1];
|
|
148
|
+
matches.push({ start: first.start, end: last.endNoNewline });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return matches;
|
|
152
|
+
}
|
|
153
|
+
function splitMarkdownTableCells(line) {
|
|
154
|
+
const normalized = normalizeLineForMatch(line).trim();
|
|
155
|
+
if (!normalized.startsWith("|") || !normalized.endsWith("|"))
|
|
156
|
+
return undefined;
|
|
157
|
+
const parts = [];
|
|
158
|
+
let current = "";
|
|
159
|
+
let escaped = false;
|
|
160
|
+
for (const char of normalized) {
|
|
161
|
+
if (escaped) {
|
|
162
|
+
current += char;
|
|
163
|
+
escaped = false;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (char === "\\") {
|
|
167
|
+
current += char;
|
|
168
|
+
escaped = true;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (char === "|") {
|
|
172
|
+
parts.push(current);
|
|
173
|
+
current = "";
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
current += char;
|
|
177
|
+
}
|
|
178
|
+
parts.push(current);
|
|
179
|
+
if (parts.length < 4 || parts[0] !== "" || parts[parts.length - 1] !== "")
|
|
180
|
+
return undefined;
|
|
181
|
+
const cells = parts.slice(1, -1).map((cell) => cell.trim());
|
|
182
|
+
return cells.length >= 2 ? cells : undefined;
|
|
183
|
+
}
|
|
184
|
+
function sameCells(a, b) {
|
|
185
|
+
return a.length === b.length && a.every((cell, index) => cell === b[index]);
|
|
186
|
+
}
|
|
187
|
+
function findMarkdownTableMatches(content, oldText) {
|
|
188
|
+
const oldLine = singleNonBlankOldLine(oldText);
|
|
189
|
+
if (!oldLine)
|
|
190
|
+
return [];
|
|
191
|
+
const oldCells = splitMarkdownTableCells(oldLine);
|
|
192
|
+
if (!oldCells)
|
|
193
|
+
return [];
|
|
194
|
+
const matches = [];
|
|
195
|
+
for (const line of splitLines(content)) {
|
|
196
|
+
const cells = splitMarkdownTableCells(line.text);
|
|
197
|
+
if (cells && sameCells(cells, oldCells)) {
|
|
198
|
+
matches.push({ start: line.start, end: line.endNoNewline });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return matches;
|
|
202
|
+
}
|
|
203
|
+
function collapseInlineWhitespace(text) {
|
|
204
|
+
return normalizeLineForMatch(text).trim().replace(/[ \t]+/g, " ");
|
|
205
|
+
}
|
|
206
|
+
function isDocumentLikePath(path) {
|
|
207
|
+
return !!path && /\.(?:md|mdx|markdown|txt|rst|adoc)$/i.test(path);
|
|
208
|
+
}
|
|
209
|
+
function findSingleLineWhitespaceMatches(content, oldText, options) {
|
|
210
|
+
if (!isDocumentLikePath(options?.path))
|
|
211
|
+
return [];
|
|
212
|
+
const oldLine = singleNonBlankOldLine(oldText);
|
|
213
|
+
if (!oldLine)
|
|
214
|
+
return [];
|
|
215
|
+
const normalizedOld = collapseInlineWhitespace(oldLine);
|
|
216
|
+
if (normalizedOld.length === 0)
|
|
217
|
+
return [];
|
|
218
|
+
const matches = [];
|
|
219
|
+
for (const line of splitLines(content)) {
|
|
220
|
+
const collapsed = collapseInlineWhitespace(line.text);
|
|
221
|
+
if (collapsed === normalizedOld && line.text !== oldLine) {
|
|
222
|
+
matches.push({ start: line.start, end: line.endNoNewline });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return matches;
|
|
226
|
+
}
|
|
93
227
|
function summarizeOldText(oldText) {
|
|
94
228
|
const firstLine = normalizeToLF(oldText).split("\n").find((line) => line.trim().length > 0) ?? oldText;
|
|
95
229
|
return firstLine.length > 80 ? `${firstLine.slice(0, 80)}...` : firstLine;
|
|
@@ -114,7 +248,7 @@ function findBestLineHint(content, oldText) {
|
|
|
114
248
|
const startLine = contentLines[best.index].lineIndex + 1;
|
|
115
249
|
return `Closest line-based candidate starts near line ${startLine} and matched ${best.score}/${oldLines.length} non-blank lines.`;
|
|
116
250
|
}
|
|
117
|
-
function matchEdit(content, edit, index, total) {
|
|
251
|
+
function matchEdit(content, edit, index, total, options) {
|
|
118
252
|
if (edit.oldText.length === 0) {
|
|
119
253
|
throw new EditApplyError(total === 1 ? "Error: oldText must not be empty." : `Error: edits[${index}].oldText must not be empty.`);
|
|
120
254
|
}
|
|
@@ -127,37 +261,93 @@ function matchEdit(content, edit, index, total) {
|
|
|
127
261
|
"",
|
|
128
262
|
"Common causes and how to escape:",
|
|
129
263
|
"- Your tokenizer may be folding repeated characters into a single token (hex colors like '#ec489' vs '#ec4899', repeated digits, etc.). The two strings feel different in your head but serialize to identical bytes.",
|
|
130
|
-
"- Use the write tool with
|
|
264
|
+
"- Use the write tool with the full new content for full-file replacements that hinge on a single repeated character or trailing digit.",
|
|
131
265
|
"- Or re-read the file with the read tool, then copy the exact bytes you want to replace before retrying.",
|
|
132
266
|
].join("\n"));
|
|
133
267
|
}
|
|
134
268
|
const oldText = normalizeToLF(edit.oldText);
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
269
|
+
const candidates = generateTextCandidates(oldText);
|
|
270
|
+
for (const candidate of candidates) {
|
|
271
|
+
const exact = findAllOccurrences(content, candidate.text);
|
|
272
|
+
if (exact.length === 1) {
|
|
273
|
+
return { editIndex: index, mode: candidate.mode, start: exact[0], end: exact[0] + candidate.text.length };
|
|
274
|
+
}
|
|
275
|
+
if (exact.length > 1) {
|
|
276
|
+
const recovery = [
|
|
277
|
+
"",
|
|
278
|
+
"Extend oldText with more surrounding context (the lines immediately before/after) until it uniquely identifies the intended span.",
|
|
279
|
+
].join("\n");
|
|
280
|
+
const duplicateReason = candidate.mode === "exact"
|
|
281
|
+
? `appears ${exact.length} times in file`
|
|
282
|
+
: `matched ${exact.length} times after ${candidate.mode} matching`;
|
|
283
|
+
throw new EditApplyError(total === 1
|
|
284
|
+
? `Error: oldText ${duplicateReason}. Must be unique: "${summarizeOldText(oldText)}"${recovery}`
|
|
285
|
+
: `Error: edits[${index}].oldText ${duplicateReason}. Must be unique: "${summarizeOldText(oldText)}"${recovery}`);
|
|
286
|
+
}
|
|
138
287
|
}
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
288
|
+
for (const candidate of candidates) {
|
|
289
|
+
const normalizedLineMatches = findNormalizedLineMatches(content, candidate.text);
|
|
290
|
+
if (normalizedLineMatches.length === 1) {
|
|
291
|
+
return {
|
|
292
|
+
editIndex: index,
|
|
293
|
+
mode: candidate.mode === "exact" ? "normalized-line" : candidate.mode,
|
|
294
|
+
start: normalizedLineMatches[0].start,
|
|
295
|
+
end: normalizedLineMatches[0].end,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (normalizedLineMatches.length > 1) {
|
|
299
|
+
throw new EditApplyError(total === 1
|
|
300
|
+
? `Error: oldText matched ${normalizedLineMatches.length} normalized line blocks in file. Provide more surrounding context.`
|
|
301
|
+
: `Error: edits[${index}].oldText matched ${normalizedLineMatches.length} normalized line blocks in file. Provide more surrounding context.`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
for (const candidate of candidates) {
|
|
305
|
+
const markdownTableMatches = findMarkdownTableMatches(content, candidate.text);
|
|
306
|
+
if (markdownTableMatches.length === 1) {
|
|
307
|
+
return {
|
|
308
|
+
editIndex: index,
|
|
309
|
+
mode: "markdown-table",
|
|
310
|
+
start: markdownTableMatches[0].start,
|
|
311
|
+
end: markdownTableMatches[0].end,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (markdownTableMatches.length > 1) {
|
|
315
|
+
throw new EditApplyError(total === 1
|
|
316
|
+
? `Error: oldText matched ${markdownTableMatches.length} markdown table rows in file. Provide more surrounding context.`
|
|
317
|
+
: `Error: edits[${index}].oldText matched ${markdownTableMatches.length} markdown table rows in file. Provide more surrounding context.`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
for (const candidate of candidates) {
|
|
321
|
+
const whitespaceMatches = findSingleLineWhitespaceMatches(content, candidate.text, options);
|
|
322
|
+
if (whitespaceMatches.length === 1) {
|
|
323
|
+
return {
|
|
324
|
+
editIndex: index,
|
|
325
|
+
mode: "single-line-whitespace",
|
|
326
|
+
start: whitespaceMatches[0].start,
|
|
327
|
+
end: whitespaceMatches[0].end,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (whitespaceMatches.length > 1) {
|
|
331
|
+
throw new EditApplyError(total === 1
|
|
332
|
+
? `Error: oldText matched ${whitespaceMatches.length} whitespace-normalized lines in file. Provide more surrounding context.`
|
|
333
|
+
: `Error: edits[${index}].oldText matched ${whitespaceMatches.length} whitespace-normalized lines in file. Provide more surrounding context.`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
for (const candidate of candidates) {
|
|
337
|
+
const smartLineMatches = findSmartLineMatches(content, candidate.text);
|
|
338
|
+
if (smartLineMatches.length === 1) {
|
|
339
|
+
return {
|
|
340
|
+
editIndex: index,
|
|
341
|
+
mode: "smart-line",
|
|
342
|
+
start: smartLineMatches[0].start,
|
|
343
|
+
end: smartLineMatches[0].end,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (smartLineMatches.length > 1) {
|
|
347
|
+
throw new EditApplyError(total === 1
|
|
348
|
+
? `Error: oldText matched ${smartLineMatches.length} indentation-normalized line blocks in file. Provide more surrounding context.`
|
|
349
|
+
: `Error: edits[${index}].oldText matched ${smartLineMatches.length} indentation-normalized line blocks in file. Provide more surrounding context.`);
|
|
350
|
+
}
|
|
161
351
|
}
|
|
162
352
|
const hint = findBestLineHint(content, oldText);
|
|
163
353
|
const hintSuffix = hint ? `\n${hint}` : "";
|
|
@@ -166,7 +356,7 @@ function matchEdit(content, edit, index, total) {
|
|
|
166
356
|
"How to recover:",
|
|
167
357
|
"- Re-read the file with the read tool to see its current bytes; the file may have been changed by a prior edit this turn.",
|
|
168
358
|
"- Shorten oldText to a smaller unique anchor and try again. Long multi-line anchors are fragile to whitespace and indentation.",
|
|
169
|
-
"- If many lines need to change, use the write tool with
|
|
359
|
+
"- If many lines need to change, use the write tool with the full new content instead of stacking edits.",
|
|
170
360
|
].join("\n");
|
|
171
361
|
throw new EditApplyError(total === 1
|
|
172
362
|
? `Error: oldText not found in file: "${summarizeOldText(oldText)}"${hintSuffix}${recovery}`
|
|
@@ -182,7 +372,7 @@ function assertNoOverlaps(matches) {
|
|
|
182
372
|
}
|
|
183
373
|
}
|
|
184
374
|
}
|
|
185
|
-
export function applyEditsToContent(rawContent, edits) {
|
|
375
|
+
export function applyEditsToContent(rawContent, edits, options) {
|
|
186
376
|
if (!Array.isArray(edits) || edits.length === 0) {
|
|
187
377
|
throw new EditApplyError("Error: No edits provided");
|
|
188
378
|
}
|
|
@@ -193,7 +383,7 @@ export function applyEditsToContent(rawContent, edits) {
|
|
|
193
383
|
oldText: normalizeToLF(edit.oldText),
|
|
194
384
|
newText: normalizeToLF(edit.newText),
|
|
195
385
|
}));
|
|
196
|
-
const matches = normalizedEdits.map((edit, index) => matchEdit(normalizedOriginal, edit, index, normalizedEdits.length));
|
|
386
|
+
const matches = normalizedEdits.map((edit, index) => matchEdit(normalizedOriginal, edit, index, normalizedEdits.length, options));
|
|
197
387
|
assertNoOverlaps(matches);
|
|
198
388
|
const byDescendingStart = [...matches].sort((a, b) => b.start - a.start);
|
|
199
389
|
let normalizedNext = normalizedOriginal;
|
|
@@ -208,7 +398,7 @@ export function applyEditsToContent(rawContent, edits) {
|
|
|
208
398
|
"Common causes and how to escape:",
|
|
209
399
|
"- oldText and newText are byte-identical. Verify newText actually contains the intended change (a missing trailing char like turning '#ec489' into '#ec4899' is a frequent culprit).",
|
|
210
400
|
"- The file already contains newText. Re-read the file to confirm the current state before editing again.",
|
|
211
|
-
"- For wholesale rewrites, use the write tool with
|
|
401
|
+
"- For wholesale rewrites, use the write tool with the full new content instead.",
|
|
212
402
|
].join("\n"));
|
|
213
403
|
}
|
|
214
404
|
return {
|
package/dist/tools/edit.js
CHANGED
|
@@ -18,7 +18,7 @@ export function createEditTool(cwd, approval, lsp, fileState) {
|
|
|
18
18
|
name: "edit",
|
|
19
19
|
effect: "write_direct",
|
|
20
20
|
requiresApproval: true,
|
|
21
|
-
description: "Apply targeted string replacements to a file. Prefer exact oldText. The tool can tolerate line ending, trailing whitespace, Unicode punctuation/space, and blank-line differences
|
|
21
|
+
description: "Apply targeted string replacements to a file. Prefer exact oldText copied from a recent read. The tool can tolerate common AI formatting mistakes such as extra leading/trailing whitespace, over-escaped sequences, line ending differences, indentation differences, trailing whitespace, Unicode punctuation/space, and blank-line differences when the target is unique.",
|
|
22
22
|
parameters: {
|
|
23
23
|
type: "object",
|
|
24
24
|
properties: {
|
|
@@ -62,11 +62,20 @@ export function createEditTool(cwd, approval, lsp, fileState) {
|
|
|
62
62
|
const original = await readFile(filePath, "utf-8");
|
|
63
63
|
let applied;
|
|
64
64
|
try {
|
|
65
|
-
applied = applyEditsToContent(original, args.edits);
|
|
65
|
+
applied = applyEditsToContent(original, args.edits, { path: filePath });
|
|
66
66
|
}
|
|
67
67
|
catch (err) {
|
|
68
68
|
if (err instanceof EditApplyError) {
|
|
69
|
-
return {
|
|
69
|
+
return {
|
|
70
|
+
content: err.message,
|
|
71
|
+
isError: true,
|
|
72
|
+
status: err.status,
|
|
73
|
+
metadata: {
|
|
74
|
+
kind: "edit",
|
|
75
|
+
path: filePath,
|
|
76
|
+
reason: err.status,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
70
79
|
}
|
|
71
80
|
throw err;
|
|
72
81
|
}
|
|
@@ -11,7 +11,18 @@ function queueKey(filePath) {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
export async function withFileMutationQueue(filePath, fn) {
|
|
14
|
-
|
|
14
|
+
return withQueueKey(queueKey(filePath), fn);
|
|
15
|
+
}
|
|
16
|
+
export async function withFileMutationQueues(filePaths, fn) {
|
|
17
|
+
const keys = [...new Set(filePaths.map(queueKey))].sort();
|
|
18
|
+
const run = (index) => {
|
|
19
|
+
if (index >= keys.length)
|
|
20
|
+
return fn();
|
|
21
|
+
return withQueueKey(keys[index], () => run(index + 1));
|
|
22
|
+
};
|
|
23
|
+
return run(0);
|
|
24
|
+
}
|
|
25
|
+
async function withQueueKey(key, fn) {
|
|
15
26
|
const current = queues.get(key) ?? Promise.resolve();
|
|
16
27
|
let release;
|
|
17
28
|
const next = new Promise((resolveNext) => {
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { createReadTool } from "./read.js";
|
|
5
5
|
export { createBashTool } from "./bash.js";
|
|
6
|
+
export { createManagedServerTools } from "./server.js";
|
|
6
7
|
export { createWriteTool } from "./write.js";
|
|
7
8
|
export { createEditTool } from "./edit.js";
|
|
9
|
+
export { createApplyPatchTool } from "./apply-patch.js";
|
|
8
10
|
export { createGlobTool } from "./glob.js";
|
|
9
11
|
export { createGrepTool } from "./grep.js";
|
|
10
12
|
export { createLspTool } from "./lsp.js";
|
package/dist/tools/index.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { createReadTool } from "./read.js";
|
|
5
5
|
export { createBashTool } from "./bash.js";
|
|
6
|
+
export { createManagedServerTools } from "./server.js";
|
|
6
7
|
export { createWriteTool } from "./write.js";
|
|
7
8
|
export { createEditTool } from "./edit.js";
|
|
9
|
+
export { createApplyPatchTool } from "./apply-patch.js";
|
|
8
10
|
export { createGlobTool } from "./glob.js";
|
|
9
11
|
export { createGrepTool } from "./grep.js";
|
|
10
12
|
export { createLspTool } from "./lsp.js";
|
|
@@ -19,7 +21,9 @@ export { createToolSearchTool } from "./tool-search.js";
|
|
|
19
21
|
export { createQuestionTool } from "./question.js";
|
|
20
22
|
export { createMemoryReadSummaryTool, createMemorySearchTool } from "./memory.js";
|
|
21
23
|
import { createBashTool } from "./bash.js";
|
|
24
|
+
import { createManagedServerTools } from "./server.js";
|
|
22
25
|
import { createEditTool } from "./edit.js";
|
|
26
|
+
import { createApplyPatchTool } from "./apply-patch.js";
|
|
23
27
|
import { createExitPlanModeTool } from "./exit-plan-mode.js";
|
|
24
28
|
import { createGlobTool } from "./glob.js";
|
|
25
29
|
import { createGrepTool } from "./grep.js";
|
|
@@ -44,8 +48,10 @@ export function createAllTools(cwd, skillRegistry, options = {}) {
|
|
|
44
48
|
return [
|
|
45
49
|
createReadTool(cwd, approval, lsp, fileState),
|
|
46
50
|
createBashTool(cwd, approval, fileState),
|
|
47
|
-
|
|
51
|
+
...createManagedServerTools(cwd, approval),
|
|
52
|
+
createWriteTool(cwd, {}, approval, lsp, fileState),
|
|
48
53
|
createEditTool(cwd, approval, lsp, fileState),
|
|
54
|
+
createApplyPatchTool(cwd, approval, lsp, fileState),
|
|
49
55
|
createGlobTool(cwd),
|
|
50
56
|
createGrepTool(cwd),
|
|
51
57
|
createLspTool(cwd, lsp, approval),
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type PatchFileOperation = {
|
|
2
|
+
type: "add";
|
|
3
|
+
path: string;
|
|
4
|
+
lines: string[];
|
|
5
|
+
} | {
|
|
6
|
+
type: "delete";
|
|
7
|
+
path: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: "update";
|
|
10
|
+
path: string;
|
|
11
|
+
movePath?: string;
|
|
12
|
+
chunks: PatchChunk[];
|
|
13
|
+
};
|
|
14
|
+
export interface PatchChunk {
|
|
15
|
+
header: string;
|
|
16
|
+
lines: PatchLine[];
|
|
17
|
+
}
|
|
18
|
+
export type PatchLine = {
|
|
19
|
+
kind: "context";
|
|
20
|
+
text: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "remove";
|
|
23
|
+
text: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "add";
|
|
26
|
+
text: string;
|
|
27
|
+
};
|
|
28
|
+
export interface ParsedApplyPatch {
|
|
29
|
+
operations: PatchFileOperation[];
|
|
30
|
+
}
|
|
31
|
+
export interface PatchedContentResult {
|
|
32
|
+
content: string;
|
|
33
|
+
usedFallback: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare class PatchApplyError extends Error {
|
|
36
|
+
readonly status: "no_match" | "blocked";
|
|
37
|
+
constructor(message: string, status?: "no_match" | "blocked");
|
|
38
|
+
}
|
|
39
|
+
export declare function parseApplyPatch(patchText: string): ParsedApplyPatch;
|
|
40
|
+
export declare function buildAddedFileContent(lines: string[]): string;
|
|
41
|
+
export declare function applyPatchChunks(rawContent: string, chunks: PatchChunk[], path: string): PatchedContentResult;
|