@khanhicetea/pi-better-tool 0.1.0 → 0.2.2
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/README.md +69 -45
- package/package.json +1 -1
- package/src/apply.ts +53 -18
- package/src/diagnostics.ts +239 -39
- package/src/index.ts +17 -7
- package/src/read-evidence.ts +174 -0
- package/src/similarity.ts +210 -156
- package/src/text.ts +36 -7
- package/src/tool.ts +265 -83
package/src/tool.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The "better edit" tool: a drop-in override of pi's built-in edit tool.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Normal matching closely tracks the built-in tool and keeps its success
|
|
5
|
+
* result shape so the built-in diff renderer is inherited. Conservative
|
|
6
|
+
* schema, encoding, read-evidence, and commit-boundary checks are intentional
|
|
7
|
+
* safety differences. Instead of a bare "Could
|
|
7
8
|
* not find edits[1]" error that forces the model to re-read the file and
|
|
8
9
|
* guess at larger context, failures include recovery context:
|
|
9
10
|
*
|
|
10
|
-
* - ambiguous oldText →
|
|
11
|
-
*
|
|
11
|
+
* - ambiguous literal oldText → if the latest verified read of the file shows
|
|
12
|
+
* exactly one occurrence, select it and report up to four retryable remaining
|
|
13
|
+
* candidates; otherwise return occurrence line numbers + minimal context
|
|
12
14
|
* - not-found oldText → closest matching region with a per-line comparison
|
|
13
15
|
* and the exact file bytes to retry with
|
|
14
16
|
*/
|
|
@@ -21,14 +23,34 @@ import {
|
|
|
21
23
|
type ExtensionAPI,
|
|
22
24
|
type ExtensionContext,
|
|
23
25
|
} from "@earendil-works/pi-coding-agent";
|
|
24
|
-
import { constants } from "node:fs";
|
|
25
|
-
import {
|
|
26
|
+
import { constants, type Stats } from "node:fs";
|
|
27
|
+
import {
|
|
28
|
+
access as fsAccess,
|
|
29
|
+
readFile as fsReadFile,
|
|
30
|
+
realpath as fsRealpath,
|
|
31
|
+
stat as fsStat,
|
|
32
|
+
writeFile as fsWriteFile,
|
|
33
|
+
} from "node:fs/promises";
|
|
26
34
|
import { homedir } from "node:os";
|
|
27
|
-
import { resolve } from "node:path";
|
|
35
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
36
|
+
import { fileURLToPath } from "node:url";
|
|
28
37
|
import { type Static, Type } from "typebox";
|
|
29
|
-
import { analyzeEdits, applyAnalysis, type EditOp } from "./apply.ts";
|
|
30
|
-
import {
|
|
31
|
-
|
|
38
|
+
import { analyzeEdits, applyAnalysis, fuzzyFindText, normalizeEdits, type EditOp } from "./apply.ts";
|
|
39
|
+
import {
|
|
40
|
+
formatAutoDisambiguationSuccess,
|
|
41
|
+
formatEditFailure,
|
|
42
|
+
type AutoDisambiguation,
|
|
43
|
+
} from "./diagnostics.ts";
|
|
44
|
+
import { findLatestReadEvidence } from "./read-evidence.ts";
|
|
45
|
+
import {
|
|
46
|
+
detectLineEnding,
|
|
47
|
+
findAllOccurrences,
|
|
48
|
+
getLineSpans,
|
|
49
|
+
lineAt,
|
|
50
|
+
normalizeToLF,
|
|
51
|
+
restoreLineEndings,
|
|
52
|
+
splitBom,
|
|
53
|
+
} from "./text.ts";
|
|
32
54
|
|
|
33
55
|
const replaceEditSchema = Type.Object({
|
|
34
56
|
oldText: Type.String({
|
|
@@ -39,8 +61,10 @@ const replaceEditSchema = Type.Object({
|
|
|
39
61
|
});
|
|
40
62
|
|
|
41
63
|
export const betterEditSchema = Type.Object({
|
|
42
|
-
path: Type.String({ description: "Path to the file to edit (relative or absolute)" }),
|
|
64
|
+
path: Type.String({ minLength: 1, description: "Path to the file to edit (relative or absolute)" }),
|
|
43
65
|
edits: Type.Array(replaceEditSchema, {
|
|
66
|
+
minItems: 1,
|
|
67
|
+
maxItems: 100,
|
|
44
68
|
description:
|
|
45
69
|
"One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead.",
|
|
46
70
|
}),
|
|
@@ -48,14 +72,29 @@ export const betterEditSchema = Type.Object({
|
|
|
48
72
|
|
|
49
73
|
export type BetterEditInput = Static<typeof betterEditSchema>;
|
|
50
74
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
76
|
+
|
|
77
|
+
/** Match pi's built-in path normalization for tool arguments. */
|
|
78
|
+
function normalizeToolPath(input: string): string {
|
|
79
|
+
let path = input.replace(UNICODE_SPACES, " ");
|
|
80
|
+
if (path.startsWith("@")) path = path.slice(1);
|
|
81
|
+
|
|
82
|
+
if (process.platform === "win32" && path.startsWith("/") && !path.startsWith("//") && !path.includes("\\")) {
|
|
83
|
+
const match = path.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
|
|
84
|
+
if (match) path = `${match[1].toUpperCase()}:\\${match[2]?.replaceAll("/", "\\") ?? ""}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (path === "~") return homedir();
|
|
88
|
+
if (path.startsWith("~/") || (process.platform === "win32" && path.startsWith("~\\"))) {
|
|
89
|
+
return join(homedir(), path.slice(2));
|
|
57
90
|
}
|
|
58
|
-
return
|
|
91
|
+
if (/^file:\/\//.test(path)) return fileURLToPath(path);
|
|
92
|
+
return path;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveToCwd(filePath: string, cwd: string): string {
|
|
96
|
+
const path = normalizeToolPath(filePath);
|
|
97
|
+
return isAbsolute(path) ? resolve(path) : resolve(cwd, path);
|
|
59
98
|
}
|
|
60
99
|
|
|
61
100
|
function isSingleEditInput(value: unknown): value is { oldText: string; newText: string } {
|
|
@@ -82,28 +121,26 @@ export function prepareEditArguments(input: unknown): unknown {
|
|
|
82
121
|
[path: string]: unknown;
|
|
83
122
|
};
|
|
84
123
|
|
|
85
|
-
|
|
124
|
+
let preparedEdits = args.edits;
|
|
125
|
+
if (typeof preparedEdits === "string") {
|
|
86
126
|
try {
|
|
87
|
-
const parsed: unknown = JSON.parse(
|
|
88
|
-
if (Array.isArray(parsed))
|
|
89
|
-
|
|
90
|
-
} else if (isSingleEditInput(parsed)) {
|
|
91
|
-
args.edits = [parsed];
|
|
92
|
-
}
|
|
127
|
+
const parsed: unknown = JSON.parse(preparedEdits);
|
|
128
|
+
if (Array.isArray(parsed)) preparedEdits = parsed;
|
|
129
|
+
else if (isSingleEditInput(parsed)) preparedEdits = [parsed];
|
|
93
130
|
} catch {
|
|
94
131
|
// leave as-is; schema validation will report it
|
|
95
132
|
}
|
|
96
|
-
} else if (isSingleEditInput(
|
|
97
|
-
|
|
133
|
+
} else if (isSingleEditInput(preparedEdits)) {
|
|
134
|
+
preparedEdits = [preparedEdits];
|
|
98
135
|
}
|
|
99
136
|
|
|
100
137
|
if (typeof args.oldText === "string" && typeof args.newText === "string") {
|
|
101
|
-
const edits = Array.isArray(
|
|
138
|
+
const edits = Array.isArray(preparedEdits) ? [...(preparedEdits as EditOp[])] : [];
|
|
102
139
|
edits.push({ oldText: args.oldText, newText: args.newText });
|
|
103
140
|
const { oldText: _oldText, newText: _newText, ...rest } = args;
|
|
104
141
|
return { ...rest, edits };
|
|
105
142
|
}
|
|
106
|
-
return args;
|
|
143
|
+
return preparedEdits === args.edits ? args : { ...args, edits: preparedEdits };
|
|
107
144
|
}
|
|
108
145
|
|
|
109
146
|
export interface BetterEditSuccess {
|
|
@@ -111,111 +148,256 @@ export interface BetterEditSuccess {
|
|
|
111
148
|
details: EditToolDetails;
|
|
112
149
|
}
|
|
113
150
|
|
|
151
|
+
export interface BetterEditOperations {
|
|
152
|
+
access(path: string): Promise<void>;
|
|
153
|
+
readFile(path: string): Promise<Buffer>;
|
|
154
|
+
realpath(path: string): Promise<string>;
|
|
155
|
+
stat(path: string): Promise<Stats>;
|
|
156
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const defaultOperations: BetterEditOperations = {
|
|
160
|
+
access: (path) => fsAccess(path, constants.R_OK | constants.W_OK),
|
|
161
|
+
readFile: (path) => fsReadFile(path),
|
|
162
|
+
realpath: (path) => fsRealpath(path),
|
|
163
|
+
stat: (path) => fsStat(path),
|
|
164
|
+
writeFile: (path, content) => fsWriteFile(path, content, "utf-8"),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export interface BetterEditExecutionOptions {
|
|
168
|
+
operations?: BetterEditOperations;
|
|
169
|
+
/** Test/integration seam; runs before the write commit boundary. */
|
|
170
|
+
formatSuccess?: typeof formatAutoDisambiguationSuccess;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function describeFsError(error: unknown): string {
|
|
174
|
+
return error instanceof Error && "code" in error
|
|
175
|
+
? `Error code: ${(error as NodeJS.ErrnoException).code}`
|
|
176
|
+
: error instanceof Error
|
|
177
|
+
? error.message
|
|
178
|
+
: String(error);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function sameFileIdentity(a: Stats, b: Stats): boolean {
|
|
182
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function decodeEditableUtf8(buffer: Buffer, path: string): string {
|
|
186
|
+
try {
|
|
187
|
+
new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
188
|
+
} catch {
|
|
189
|
+
throw new Error(`Could not edit file: ${path}. The file is not valid UTF-8; unsupported encodings are never transcoded.`);
|
|
190
|
+
}
|
|
191
|
+
if (buffer.includes(0)) {
|
|
192
|
+
throw new Error(`Could not edit file: ${path}. The file contains NUL bytes and is treated as binary or an unsupported text encoding.`);
|
|
193
|
+
}
|
|
194
|
+
return buffer.toString("utf-8");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function countLiteralOccurrences(haystack: string, needle: string): number {
|
|
198
|
+
if (!needle) return 0;
|
|
199
|
+
let count = 0;
|
|
200
|
+
let index = haystack.indexOf(needle);
|
|
201
|
+
while (index !== -1) {
|
|
202
|
+
count++;
|
|
203
|
+
index = haystack.indexOf(needle, index + needle.length);
|
|
204
|
+
}
|
|
205
|
+
return count;
|
|
206
|
+
}
|
|
207
|
+
|
|
114
208
|
export async function executeBetterEdit(
|
|
115
209
|
input: BetterEditInput,
|
|
116
210
|
signal: AbortSignal | undefined,
|
|
117
|
-
ctx: Pick<ExtensionContext, "cwd"
|
|
211
|
+
ctx: Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "sessionManager">>,
|
|
212
|
+
options: BetterEditExecutionOptions = {},
|
|
118
213
|
): Promise<BetterEditSuccess> {
|
|
119
214
|
const edits: EditOp[] = input.edits ?? [];
|
|
120
|
-
if (!Array.isArray(edits) || edits.length === 0) {
|
|
121
|
-
throw new Error("Edit tool input is invalid. edits must contain
|
|
215
|
+
if (!input.path || !Array.isArray(edits) || edits.length === 0 || edits.length > 100) {
|
|
216
|
+
throw new Error("Edit tool input is invalid. path must be non-empty and edits must contain 1-100 replacements.");
|
|
122
217
|
}
|
|
123
218
|
const absolutePath = resolveToCwd(input.path, ctx.cwd);
|
|
219
|
+
const ops = options.operations ?? defaultOperations;
|
|
220
|
+
const formatSuccess = options.formatSuccess ?? formatAutoDisambiguationSuccess;
|
|
124
221
|
|
|
125
222
|
return withFileMutationQueue(absolutePath, async () => {
|
|
126
|
-
//
|
|
127
|
-
//
|
|
223
|
+
// Never reject from an abort listener: the queue must remain held until
|
|
224
|
+
// in-flight filesystem work settles.
|
|
128
225
|
const throwIfAborted = () => {
|
|
129
|
-
if (signal?.aborted) throw new Error("Operation aborted");
|
|
226
|
+
if (signal?.aborted) throw new Error("Operation aborted before the edit was committed; no write was started.");
|
|
130
227
|
};
|
|
131
228
|
throwIfAborted();
|
|
132
229
|
|
|
133
230
|
try {
|
|
134
|
-
await
|
|
231
|
+
await ops.access(absolutePath);
|
|
135
232
|
} catch (error) {
|
|
136
233
|
throwIfAborted();
|
|
137
|
-
|
|
138
|
-
error instanceof Error && "code" in error
|
|
139
|
-
? `Error code: ${(error as NodeJS.ErrnoException).code}`
|
|
140
|
-
: String(error);
|
|
141
|
-
throw new Error(`Could not edit file: ${input.path}. ${errorMessage}.`);
|
|
234
|
+
throw new Error(`Could not access file for editing: ${input.path}. ${describeFsError(error)}. No write was started.`);
|
|
142
235
|
}
|
|
143
236
|
throwIfAborted();
|
|
144
237
|
|
|
145
|
-
|
|
146
|
-
|
|
238
|
+
let buffer: Buffer;
|
|
239
|
+
let initialStat: Stats;
|
|
240
|
+
try {
|
|
241
|
+
const beforeReadStat = await ops.stat(absolutePath);
|
|
242
|
+
buffer = await ops.readFile(absolutePath);
|
|
243
|
+
initialStat = await ops.stat(absolutePath);
|
|
244
|
+
if (!sameFileIdentity(beforeReadStat, initialStat)) throw new Error("target-changed-during-read");
|
|
245
|
+
} catch (error) {
|
|
246
|
+
throwIfAborted();
|
|
247
|
+
if (error instanceof Error && error.message === "target-changed-during-read") {
|
|
248
|
+
throw new Error(`Could not edit file: ${input.path}. The target identity changed while it was being read; no write was started. Re-read and retry.`);
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`Could not read file for editing: ${input.path}. ${describeFsError(error)}. No write was started.`);
|
|
251
|
+
}
|
|
252
|
+
const rawContent = decodeEditableUtf8(buffer, input.path);
|
|
147
253
|
throwIfAborted();
|
|
148
254
|
|
|
149
255
|
const { bom, text: content } = splitBom(rawContent);
|
|
150
256
|
const originalEnding = detectLineEnding(content);
|
|
151
257
|
const normalizedContent = normalizeToLF(content);
|
|
152
258
|
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
259
|
+
const selections = new Map<number, number>();
|
|
260
|
+
const resolutions: AutoDisambiguation[] = [];
|
|
261
|
+
let result = analyzeEdits(normalizedContent, edits, { ambiguousSelections: selections });
|
|
262
|
+
let readEvidence: Awaited<ReturnType<typeof findLatestReadEvidence>> | undefined;
|
|
263
|
+
const normalizedEdits = normalizeEdits(edits);
|
|
264
|
+
const exactBase = normalizedEdits.every(
|
|
265
|
+
(edit) => !fuzzyFindText(normalizedContent, edit.oldText).usedFuzzyMatch,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
while (!result.ok && result.failure.kind === "ambiguous" && exactBase) {
|
|
269
|
+
const editIndex = result.failure.editIndex;
|
|
270
|
+
if (selections.has(editIndex)) break;
|
|
271
|
+
const edit = normalizedEdits[editIndex];
|
|
272
|
+
const exactCount = countLiteralOccurrences(normalizedContent, edit.oldText);
|
|
273
|
+
const exactOffsets = findAllOccurrences(normalizedContent, edit.oldText, 256);
|
|
274
|
+
// Fuzzy-equivalent aliases cannot be mapped safely to original offsets.
|
|
275
|
+
if (exactCount !== result.failure.occurrenceCount) break;
|
|
276
|
+
|
|
277
|
+
if (readEvidence === undefined) {
|
|
278
|
+
let canonicalTarget: string;
|
|
279
|
+
try {
|
|
280
|
+
canonicalTarget = await ops.realpath(absolutePath);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
throw new Error(`Could not resolve edit target: ${input.path}. ${describeFsError(error)}. No write was started.`);
|
|
283
|
+
}
|
|
284
|
+
readEvidence = await findLatestReadEvidence(
|
|
285
|
+
ctx.sessionManager,
|
|
286
|
+
canonicalTarget,
|
|
158
287
|
normalizedContent,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
288
|
+
async (readPath) => ops.realpath(resolveToCwd(readPath, ctx.cwd)),
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (!readEvidence) break;
|
|
292
|
+
|
|
293
|
+
const candidates = exactOffsets.filter(
|
|
294
|
+
(offset) =>
|
|
295
|
+
offset >= readEvidence!.startOffset &&
|
|
296
|
+
offset + edit.oldText.length <= readEvidence!.endOffset,
|
|
162
297
|
);
|
|
298
|
+
if (candidates.length !== 1) break;
|
|
299
|
+
|
|
300
|
+
const selectedOffset = candidates[0];
|
|
301
|
+
const spans = getLineSpans(normalizedContent);
|
|
302
|
+
selections.set(editIndex, selectedOffset);
|
|
303
|
+
resolutions.push({
|
|
304
|
+
editIndex,
|
|
305
|
+
oldText: edit.oldText,
|
|
306
|
+
chosenRange: {
|
|
307
|
+
start: lineAt(spans, selectedOffset) + 1,
|
|
308
|
+
end: lineAt(spans, selectedOffset + Math.max(1, edit.oldText.length) - 1) + 1,
|
|
309
|
+
},
|
|
310
|
+
readRange: { start: readEvidence.startLine, end: readEvidence.endLine },
|
|
311
|
+
});
|
|
312
|
+
result = analyzeEdits(normalizedContent, edits, { ambiguousSelections: selections });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!result.ok) {
|
|
316
|
+
throw new Error(formatEditFailure({ path: input.path, normalizedContent, edits, failure: result.failure }));
|
|
163
317
|
}
|
|
164
318
|
|
|
165
319
|
const { baseContent, newContent } = applyAnalysis(normalizedContent, result.analysis);
|
|
166
320
|
if (baseContent === newContent) {
|
|
167
|
-
throw new Error(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}),
|
|
174
|
-
);
|
|
321
|
+
throw new Error(formatEditFailure({
|
|
322
|
+
path: input.path,
|
|
323
|
+
normalizedContent,
|
|
324
|
+
edits,
|
|
325
|
+
failure: { kind: "no-change" },
|
|
326
|
+
}));
|
|
175
327
|
}
|
|
176
|
-
throwIfAborted();
|
|
177
328
|
|
|
329
|
+
// Prepare every required success artifact before crossing the write
|
|
330
|
+
// boundary. Optional diagnostics are bounded and cannot fail after commit.
|
|
178
331
|
const finalContent = bom + restoreLineEndings(newContent, originalEnding);
|
|
179
|
-
await fsWriteFile(absolutePath, finalContent, "utf-8");
|
|
180
|
-
throwIfAborted();
|
|
181
|
-
|
|
182
332
|
const diffResult = generateDiffString(baseContent, newContent);
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
333
|
+
const success: BetterEditSuccess = {
|
|
334
|
+
content: [{
|
|
335
|
+
type: "text",
|
|
336
|
+
text: formatSuccess(
|
|
337
|
+
`Successfully replaced ${edits.length} block(s) in ${input.path}.`,
|
|
338
|
+
newContent,
|
|
339
|
+
resolutions,
|
|
340
|
+
),
|
|
341
|
+
}],
|
|
191
342
|
details: {
|
|
192
343
|
diff: diffResult.diff,
|
|
193
|
-
patch,
|
|
344
|
+
patch: generateUnifiedPatch(input.path, baseContent, newContent),
|
|
194
345
|
firstChangedLine: diffResult.firstChangedLine,
|
|
195
|
-
}
|
|
346
|
+
},
|
|
196
347
|
};
|
|
348
|
+
throwIfAborted();
|
|
349
|
+
|
|
350
|
+
// Best-effort external modification detection. This is not a lock or a
|
|
351
|
+
// race-free compare-and-swap; another process can still change the target
|
|
352
|
+
// after this check and before/during the write.
|
|
353
|
+
try {
|
|
354
|
+
const [currentBuffer, currentStat] = await Promise.all([
|
|
355
|
+
ops.readFile(absolutePath),
|
|
356
|
+
ops.stat(absolutePath),
|
|
357
|
+
]);
|
|
358
|
+
if (!sameFileIdentity(initialStat, currentStat) || !buffer.equals(currentBuffer)) {
|
|
359
|
+
throw new Error("target-changed");
|
|
360
|
+
}
|
|
361
|
+
} catch (error) {
|
|
362
|
+
throwIfAborted();
|
|
363
|
+
if (error instanceof Error && error.message === "target-changed") {
|
|
364
|
+
throw new Error(`Could not edit file: ${input.path}. The target changed after it was read; no write was started. Re-read and retry.`);
|
|
365
|
+
}
|
|
366
|
+
throw new Error(`Could not revalidate file before writing: ${input.path}. ${describeFsError(error)}. No write was started.`);
|
|
367
|
+
}
|
|
368
|
+
throwIfAborted();
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
await ops.writeFile(absolutePath, finalContent);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
throw new Error(`Could not complete write to ${input.path}. ${describeFsError(error)}. The file may be unchanged, partially written, or fully written; inspect it before retrying.`);
|
|
374
|
+
}
|
|
375
|
+
// A resolved write is the commit boundary. Ignore cancellation that arrived
|
|
376
|
+
// during it and report the committed result; no fallible formatting remains.
|
|
377
|
+
return success;
|
|
197
378
|
});
|
|
198
379
|
}
|
|
199
380
|
|
|
200
|
-
export function registerBetterEditTool(pi: ExtensionAPI): void {
|
|
381
|
+
export function registerBetterEditTool(pi: ExtensionAPI, options: BetterEditExecutionOptions = {}): void {
|
|
201
382
|
pi.registerTool({
|
|
202
383
|
name: "edit",
|
|
203
384
|
label: "edit",
|
|
204
385
|
description:
|
|
205
|
-
"Edit a single file using exact text replacement. Every edits[].oldText
|
|
386
|
+
"Edit a single local file using exact text replacement. Every edits[].oldText should match a unique, non-overlapping region of the original file. Repeated literal text is selected only when exactly one tracked occurrence is contained in the latest verified stored-context read. Failures include bounded recovery context; low-confidence, non-unique, omitted, or stale candidates require a read before retrying.",
|
|
206
387
|
promptSnippet:
|
|
207
|
-
"Make precise file edits with exact text replacement; failures return recovery context
|
|
388
|
+
"Make precise local file edits with exact text replacement; failures return bounded recovery context",
|
|
208
389
|
promptGuidelines: [
|
|
209
|
-
"Use edit for precise changes (edits[].oldText must match exactly)",
|
|
210
|
-
"When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls",
|
|
211
|
-
"
|
|
212
|
-
"Keep edits[].oldText as small as possible while still being unique in the file
|
|
213
|
-
"When
|
|
390
|
+
"Use edit for precise changes (edits[].oldText must match exactly).",
|
|
391
|
+
"When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls.",
|
|
392
|
+
"In edit, each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits; merge nearby changes into one edit.",
|
|
393
|
+
"Keep edit edits[].oldText as small as possible while still being unique in the file; do not pad with large unchanged regions.",
|
|
394
|
+
"When edit safely auto-disambiguates repeated text from the latest verified stored-context read, its success message lists bounded remaining candidates for an optional follow-up edit.",
|
|
395
|
+
"When edit fails, reuse only a fenced snippet explicitly marked retryable. If edit reports low confidence, competing candidates, omitted output, stale evidence, or a write that may have modified the file, read the referenced file/range before retrying.",
|
|
214
396
|
],
|
|
215
397
|
parameters: betterEditSchema,
|
|
216
398
|
prepareArguments: (args: unknown): BetterEditInput => prepareEditArguments(args) as BetterEditInput,
|
|
217
399
|
async execute(_toolCallId, input, signal, _onUpdate, ctx) {
|
|
218
|
-
return executeBetterEdit(input, signal, ctx);
|
|
400
|
+
return executeBetterEdit(input, signal, ctx, options);
|
|
219
401
|
},
|
|
220
402
|
});
|
|
221
403
|
}
|