@khanhicetea/pi-better-tool 0.2.1 → 0.2.3
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 +116 -69
- package/package.json +34 -5
- package/src/apply.ts +22 -12
- package/src/diagnostics.ts +159 -26
- package/src/index.ts +15 -5
- package/src/paths.ts +19 -0
- package/src/read-evidence.ts +74 -33
- package/src/read-symbol.ts +214 -0
- package/src/similarity.ts +140 -63
- package/src/source-file.ts +60 -0
- package/src/symbols.ts +228 -0
- package/src/text.ts +36 -7
- package/src/tool.ts +178 -106
package/src/tool.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
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
|
*
|
|
@@ -22,11 +23,15 @@ import {
|
|
|
22
23
|
type ExtensionAPI,
|
|
23
24
|
type ExtensionContext,
|
|
24
25
|
} from "@earendil-works/pi-coding-agent";
|
|
25
|
-
import { constants } from "node:fs";
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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";
|
|
34
|
+
import { resolveToolPath as resolveToCwd } from "./paths.ts";
|
|
30
35
|
import { type Static, Type } from "typebox";
|
|
31
36
|
import { analyzeEdits, applyAnalysis, fuzzyFindText, normalizeEdits, type EditOp } from "./apply.ts";
|
|
32
37
|
import {
|
|
@@ -54,8 +59,10 @@ const replaceEditSchema = Type.Object({
|
|
|
54
59
|
});
|
|
55
60
|
|
|
56
61
|
export const betterEditSchema = Type.Object({
|
|
57
|
-
path: Type.String({ description: "Path to the file to edit (relative or absolute)" }),
|
|
62
|
+
path: Type.String({ minLength: 1, description: "Path to the file to edit (relative or absolute)" }),
|
|
58
63
|
edits: Type.Array(replaceEditSchema, {
|
|
64
|
+
minItems: 1,
|
|
65
|
+
maxItems: 100,
|
|
59
66
|
description:
|
|
60
67
|
"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.",
|
|
61
68
|
}),
|
|
@@ -63,31 +70,6 @@ export const betterEditSchema = Type.Object({
|
|
|
63
70
|
|
|
64
71
|
export type BetterEditInput = Static<typeof betterEditSchema>;
|
|
65
72
|
|
|
66
|
-
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
67
|
-
|
|
68
|
-
/** Match pi's built-in path normalization for tool arguments. */
|
|
69
|
-
function normalizeToolPath(input: string): string {
|
|
70
|
-
let path = input.replace(UNICODE_SPACES, " ");
|
|
71
|
-
if (path.startsWith("@")) path = path.slice(1);
|
|
72
|
-
|
|
73
|
-
if (process.platform === "win32" && path.startsWith("/") && !path.startsWith("//") && !path.includes("\\")) {
|
|
74
|
-
const match = path.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
|
|
75
|
-
if (match) path = `${match[1].toUpperCase()}:\\${match[2]?.replaceAll("/", "\\") ?? ""}`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (path === "~") return homedir();
|
|
79
|
-
if (path.startsWith("~/") || (process.platform === "win32" && path.startsWith("~\\"))) {
|
|
80
|
-
return join(homedir(), path.slice(2));
|
|
81
|
-
}
|
|
82
|
-
if (/^file:\/\//.test(path)) return fileURLToPath(path);
|
|
83
|
-
return path;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function resolveToCwd(filePath: string, cwd: string): string {
|
|
87
|
-
const path = normalizeToolPath(filePath);
|
|
88
|
-
return isAbsolute(path) ? resolve(path) : resolve(cwd, path);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
73
|
function isSingleEditInput(value: unknown): value is { oldText: string; newText: string } {
|
|
92
74
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
93
75
|
return false;
|
|
@@ -112,28 +94,26 @@ export function prepareEditArguments(input: unknown): unknown {
|
|
|
112
94
|
[path: string]: unknown;
|
|
113
95
|
};
|
|
114
96
|
|
|
115
|
-
|
|
97
|
+
let preparedEdits = args.edits;
|
|
98
|
+
if (typeof preparedEdits === "string") {
|
|
116
99
|
try {
|
|
117
|
-
const parsed: unknown = JSON.parse(
|
|
118
|
-
if (Array.isArray(parsed))
|
|
119
|
-
|
|
120
|
-
} else if (isSingleEditInput(parsed)) {
|
|
121
|
-
args.edits = [parsed];
|
|
122
|
-
}
|
|
100
|
+
const parsed: unknown = JSON.parse(preparedEdits);
|
|
101
|
+
if (Array.isArray(parsed)) preparedEdits = parsed;
|
|
102
|
+
else if (isSingleEditInput(parsed)) preparedEdits = [parsed];
|
|
123
103
|
} catch {
|
|
124
104
|
// leave as-is; schema validation will report it
|
|
125
105
|
}
|
|
126
|
-
} else if (isSingleEditInput(
|
|
127
|
-
|
|
106
|
+
} else if (isSingleEditInput(preparedEdits)) {
|
|
107
|
+
preparedEdits = [preparedEdits];
|
|
128
108
|
}
|
|
129
109
|
|
|
130
110
|
if (typeof args.oldText === "string" && typeof args.newText === "string") {
|
|
131
|
-
const edits = Array.isArray(
|
|
111
|
+
const edits = Array.isArray(preparedEdits) ? [...(preparedEdits as EditOp[])] : [];
|
|
132
112
|
edits.push({ oldText: args.oldText, newText: args.newText });
|
|
133
113
|
const { oldText: _oldText, newText: _newText, ...rest } = args;
|
|
134
114
|
return { ...rest, edits };
|
|
135
115
|
}
|
|
136
|
-
return args;
|
|
116
|
+
return preparedEdits === args.edits ? args : { ...args, edits: preparedEdits };
|
|
137
117
|
}
|
|
138
118
|
|
|
139
119
|
export interface BetterEditSuccess {
|
|
@@ -141,39 +121,108 @@ export interface BetterEditSuccess {
|
|
|
141
121
|
details: EditToolDetails;
|
|
142
122
|
}
|
|
143
123
|
|
|
124
|
+
export interface BetterEditOperations {
|
|
125
|
+
access(path: string): Promise<void>;
|
|
126
|
+
readFile(path: string): Promise<Buffer>;
|
|
127
|
+
realpath(path: string): Promise<string>;
|
|
128
|
+
stat(path: string): Promise<Stats>;
|
|
129
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const defaultOperations: BetterEditOperations = {
|
|
133
|
+
access: (path) => fsAccess(path, constants.R_OK | constants.W_OK),
|
|
134
|
+
readFile: (path) => fsReadFile(path),
|
|
135
|
+
realpath: (path) => fsRealpath(path),
|
|
136
|
+
stat: (path) => fsStat(path),
|
|
137
|
+
writeFile: (path, content) => fsWriteFile(path, content, "utf-8"),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export interface BetterEditExecutionOptions {
|
|
141
|
+
operations?: BetterEditOperations;
|
|
142
|
+
/** Test/integration seam; runs before the write commit boundary. */
|
|
143
|
+
formatSuccess?: typeof formatAutoDisambiguationSuccess;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function describeFsError(error: unknown): string {
|
|
147
|
+
return error instanceof Error && "code" in error
|
|
148
|
+
? `Error code: ${(error as NodeJS.ErrnoException).code}`
|
|
149
|
+
: error instanceof Error
|
|
150
|
+
? error.message
|
|
151
|
+
: String(error);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function sameFileIdentity(a: Stats, b: Stats): boolean {
|
|
155
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function decodeEditableUtf8(buffer: Buffer, path: string): string {
|
|
159
|
+
try {
|
|
160
|
+
new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
161
|
+
} catch {
|
|
162
|
+
throw new Error(`Could not edit file: ${path}. The file is not valid UTF-8; unsupported encodings are never transcoded.`);
|
|
163
|
+
}
|
|
164
|
+
if (buffer.includes(0)) {
|
|
165
|
+
throw new Error(`Could not edit file: ${path}. The file contains NUL bytes and is treated as binary or an unsupported text encoding.`);
|
|
166
|
+
}
|
|
167
|
+
return buffer.toString("utf-8");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function countLiteralOccurrences(haystack: string, needle: string): number {
|
|
171
|
+
if (!needle) return 0;
|
|
172
|
+
let count = 0;
|
|
173
|
+
let index = haystack.indexOf(needle);
|
|
174
|
+
while (index !== -1) {
|
|
175
|
+
count++;
|
|
176
|
+
index = haystack.indexOf(needle, index + needle.length);
|
|
177
|
+
}
|
|
178
|
+
return count;
|
|
179
|
+
}
|
|
180
|
+
|
|
144
181
|
export async function executeBetterEdit(
|
|
145
182
|
input: BetterEditInput,
|
|
146
183
|
signal: AbortSignal | undefined,
|
|
147
184
|
ctx: Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "sessionManager">>,
|
|
185
|
+
options: BetterEditExecutionOptions = {},
|
|
148
186
|
): Promise<BetterEditSuccess> {
|
|
149
187
|
const edits: EditOp[] = input.edits ?? [];
|
|
150
|
-
if (!Array.isArray(edits) || edits.length === 0) {
|
|
151
|
-
throw new Error("Edit tool input is invalid. edits must contain
|
|
188
|
+
if (!input.path || !Array.isArray(edits) || edits.length === 0 || edits.length > 100) {
|
|
189
|
+
throw new Error("Edit tool input is invalid. path must be non-empty and edits must contain 1-100 replacements.");
|
|
152
190
|
}
|
|
153
191
|
const absolutePath = resolveToCwd(input.path, ctx.cwd);
|
|
192
|
+
const ops = options.operations ?? defaultOperations;
|
|
193
|
+
const formatSuccess = options.formatSuccess ?? formatAutoDisambiguationSuccess;
|
|
154
194
|
|
|
155
195
|
return withFileMutationQueue(absolutePath, async () => {
|
|
156
|
-
//
|
|
157
|
-
//
|
|
196
|
+
// Never reject from an abort listener: the queue must remain held until
|
|
197
|
+
// in-flight filesystem work settles.
|
|
158
198
|
const throwIfAborted = () => {
|
|
159
|
-
if (signal?.aborted) throw new Error("Operation aborted");
|
|
199
|
+
if (signal?.aborted) throw new Error("Operation aborted before the edit was committed; no write was started.");
|
|
160
200
|
};
|
|
161
201
|
throwIfAborted();
|
|
162
202
|
|
|
163
203
|
try {
|
|
164
|
-
await
|
|
204
|
+
await ops.access(absolutePath);
|
|
165
205
|
} catch (error) {
|
|
166
206
|
throwIfAborted();
|
|
167
|
-
|
|
168
|
-
error instanceof Error && "code" in error
|
|
169
|
-
? `Error code: ${(error as NodeJS.ErrnoException).code}`
|
|
170
|
-
: String(error);
|
|
171
|
-
throw new Error(`Could not edit file: ${input.path}. ${errorMessage}.`);
|
|
207
|
+
throw new Error(`Could not access file for editing: ${input.path}. ${describeFsError(error)}. No write was started.`);
|
|
172
208
|
}
|
|
173
209
|
throwIfAborted();
|
|
174
210
|
|
|
175
|
-
|
|
176
|
-
|
|
211
|
+
let buffer: Buffer;
|
|
212
|
+
let initialStat: Stats;
|
|
213
|
+
try {
|
|
214
|
+
const beforeReadStat = await ops.stat(absolutePath);
|
|
215
|
+
buffer = await ops.readFile(absolutePath);
|
|
216
|
+
initialStat = await ops.stat(absolutePath);
|
|
217
|
+
if (!sameFileIdentity(beforeReadStat, initialStat)) throw new Error("target-changed-during-read");
|
|
218
|
+
} catch (error) {
|
|
219
|
+
throwIfAborted();
|
|
220
|
+
if (error instanceof Error && error.message === "target-changed-during-read") {
|
|
221
|
+
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.`);
|
|
222
|
+
}
|
|
223
|
+
throw new Error(`Could not read file for editing: ${input.path}. ${describeFsError(error)}. No write was started.`);
|
|
224
|
+
}
|
|
225
|
+
const rawContent = decodeEditableUtf8(buffer, input.path);
|
|
177
226
|
throwIfAborted();
|
|
178
227
|
|
|
179
228
|
const { bom, text: content } = splitBom(rawContent);
|
|
@@ -193,17 +242,23 @@ export async function executeBetterEdit(
|
|
|
193
242
|
const editIndex = result.failure.editIndex;
|
|
194
243
|
if (selections.has(editIndex)) break;
|
|
195
244
|
const edit = normalizedEdits[editIndex];
|
|
196
|
-
const
|
|
245
|
+
const exactCount = countLiteralOccurrences(normalizedContent, edit.oldText);
|
|
246
|
+
const exactOffsets = findAllOccurrences(normalizedContent, edit.oldText, 256);
|
|
197
247
|
// Fuzzy-equivalent aliases cannot be mapped safely to original offsets.
|
|
198
|
-
if (
|
|
248
|
+
if (exactCount !== result.failure.occurrenceCount) break;
|
|
199
249
|
|
|
200
250
|
if (readEvidence === undefined) {
|
|
201
|
-
|
|
251
|
+
let canonicalTarget: string;
|
|
252
|
+
try {
|
|
253
|
+
canonicalTarget = await ops.realpath(absolutePath);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
throw new Error(`Could not resolve edit target: ${input.path}. ${describeFsError(error)}. No write was started.`);
|
|
256
|
+
}
|
|
202
257
|
readEvidence = await findLatestReadEvidence(
|
|
203
258
|
ctx.sessionManager,
|
|
204
259
|
canonicalTarget,
|
|
205
260
|
normalizedContent,
|
|
206
|
-
async (readPath) =>
|
|
261
|
+
async (readPath) => ops.realpath(resolveToCwd(readPath, ctx.cwd)),
|
|
207
262
|
);
|
|
208
263
|
}
|
|
209
264
|
if (!readEvidence) break;
|
|
@@ -231,75 +286,92 @@ export async function executeBetterEdit(
|
|
|
231
286
|
}
|
|
232
287
|
|
|
233
288
|
if (!result.ok) {
|
|
234
|
-
throw new Error(
|
|
235
|
-
formatEditFailure({
|
|
236
|
-
path: input.path,
|
|
237
|
-
normalizedContent,
|
|
238
|
-
edits,
|
|
239
|
-
failure: result.failure,
|
|
240
|
-
}),
|
|
241
|
-
);
|
|
289
|
+
throw new Error(formatEditFailure({ path: input.path, normalizedContent, edits, failure: result.failure }));
|
|
242
290
|
}
|
|
243
291
|
|
|
244
292
|
const { baseContent, newContent } = applyAnalysis(normalizedContent, result.analysis);
|
|
245
293
|
if (baseContent === newContent) {
|
|
246
|
-
throw new Error(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}),
|
|
253
|
-
);
|
|
294
|
+
throw new Error(formatEditFailure({
|
|
295
|
+
path: input.path,
|
|
296
|
+
normalizedContent,
|
|
297
|
+
edits,
|
|
298
|
+
failure: { kind: "no-change" },
|
|
299
|
+
}));
|
|
254
300
|
}
|
|
255
|
-
throwIfAborted();
|
|
256
301
|
|
|
302
|
+
// Prepare every required success artifact before crossing the write
|
|
303
|
+
// boundary. Optional diagnostics are bounded and cannot fail after commit.
|
|
257
304
|
const finalContent = bom + restoreLineEndings(newContent, originalEnding);
|
|
258
|
-
await fsWriteFile(absolutePath, finalContent, "utf-8");
|
|
259
|
-
throwIfAborted();
|
|
260
|
-
|
|
261
305
|
const diffResult = generateDiffString(baseContent, newContent);
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
),
|
|
272
|
-
},
|
|
273
|
-
],
|
|
306
|
+
const success: BetterEditSuccess = {
|
|
307
|
+
content: [{
|
|
308
|
+
type: "text",
|
|
309
|
+
text: formatSuccess(
|
|
310
|
+
`Successfully replaced ${edits.length} block(s) in ${input.path}.`,
|
|
311
|
+
newContent,
|
|
312
|
+
resolutions,
|
|
313
|
+
),
|
|
314
|
+
}],
|
|
274
315
|
details: {
|
|
275
316
|
diff: diffResult.diff,
|
|
276
|
-
patch,
|
|
317
|
+
patch: generateUnifiedPatch(input.path, baseContent, newContent),
|
|
277
318
|
firstChangedLine: diffResult.firstChangedLine,
|
|
278
|
-
}
|
|
319
|
+
},
|
|
279
320
|
};
|
|
321
|
+
throwIfAborted();
|
|
322
|
+
|
|
323
|
+
// Best-effort external modification detection. This is not a lock or a
|
|
324
|
+
// race-free compare-and-swap; another process can still change the target
|
|
325
|
+
// after this check and before/during the write.
|
|
326
|
+
try {
|
|
327
|
+
const [currentBuffer, currentStat] = await Promise.all([
|
|
328
|
+
ops.readFile(absolutePath),
|
|
329
|
+
ops.stat(absolutePath),
|
|
330
|
+
]);
|
|
331
|
+
if (!sameFileIdentity(initialStat, currentStat) || !buffer.equals(currentBuffer)) {
|
|
332
|
+
throw new Error("target-changed");
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
throwIfAborted();
|
|
336
|
+
if (error instanceof Error && error.message === "target-changed") {
|
|
337
|
+
throw new Error(`Could not edit file: ${input.path}. The target changed after it was read; no write was started. Re-read and retry.`);
|
|
338
|
+
}
|
|
339
|
+
throw new Error(`Could not revalidate file before writing: ${input.path}. ${describeFsError(error)}. No write was started.`);
|
|
340
|
+
}
|
|
341
|
+
throwIfAborted();
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
await ops.writeFile(absolutePath, finalContent);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
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.`);
|
|
347
|
+
}
|
|
348
|
+
// A resolved write is the commit boundary. Ignore cancellation that arrived
|
|
349
|
+
// during it and report the committed result; no fallible formatting remains.
|
|
350
|
+
return success;
|
|
280
351
|
});
|
|
281
352
|
}
|
|
282
353
|
|
|
283
|
-
export function registerBetterEditTool(pi: ExtensionAPI): void {
|
|
354
|
+
export function registerBetterEditTool(pi: ExtensionAPI, options: BetterEditExecutionOptions = {}): void {
|
|
284
355
|
pi.registerTool({
|
|
285
356
|
name: "edit",
|
|
286
357
|
label: "edit",
|
|
287
358
|
description:
|
|
288
|
-
"Edit a single file using exact text replacement. Every edits[].oldText should match a unique, non-overlapping region of the original file.
|
|
359
|
+
"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.",
|
|
289
360
|
promptSnippet:
|
|
290
|
-
"Make precise file edits with exact text replacement; failures return recovery context
|
|
361
|
+
"Make precise local file edits with exact text replacement; failures return bounded recovery context",
|
|
291
362
|
promptGuidelines: [
|
|
292
|
-
"Use edit for precise changes (edits[].oldText must match exactly)",
|
|
293
|
-
"When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls",
|
|
294
|
-
"
|
|
295
|
-
"Keep edits[].oldText as small as possible while still being unique in the file
|
|
296
|
-
"When edit safely auto-disambiguates repeated text from the latest verified read, its success message lists
|
|
297
|
-
"
|
|
363
|
+
"Use edit for precise changes (edits[].oldText must match exactly).",
|
|
364
|
+
"When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls.",
|
|
365
|
+
"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.",
|
|
366
|
+
"Keep edit edits[].oldText as small as possible while still being unique in the file; do not pad with large unchanged regions.",
|
|
367
|
+
"When edit safely auto-disambiguates repeated text from the latest verified stored-context read or read_symbol result, its success message lists bounded remaining candidates for an optional follow-up edit.",
|
|
368
|
+
"An edit matching failure applies none of the batch. Fix the reported entries and resubmit the complete batch, not only the failed replacement. Use suggested read_symbol calls when you need the whole enclosing function.",
|
|
369
|
+
"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.",
|
|
298
370
|
],
|
|
299
371
|
parameters: betterEditSchema,
|
|
300
372
|
prepareArguments: (args: unknown): BetterEditInput => prepareEditArguments(args) as BetterEditInput,
|
|
301
373
|
async execute(_toolCallId, input, signal, _onUpdate, ctx) {
|
|
302
|
-
return executeBetterEdit(input, signal, ctx);
|
|
374
|
+
return executeBetterEdit(input, signal, ctx, options);
|
|
303
375
|
},
|
|
304
376
|
});
|
|
305
377
|
}
|