pi-hashline-edit-pro 6.3.1 → 6.4.1
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 +33 -32
- package/index.ts +32 -16
- package/package.json +1 -1
- package/src/batch.ts +30 -9
- package/src/commit.ts +17 -3
- package/src/config-ui.ts +1 -1
- package/src/constants.ts +3 -0
- package/src/copy-move.ts +100 -68
- package/src/edit-common.ts +86 -54
- package/src/edit-fidelity.ts +16 -6
- package/src/fs-write.ts +14 -2
- package/src/grep.ts +22 -11
- package/src/insert.ts +5 -4
- package/src/line-endings.ts +6 -2
- package/src/model-gate.ts +1 -1
- package/src/payload-contract.ts +13 -9
- package/src/pre-edit-snapshot.ts +19 -0
- package/src/prompts.ts +32 -0
- package/src/read.ts +5 -10
- package/src/replace-diff.ts +5 -3
- package/src/replace-match.ts +2 -2
- package/src/replace-render.ts +4 -4
- package/src/replace-response.ts +1 -0
- package/src/replace-undo.ts +4 -4
- package/src/replace.ts +2 -2
- package/src/structured.ts +2 -1
- package/src/utils.ts +29 -0
- package/src/write-hook.ts +1 -1
- package/tool-prompts/copy-snippet.md +1 -1
- package/tool-prompts/copy.md +2 -2
- package/tool-prompts/grep-guidelines.md +2 -1
- package/tool-prompts/grep-snippet.md +1 -1
- package/tool-prompts/grep.md +1 -1
- package/tool-prompts/insert.md +1 -1
- package/tool-prompts/move-snippet.md +1 -1
- package/tool-prompts/move.md +2 -2
- package/tool-prompts/read-guidelines.md +2 -2
- package/tool-prompts/read.md +1 -3
- package/tool-prompts/replace-match-guidelines.md +1 -0
- package/tool-prompts/replace-match-snippet.md +1 -1
- package/tool-prompts/replace-match.md +1 -1
- package/tool-prompts/replace.md +3 -2
- package/tool-prompts/transfer-guidelines.md +3 -3
- package/tool-prompts/undo-last-change-snippet.md +1 -1
- package/tool-prompts/undo-last-change.md +1 -1
package/src/copy-move.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { constants } from "node:fs";
|
|
|
5
5
|
import { execPipeline, noteAnchorError, previewFromPipe, previewError, type PipelineResult, type ReplaceDetails } from "./replace";
|
|
6
6
|
import { commitEdit } from "./commit";
|
|
7
7
|
import { readNormFile, safeSnapId, type NormFile } from "./file-reader";
|
|
8
|
+
import { preEditSnapshotFor } from "./pre-edit-snapshot";
|
|
8
9
|
import {
|
|
9
10
|
lineHashes,
|
|
10
11
|
MAX_HASH_LINES,
|
|
@@ -20,11 +21,11 @@ import {
|
|
|
20
21
|
} from "./hashline";
|
|
21
22
|
import { formatAnchorReclaimNotice, servedForPath, takeReclaimedPaths, withAnchorSession } from "./anchor-registry";
|
|
22
23
|
import { batchMemberFor, batchServedFor, ensureBatchBase, executeBatchMember, noteBatchFailure, pendingBatchMemberFor, registerBatchPlanner, type BatchBase, type BatchMemberPlanner, type PlannedMember } from "./batch";
|
|
23
|
-
import { loadP,
|
|
24
|
+
import { loadP, loadGuidelines } from "./prompts";
|
|
24
25
|
import { assertTransferReq, normReq, type TransferReq } from "./payload-contract";
|
|
25
26
|
import { abortIf, assertByteLimit, isRec, splitLines } from "./utils";
|
|
26
27
|
import { toDisplayPath } from "./paths";
|
|
27
|
-
import { getDiffContextLines
|
|
28
|
+
import { getDiffContextLines } from "./config";
|
|
28
29
|
import { resolveInCwd, writeAtomic } from "./fs-write";
|
|
29
30
|
import { restoreEndings, type LineEnding } from "./normalize";
|
|
30
31
|
import { applyEndingOverrides, endingsForRange, joinSeparators, separatorsForSpans } from "./line-endings";
|
|
@@ -33,6 +34,7 @@ import { buildChanged, buildMetrics, type TResult } from "./replace-response";
|
|
|
33
34
|
import { servedHashesFromDiff, serveRows } from "./served";
|
|
34
35
|
import {
|
|
35
36
|
assertBoundaryLinesServed,
|
|
37
|
+
assertPathOption,
|
|
36
38
|
DEFAULT_EDIT_FLAGS,
|
|
37
39
|
editRenderResultWrapper,
|
|
38
40
|
editToolBase,
|
|
@@ -52,7 +54,8 @@ export type TransferKind = "copy" | "move";
|
|
|
52
54
|
interface TransferRefs {
|
|
53
55
|
sourceFrom: Anchor;
|
|
54
56
|
sourceTo: Anchor;
|
|
55
|
-
|
|
57
|
+
anchor: Anchor;
|
|
58
|
+
direction: "before" | "after";
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
export interface TransferPlan {
|
|
@@ -69,6 +72,7 @@ interface PairFileCommit {
|
|
|
69
72
|
absolutePath: string;
|
|
70
73
|
mutationTargetPath: string;
|
|
71
74
|
foldedAnchorLines: number;
|
|
75
|
+
anchorCarry?: number;
|
|
72
76
|
endingOverrides?: (LineEnding | undefined)[];
|
|
73
77
|
}
|
|
74
78
|
|
|
@@ -87,12 +91,13 @@ export function parseTransferAnchors(req: TransferReq): { refs: TransferRefs; wa
|
|
|
87
91
|
const warnings: string[] = [];
|
|
88
92
|
const from = stripAnchorRow(req.source_from.trim(), "source_from entry", warnings);
|
|
89
93
|
const to = stripAnchorRow(req.source_to.trim(), "source_to entry", warnings);
|
|
90
|
-
const
|
|
94
|
+
const destination = stripAnchorRow(req.anchor.trim(), "anchor entry", warnings);
|
|
91
95
|
return {
|
|
92
96
|
refs: {
|
|
93
97
|
sourceFrom: parseHashRef(from),
|
|
94
98
|
sourceTo: parseHashRef(to),
|
|
95
|
-
|
|
99
|
+
anchor: parseHashRef(destination),
|
|
100
|
+
direction: req.direction,
|
|
96
101
|
},
|
|
97
102
|
warnings,
|
|
98
103
|
};
|
|
@@ -118,13 +123,14 @@ export function buildTransferEdit(input: {
|
|
|
118
123
|
const toLine = resolveAnchorLine(refs.sourceTo, fileLines, preload.fileHashes, displayPath);
|
|
119
124
|
const sourceStart = Math.min(fromLine, toLine);
|
|
120
125
|
const sourceEnd = Math.max(fromLine, toLine);
|
|
121
|
-
const
|
|
126
|
+
const anchorLine = resolveAnchorLine(refs.anchor, fileLines, preload.fileHashes, displayPath);
|
|
127
|
+
const insertLine = refs.direction === "after" ? anchorLine : anchorLine - 1;
|
|
122
128
|
if (insertLine >= sourceStart && insertLine < sourceEnd) {
|
|
123
129
|
throw new Error(
|
|
124
|
-
`[E_BAD_SHAPE] "
|
|
130
|
+
`[E_BAD_SHAPE] "anchor" resolves to line ${anchorLine}, which places the block inside the source range (lines ${sourceStart}-${sourceEnd}). Use a line outside the source range.`,
|
|
125
131
|
);
|
|
126
132
|
}
|
|
127
|
-
assertBoundaryLinesServed(fileLines, preload.fileHashes, served,
|
|
133
|
+
assertBoundaryLinesServed(fileLines, preload.fileHashes, served, anchorLine, anchorLine, displayPath);
|
|
128
134
|
assertBoundaryLinesServed(fileLines, preload.fileHashes, served, sourceStart, sourceStart, displayPath);
|
|
129
135
|
assertBoundaryLinesServed(fileLines, preload.fileHashes, served, sourceEnd, sourceEnd, displayPath);
|
|
130
136
|
const sourceLines = fileLines.slice(sourceStart - 1, sourceEnd);
|
|
@@ -132,13 +138,17 @@ export function buildTransferEdit(input: {
|
|
|
132
138
|
if (kind === "copy") {
|
|
133
139
|
return {
|
|
134
140
|
editParams: {
|
|
135
|
-
remove_from: preload.fileHashes[
|
|
136
|
-
remove_to: preload.fileHashes[
|
|
137
|
-
text:
|
|
141
|
+
remove_from: preload.fileHashes[anchorLine - 1]!,
|
|
142
|
+
remove_to: preload.fileHashes[anchorLine - 1]!,
|
|
143
|
+
text: refs.direction === "after"
|
|
144
|
+
? [fileLines[anchorLine - 1]!, ...sourceLines]
|
|
145
|
+
: [...sourceLines, fileLines[anchorLine - 1]!],
|
|
138
146
|
},
|
|
139
147
|
foldedAnchorLines: 1,
|
|
140
|
-
anchorCarry: 0,
|
|
141
|
-
endingOverrides:
|
|
148
|
+
anchorCarry: refs.direction === "after" ? 0 : sourceLines.length,
|
|
149
|
+
endingOverrides: refs.direction === "after"
|
|
150
|
+
? [undefined, ...sourceEndings]
|
|
151
|
+
: [...sourceEndings, undefined],
|
|
142
152
|
};
|
|
143
153
|
}
|
|
144
154
|
if (insertLine < sourceStart) {
|
|
@@ -169,40 +179,34 @@ export function buildTransferEdit(input: {
|
|
|
169
179
|
};
|
|
170
180
|
}
|
|
171
181
|
|
|
182
|
+
function describeTransferTargets(sourcePath: string, destinationPath: string): string {
|
|
183
|
+
return sourcePath === destinationPath
|
|
184
|
+
? `the file "${sourcePath}"`
|
|
185
|
+
: `the source file "${sourcePath}" or the destination file "${destinationPath}"`;
|
|
186
|
+
}
|
|
187
|
+
|
|
172
188
|
async function resolveTransferTargets(req: TransferReq, cwd: string): Promise<{ sourcePath: string; destinationPath: string }> {
|
|
173
|
-
const { requirePath } = await readConfig();
|
|
174
|
-
if (!requirePath && req.path !== undefined) {
|
|
175
|
-
throw new Error("[E_BAD_SHAPE] Edit request contains unknown or unsupported fields: path. Path resolution is anchor-only; retry without `path`.");
|
|
176
|
-
}
|
|
177
|
-
if (requirePath && (typeof req.path !== "string" || req.path.length === 0)) {
|
|
178
|
-
throw new Error('[E_BAD_SHAPE] Copy/move request requires a non-empty "path" string when require-path mode is on. Provide `path` matching the source or destination file the anchors were served for.');
|
|
179
|
-
}
|
|
180
189
|
const sourcePath = resolveEditTarget(req.source_from, req.source_to);
|
|
181
|
-
const destinationPath = resolveEditTarget(req.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
190
|
+
const destinationPath = resolveEditTarget(req.anchor);
|
|
191
|
+
await assertPathOption({
|
|
192
|
+
providedPath: req.path,
|
|
193
|
+
cwd,
|
|
194
|
+
targets: [sourcePath, destinationPath],
|
|
195
|
+
expected: describeTransferTargets(sourcePath, destinationPath),
|
|
196
|
+
requestLabel: "Copy/move request",
|
|
197
|
+
});
|
|
188
198
|
return { sourcePath, destinationPath };
|
|
189
199
|
}
|
|
190
200
|
|
|
191
201
|
async function assertTransferPathOption(req: TransferReq, cwd: string, member: PlannedMember): Promise<void> {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const sourceTarget = member.sourceTarget ?? member.target;
|
|
201
|
-
const { resolved } = await resolveInCwd(req.path as string, cwd);
|
|
202
|
-
if (resolved !== sourceTarget && resolved !== member.target) {
|
|
203
|
-
throw new Error(`[E_BAD_SHAPE] Provided "path" "${req.path}" does not match the source file "${sourceTarget}" or the destination file "${member.target}".`);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
202
|
+
const sourceTarget = member.sourceTarget ?? member.target;
|
|
203
|
+
await assertPathOption({
|
|
204
|
+
providedPath: req.path,
|
|
205
|
+
cwd,
|
|
206
|
+
targets: [sourceTarget, member.target],
|
|
207
|
+
expected: describeTransferTargets(sourceTarget, member.target),
|
|
208
|
+
requestLabel: "Copy/move request",
|
|
209
|
+
});
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
interface CrossTransferPreparation {
|
|
@@ -212,6 +216,7 @@ interface CrossTransferPreparation {
|
|
|
212
216
|
destinationDisplay: string;
|
|
213
217
|
destinationEdit: HTEdit;
|
|
214
218
|
destinationFolded: number;
|
|
219
|
+
anchorCarry?: number;
|
|
215
220
|
endingOverrides: (LineEnding | undefined)[];
|
|
216
221
|
sourceEdit?: HTEdit;
|
|
217
222
|
}
|
|
@@ -232,11 +237,17 @@ async function prepareCrossTransfer(input: {
|
|
|
232
237
|
const { kind, refs, sourcePath, destinationPath, cwd, signal, noPersist } = input;
|
|
233
238
|
const common = { signal, maxLines: MAX_HASH_LINES };
|
|
234
239
|
const sourceAccessMode = kind === "move" ? constants.R_OK | constants.W_OK : constants.R_OK;
|
|
235
|
-
const
|
|
240
|
+
const candidate = kind === "copy" && input.sourcePreload === undefined ? preEditSnapshotFor(sourcePath) : undefined;
|
|
241
|
+
const snapshotPreload = candidate !== undefined
|
|
242
|
+
&& candidate.fileHashes.includes(refs.sourceFrom.hash)
|
|
243
|
+
&& candidate.fileHashes.includes(refs.sourceTo.hash)
|
|
244
|
+
? candidate
|
|
245
|
+
: undefined;
|
|
246
|
+
const sourcePreload = input.sourcePreload ?? snapshotPreload ?? (await readNormFile(sourcePath, cwd, noPersist ? { ...common, accessMode: constants.R_OK, noPersist: true, allocation: "shadow" } : { ...common, accessMode: sourceAccessMode }));
|
|
236
247
|
const destinationPreload = input.destinationPreload ?? (await readNormFile(destinationPath, cwd, noPersist ? { ...common, accessMode: constants.R_OK, noPersist: true, allocation: "shadow" } : { ...common, accessMode: constants.R_OK | constants.W_OK }));
|
|
237
248
|
const sourceDisplay = toDisplayPath(cwd, sourcePreload.absolutePath, sourcePath);
|
|
238
249
|
const destinationDisplay = toDisplayPath(cwd, destinationPreload.absolutePath, destinationPath);
|
|
239
|
-
const sourceServed = input.sourceServed ?? servedForPath(sourcePreload.absolutePath);
|
|
250
|
+
const sourceServed = input.sourceServed ?? (snapshotPreload !== undefined ? snapshotPreload.served : servedForPath(sourcePreload.absolutePath));
|
|
240
251
|
const destinationServed = input.destinationServed ?? servedForPath(destinationPreload.absolutePath);
|
|
241
252
|
const sourceLines = splitLines(sourcePreload.normalized);
|
|
242
253
|
const destinationLines = splitLines(destinationPreload.normalized);
|
|
@@ -268,15 +279,15 @@ async function prepareCrossTransfer(input: {
|
|
|
268
279
|
throw error;
|
|
269
280
|
}
|
|
270
281
|
|
|
271
|
-
let
|
|
282
|
+
let anchorLine = 0;
|
|
272
283
|
try {
|
|
273
|
-
|
|
284
|
+
anchorLine = resolveAnchorLine(refs.anchor, destinationLines, destinationPreload.fileHashes, destinationDisplay);
|
|
274
285
|
} catch (error) {
|
|
275
286
|
await adopt(destinationPreload.absolutePath, error);
|
|
276
287
|
throw error;
|
|
277
288
|
}
|
|
278
289
|
try {
|
|
279
|
-
assertBoundaryLinesServed(destinationLines, destinationPreload.fileHashes, destinationServed,
|
|
290
|
+
assertBoundaryLinesServed(destinationLines, destinationPreload.fileHashes, destinationServed, anchorLine, anchorLine, destinationDisplay);
|
|
280
291
|
} catch (error) {
|
|
281
292
|
await adopt(destinationPreload.absolutePath, error);
|
|
282
293
|
throw error;
|
|
@@ -289,23 +300,33 @@ async function prepareCrossTransfer(input: {
|
|
|
289
300
|
remove_to: destinationPreload.fileHashes[0]!,
|
|
290
301
|
text: [...moved],
|
|
291
302
|
}
|
|
292
|
-
:
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
303
|
+
: refs.direction === "after"
|
|
304
|
+
? {
|
|
305
|
+
remove_from: destinationPreload.fileHashes[anchorLine - 1]!,
|
|
306
|
+
remove_to: destinationPreload.fileHashes[anchorLine - 1]!,
|
|
307
|
+
text: [destinationLines[anchorLine - 1]!, ...moved],
|
|
308
|
+
}
|
|
309
|
+
: {
|
|
310
|
+
remove_from: destinationPreload.fileHashes[anchorLine - 1]!,
|
|
311
|
+
remove_to: destinationPreload.fileHashes[anchorLine - 1]!,
|
|
312
|
+
text: [...moved, destinationLines[anchorLine - 1]!],
|
|
313
|
+
};
|
|
297
314
|
const destinationFolded = destinationPreload.normalized.length === 0 ? 0 : 1;
|
|
298
315
|
const sourceEndings = endingsForRange(sourcePreload.endingSeparators, sourceStart, sourceEnd);
|
|
299
|
-
const endingOverrides = destinationFolded ===
|
|
316
|
+
const endingOverrides = destinationFolded === 0
|
|
317
|
+
? sourceEndings
|
|
318
|
+
: refs.direction === "after"
|
|
319
|
+
? offsetEndings(1, sourceEndings)
|
|
320
|
+
: sourceEndings;
|
|
300
321
|
if (kind === "copy") {
|
|
301
|
-
return { sourcePreload, destinationPreload, sourceDisplay, destinationDisplay, destinationEdit, destinationFolded, endingOverrides };
|
|
322
|
+
return { sourcePreload, destinationPreload, sourceDisplay, destinationDisplay, destinationEdit, destinationFolded, ...(destinationFolded === 1 ? { anchorCarry: refs.direction === "after" ? 0 : moved.length } : {}), endingOverrides };
|
|
302
323
|
}
|
|
303
324
|
const sourceEdit: HTEdit = {
|
|
304
325
|
remove_from: sourcePreload.fileHashes[sourceStart - 1]!,
|
|
305
326
|
remove_to: sourcePreload.fileHashes[sourceEnd - 1]!,
|
|
306
327
|
text: [],
|
|
307
328
|
};
|
|
308
|
-
return { sourcePreload, destinationPreload, sourceDisplay, destinationDisplay, destinationEdit, destinationFolded, sourceEdit, endingOverrides };
|
|
329
|
+
return { sourcePreload, destinationPreload, sourceDisplay, destinationDisplay, destinationEdit, destinationFolded, ...(destinationFolded === 1 ? { anchorCarry: refs.direction === "after" ? 0 : moved.length } : {}), sourceEdit, endingOverrides };
|
|
309
330
|
}
|
|
310
331
|
|
|
311
332
|
function pairEntry(pipe: PipelineResult, separators: LineEnding[] | undefined): UndoEntry {
|
|
@@ -333,6 +354,7 @@ async function commitMovePair(input: {
|
|
|
333
354
|
]);
|
|
334
355
|
const destinationSpan = input.destination.pipe.spans?.[0];
|
|
335
356
|
const sourceSpan = input.source.pipe.spans?.[0];
|
|
357
|
+
if (destinationSpan && input.destination.anchorCarry !== undefined) destinationSpan.carry = input.destination.anchorCarry;
|
|
336
358
|
const destinationSeparators = destinationSpan
|
|
337
359
|
? separatorsForSpans(input.destination.pipe.originalSeparators, input.destination.pipe.originalHashes.length, [destinationSpan], input.destination.pipe.result, input.destination.pipe.originalEnding, splitLines(input.destination.pipe.originalNormalized))
|
|
338
360
|
: undefined;
|
|
@@ -558,7 +580,7 @@ async function executeCrossFile(
|
|
|
558
580
|
absolutePath: destination.absolute,
|
|
559
581
|
mutationTargetPath: destination.resolved,
|
|
560
582
|
editAnchors: [prepared.destinationEdit.remove_from, prepared.destinationEdit.remove_to],
|
|
561
|
-
...(prepared.
|
|
583
|
+
...(prepared.anchorCarry !== undefined ? { anchorCarry: prepared.anchorCarry } : {}),
|
|
562
584
|
endingOverrides: prepared.endingOverrides,
|
|
563
585
|
signal,
|
|
564
586
|
verb: "copied",
|
|
@@ -581,6 +603,7 @@ async function executeCrossFile(
|
|
|
581
603
|
absolutePath: destination.absolute,
|
|
582
604
|
mutationTargetPath: destination.resolved,
|
|
583
605
|
foldedAnchorLines: prepared.destinationFolded,
|
|
606
|
+
...(prepared.anchorCarry !== undefined ? { anchorCarry: prepared.anchorCarry } : {}),
|
|
584
607
|
endingOverrides: prepared.endingOverrides,
|
|
585
608
|
},
|
|
586
609
|
signal,
|
|
@@ -624,6 +647,7 @@ function planTransferBatchMember(kind: TransferKind): BatchMemberPlanner {
|
|
|
624
647
|
warnings: [...warnings, ...resWarnings],
|
|
625
648
|
planned,
|
|
626
649
|
foldedLines: transferPlan.foldedAnchorLines,
|
|
650
|
+
direction: refs.direction,
|
|
627
651
|
...(transferPlan.anchorCarry !== undefined ? { carryIndex: transferPlan.anchorCarry } : {}),
|
|
628
652
|
...(transferPlan.servedOverride !== undefined ? { servedOverride: transferPlan.servedOverride } : {}),
|
|
629
653
|
...(transferPlan.endingOverrides !== undefined ? { contentSeparators: transferPlan.endingOverrides } : {}),
|
|
@@ -650,7 +674,7 @@ async function executeBatchCrossCopy(
|
|
|
650
674
|
const sourceMember = pendingBatchMemberFor(sourcePath);
|
|
651
675
|
const sourcePreload = sourceMember
|
|
652
676
|
? preloadFromBase(await ensureBatchBase({ member: sourceMember, targetPath: sourcePath, mutationTargetPath: sourceMember.target, cwd, signal }))
|
|
653
|
-
:
|
|
677
|
+
: undefined;
|
|
654
678
|
prepared = await prepareCrossTransfer({
|
|
655
679
|
kind: "copy",
|
|
656
680
|
refs,
|
|
@@ -686,7 +710,7 @@ async function executeBatchCrossCopy(
|
|
|
686
710
|
hedit,
|
|
687
711
|
warnings: [...warnings, ...resWarnings],
|
|
688
712
|
foldedLines: prepared.destinationFolded,
|
|
689
|
-
...(prepared.
|
|
713
|
+
...(prepared.anchorCarry !== undefined ? { carryIndex: prepared.anchorCarry } : {}),
|
|
690
714
|
contentSeparators: prepared.endingOverrides,
|
|
691
715
|
},
|
|
692
716
|
});
|
|
@@ -751,7 +775,7 @@ async function executeBatchCrossMove(
|
|
|
751
775
|
hedit,
|
|
752
776
|
warnings: [...warnings, ...resWarnings, ...sourceWarnings],
|
|
753
777
|
foldedLines: prepared.destinationFolded,
|
|
754
|
-
...(prepared.
|
|
778
|
+
...(prepared.anchorCarry !== undefined ? { carryIndex: prepared.anchorCarry } : {}),
|
|
755
779
|
contentSeparators: prepared.endingOverrides,
|
|
756
780
|
sourceMove: {
|
|
757
781
|
displayPath: prepared.sourceDisplay,
|
|
@@ -825,7 +849,7 @@ export async function transferPreview(kind: TransferKind, request: unknown, cwd:
|
|
|
825
849
|
}
|
|
826
850
|
}
|
|
827
851
|
|
|
828
|
-
function getTransferInput(args: unknown): { path?: string; source_from?: string; source_to?: string;
|
|
852
|
+
function getTransferInput(args: unknown): { path?: string; source_from?: string; source_to?: string; anchor?: string; direction?: string } | null {
|
|
829
853
|
let normalized: unknown;
|
|
830
854
|
try {
|
|
831
855
|
normalized = normReq(args);
|
|
@@ -836,7 +860,8 @@ function getTransferInput(args: unknown): { path?: string; source_from?: string;
|
|
|
836
860
|
if (
|
|
837
861
|
typeof normalized.source_from !== "string" ||
|
|
838
862
|
typeof normalized.source_to !== "string" ||
|
|
839
|
-
typeof normalized.
|
|
863
|
+
typeof normalized.anchor !== "string" ||
|
|
864
|
+
typeof normalized.direction !== "string"
|
|
840
865
|
) {
|
|
841
866
|
return null;
|
|
842
867
|
}
|
|
@@ -844,22 +869,27 @@ function getTransferInput(args: unknown): { path?: string; source_from?: string;
|
|
|
844
869
|
...(typeof normalized.path === "string" ? { path: normalized.path } : {}),
|
|
845
870
|
source_from: normalized.source_from,
|
|
846
871
|
source_to: normalized.source_to,
|
|
847
|
-
|
|
872
|
+
anchor: normalized.anchor,
|
|
873
|
+
direction: normalized.direction,
|
|
848
874
|
};
|
|
849
875
|
}
|
|
850
876
|
|
|
851
877
|
const transferSourceFromSchema = Type.String({
|
|
852
878
|
description:
|
|
853
|
-
"4-char anchor of the FIRST source line to copy
|
|
879
|
+
"4-char anchor of the FIRST source line to copy.",
|
|
854
880
|
});
|
|
855
881
|
const transferSourceToSchema = Type.String({
|
|
856
882
|
description:
|
|
857
883
|
"4-char anchor of the LAST source line to copy.",
|
|
858
884
|
});
|
|
859
|
-
const
|
|
885
|
+
const transferAnchorSchema = Type.String({
|
|
860
886
|
description:
|
|
861
|
-
"4-char anchor of the destination line; the block
|
|
887
|
+
"4-char anchor of the destination line; the block lands before or after it and it may live in another file.",
|
|
862
888
|
});
|
|
889
|
+
const transferDirectionSchema = Type.Union(
|
|
890
|
+
[Type.Literal("after"), Type.Literal("before")],
|
|
891
|
+
{ description: '"after" places the block below the anchor line, "before" above it' },
|
|
892
|
+
);
|
|
863
893
|
const transferPathRequiredSchema = Type.String({
|
|
864
894
|
description:
|
|
865
895
|
"Path to the source or destination file the anchors were served for; required and must match anchor ownership.",
|
|
@@ -869,7 +899,8 @@ const transferToolSchema = Type.Object(
|
|
|
869
899
|
{
|
|
870
900
|
source_from: transferSourceFromSchema,
|
|
871
901
|
source_to: transferSourceToSchema,
|
|
872
|
-
|
|
902
|
+
anchor: transferAnchorSchema,
|
|
903
|
+
direction: transferDirectionSchema,
|
|
873
904
|
},
|
|
874
905
|
{ additionalProperties: true },
|
|
875
906
|
);
|
|
@@ -881,7 +912,8 @@ export function buildTransferToolSchema(requirePath: boolean): typeof transferTo
|
|
|
881
912
|
path: transferPathRequiredSchema,
|
|
882
913
|
source_from: transferSourceFromSchema,
|
|
883
914
|
source_to: transferSourceToSchema,
|
|
884
|
-
|
|
915
|
+
anchor: transferAnchorSchema,
|
|
916
|
+
direction: transferDirectionSchema,
|
|
885
917
|
},
|
|
886
918
|
{ additionalProperties: true },
|
|
887
919
|
) as typeof transferToolSchema;
|
|
@@ -895,8 +927,8 @@ export function buildTransferToolDef(kind: TransferKind, flags: EditToolFlags =
|
|
|
895
927
|
description: loadP(`../tool-prompts/${kind}.md`),
|
|
896
928
|
snippet: loadP(`../tool-prompts/${kind}-snippet.md`),
|
|
897
929
|
guidelines: [
|
|
898
|
-
...
|
|
899
|
-
...
|
|
930
|
+
...loadGuidelines(`../tool-prompts/${kind}-guidelines.md`),
|
|
931
|
+
...loadGuidelines("../tool-prompts/transfer-guidelines.md"),
|
|
900
932
|
],
|
|
901
933
|
},
|
|
902
934
|
flags,
|
package/src/edit-common.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { assertRangeServed, lineChecksum, parseHashRef, stripAnchorRow } from ".
|
|
|
6
6
|
import { readConfig } from "./config";
|
|
7
7
|
import { makeRenderCall, renderEditResult, type RPreview, type FgT } from "./replace-render";
|
|
8
8
|
import type { ReplaceDetails } from "./replace";
|
|
9
|
+
import type { Guideline } from "./prompts";
|
|
9
10
|
export const editPrepare = makePrepareArguments();
|
|
10
11
|
|
|
11
12
|
export interface EditToolFlags {
|
|
@@ -46,7 +47,7 @@ export async function currentEditFlags(codemode = false): Promise<EditToolFlags>
|
|
|
46
47
|
|
|
47
48
|
function preferenceGuideline(flags: EditToolFlags): string {
|
|
48
49
|
const tools = gatedEditOps(["read", "replace", "replace_match", "insert", "copy", "move", "undo_last_change"], flags);
|
|
49
|
-
return `Prefer the hashline tools for
|
|
50
|
+
return `Prefer the hashline tools for every file read and edit: ${joinOps(tools, { backtick: true })}.`;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
const SHARED_EDIT_OPS = ["replace", "replace_match", "insert", "copy", "move"];
|
|
@@ -61,6 +62,17 @@ const SCRIPT_TRANSFER_GUIDELINE =
|
|
|
61
62
|
const SCRIPT_UNDO_GUIDELINE =
|
|
62
63
|
"`undo_last_change`: each edit from one codemode script takes the undo slot, so only the most recent is undoable.";
|
|
63
64
|
|
|
65
|
+
const READ_REREAD_ID = "read-reread";
|
|
66
|
+
const READ_OUTLINE_ID = "read-outline";
|
|
67
|
+
const READ_MATCH_ID = "read-match-served";
|
|
68
|
+
const UNDO_WRITE_CLEARS_ID = "undo-write-clears";
|
|
69
|
+
const UNDO_CROSS_FILE_MOVE_ID = "undo-cross-file-move";
|
|
70
|
+
const NEUTRAL_READ_REREAD = "`read`: call again after an edit when you need anchors you lack.";
|
|
71
|
+
const NEUTRAL_UNDO_WRITE_CLEARS = "`undo_last_change`: only the last `replace`/`replace_match`/`insert`/`copy`/`move` per file is undoable; a `write` clears it, so undo before any other edit or write.";
|
|
72
|
+
const GREP_OPS_PHRASE = "replace, insert, copy, or move";
|
|
73
|
+
const UNDO_OPS_PHRASE = "replace, replace_match, insert, copy, or move";
|
|
74
|
+
const UNDO_GUIDELINE_OPS_PHRASE = "`replace`/`replace_match`/`insert`/`copy`/`move`";
|
|
75
|
+
|
|
64
76
|
function operationNames(ops: string[], flags: EditToolFlags): string {
|
|
65
77
|
return joinOps(gatedEditOps(ops, flags), { backtick: true, separator: "/" });
|
|
66
78
|
}
|
|
@@ -69,24 +81,24 @@ function batchGuideline(flags: EditToolFlags): string {
|
|
|
69
81
|
const tools = operationNames(SHARED_EDIT_OPS, flags);
|
|
70
82
|
const outcome = flags.autoRead ? "diff" : "result";
|
|
71
83
|
const script = flags.codemode ? ` ${SCRIPT_BATCH_GUIDELINE}` : "";
|
|
72
|
-
return `${tools}: same-file calls in one message
|
|
84
|
+
return `${tools}: same-file calls in one message commit as one batch; earlier calls reply \`In batch N (queued)\`, the last shows the combined ${outcome}, and one undo reverts all. A request-invalid call (anchors, shape, or \`old_string\`) fails alone; a failure while the batch runs aborts it.${script}`;
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
function diffGuideline(flags: EditToolFlags): string {
|
|
76
88
|
const tools = operationNames(SHARED_DIFF_OPS, flags);
|
|
77
|
-
return `${tools}: in the post-edit diff, \`-anchor│\` rows are dead anchors; \`+anchor│\` and \` anchor│\` rows
|
|
89
|
+
return `${tools}: in the post-edit diff, \`-anchor│\` rows are dead anchors; \`+anchor│\` and \` anchor│\` rows carry live anchors for the next edit.`;
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
function pathGuideline(flags: EditToolFlags): string {
|
|
81
93
|
const tools = operationNames(SHARED_EDIT_OPS, flags);
|
|
82
94
|
return flags.requirePath
|
|
83
95
|
? `${tools}: pass \`path\` matching the file the anchors were served for; it is required and must match anchor ownership.`
|
|
84
|
-
: `${tools}: path
|
|
96
|
+
: `${tools}: anchors resolve the file; \`path\` is optional but must match anchor ownership when passed.`;
|
|
85
97
|
}
|
|
86
98
|
|
|
87
99
|
function payloadGuideline(flags: EditToolFlags): string {
|
|
88
100
|
const tools = operationNames(SHARED_PAYLOAD_OPS, flags);
|
|
89
|
-
return `${tools}:
|
|
101
|
+
return `${tools}: \`text\` is written exactly as received; escapes are decoded once when the call is parsed, and the tool never decodes again — \`\\uXXXX\` is the character, \`\\\\uXXXX\` the literal text.`;
|
|
90
102
|
}
|
|
91
103
|
|
|
92
104
|
function strictInputGuideline(flags: EditToolFlags): string {
|
|
@@ -109,43 +121,45 @@ function finalizePrompts(
|
|
|
109
121
|
return { description, snippet, guidelines: [...guidelines, ...shared] };
|
|
110
122
|
}
|
|
111
123
|
|
|
112
|
-
export function withReplacePrompts(base: { description: string; snippet: string; guidelines:
|
|
124
|
+
export function withReplacePrompts(base: { description: string; snippet: string; guidelines: Guideline[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
|
113
125
|
let description = base.description;
|
|
114
|
-
const guidelines = [preferenceGuideline(flags), ...base.guidelines];
|
|
126
|
+
const guidelines = [preferenceGuideline(flags), ...base.guidelines.map((guideline) => guideline.text)];
|
|
115
127
|
if (!flags.replaceMatchEnabled) {
|
|
116
|
-
description = description
|
|
128
|
+
description = description
|
|
129
|
+
.split(/(?<=\.)\s+/)
|
|
130
|
+
.filter((sentence) => !sentence.includes("`replace_match`"))
|
|
131
|
+
.join(" ");
|
|
117
132
|
}
|
|
118
133
|
return finalizePrompts(description, base.snippet, guidelines, flags);
|
|
119
134
|
}
|
|
120
135
|
|
|
121
|
-
function readGuidelineApplies(guideline:
|
|
122
|
-
if (
|
|
123
|
-
if (
|
|
136
|
+
function readGuidelineApplies(guideline: Guideline, flags: EditToolFlags): boolean {
|
|
137
|
+
if (guideline.id === READ_OUTLINE_ID) return flags.autoReadAllOutline;
|
|
138
|
+
if (guideline.id === READ_MATCH_ID) return flags.replaceMatchEnabled;
|
|
124
139
|
return true;
|
|
125
140
|
}
|
|
126
141
|
|
|
127
|
-
export function withReadPrompts(base: { description: string; snippet: string; guidelines:
|
|
142
|
+
export function withReadPrompts(base: { description: string; snippet: string; guidelines: Guideline[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
|
128
143
|
const preference = preferenceGuideline(flags);
|
|
129
144
|
const script = flags.codemode ? [RESULT_CONTRACT_GUIDELINE] : [];
|
|
130
|
-
const
|
|
145
|
+
const applicable = base.guidelines.filter((guideline) => readGuidelineApplies(guideline, flags));
|
|
131
146
|
if (flags.autoReadAllActive) {
|
|
132
|
-
const rewritten =
|
|
133
|
-
.filter((guideline) => !guideline.includes("call again after an edit"))
|
|
147
|
+
const rewritten = applicable.filter((guideline) => guideline.id !== READ_REREAD_ID).map((guideline) => guideline.text);
|
|
134
148
|
return { description: base.description, snippet: base.snippet, guidelines: [preference, ...rewritten, ...script] };
|
|
135
149
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return { description: base.description, snippet: base.snippet, guidelines:
|
|
150
|
+
const guidelines = applicable.map((guideline) =>
|
|
151
|
+
guideline.id === READ_REREAD_ID && !flags.autoRead ? NEUTRAL_READ_REREAD : guideline.text,
|
|
152
|
+
);
|
|
153
|
+
return { description: base.description, snippet: base.snippet, guidelines: [preference, ...guidelines, ...script] };
|
|
140
154
|
}
|
|
141
155
|
|
|
142
|
-
export function withInsertPrompts(base: { description: string; snippet: string; guidelines:
|
|
143
|
-
const guidelines = [preferenceGuideline(flags), ...base.guidelines];
|
|
156
|
+
export function withInsertPrompts(base: { description: string; snippet: string; guidelines: Guideline[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
|
157
|
+
const guidelines = [preferenceGuideline(flags), ...base.guidelines.map((guideline) => guideline.text)];
|
|
144
158
|
return finalizePrompts(base.description, base.snippet, guidelines, flags);
|
|
145
159
|
}
|
|
146
160
|
|
|
147
|
-
export function withReplaceMatchPrompts(base: { description: string; snippet: string; guidelines:
|
|
148
|
-
const guidelines = [preferenceGuideline(flags), ...base.guidelines];
|
|
161
|
+
export function withReplaceMatchPrompts(base: { description: string; snippet: string; guidelines: Guideline[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
|
162
|
+
const guidelines = [preferenceGuideline(flags), ...base.guidelines.map((guideline) => guideline.text)];
|
|
149
163
|
return finalizePrompts(base.description, base.snippet, guidelines, flags);
|
|
150
164
|
}
|
|
151
165
|
|
|
@@ -166,35 +180,34 @@ function joinOps(ops: string[], options?: { backtick?: boolean; separator?: "/"
|
|
|
166
180
|
return formatted.length === 2 ? `${head} or ${last}` : `${head}, or ${last}`;
|
|
167
181
|
}
|
|
168
182
|
|
|
169
|
-
export function withGrepPrompts(base: { description: string; snippet: string; guidelines:
|
|
170
|
-
const guidelines = [preferenceGuideline(flags), ...base.guidelines];
|
|
183
|
+
export function withGrepPrompts(base: { description: string; snippet: string; guidelines: Guideline[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
|
184
|
+
const guidelines = [preferenceGuideline(flags), ...base.guidelines.map((guideline) => guideline.text)];
|
|
171
185
|
if (flags.codemode) guidelines.push(RESULT_CONTRACT_GUIDELINE);
|
|
172
|
-
if (flags.copyMoveEnabled) return {
|
|
173
|
-
|
|
186
|
+
if (flags.copyMoveEnabled) return { description: base.description, snippet: base.snippet, guidelines };
|
|
187
|
+
const description = base.description.replaceAll(GREP_OPS_PHRASE, joinOps(gatedEditOps(["replace", "insert", "copy", "move"], flags)));
|
|
188
|
+
return { description, snippet: base.snippet, guidelines };
|
|
174
189
|
}
|
|
175
190
|
|
|
176
|
-
export function withUndoPrompts(base: { description: string; snippet: string; guidelines:
|
|
191
|
+
export function withUndoPrompts(base: { description: string; snippet: string; guidelines: Guideline[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
|
177
192
|
const ops = gatedEditOps(["replace", "replace_match", "insert", "copy", "move"], flags);
|
|
178
|
-
let description = base.description;
|
|
179
|
-
let snippet = base.snippet;
|
|
180
193
|
const script = flags.codemode ? [RESULT_CONTRACT_GUIDELINE, SCRIPT_UNDO_GUIDELINE] : [];
|
|
181
|
-
let
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
194
|
+
let description = base.description;
|
|
195
|
+
let guidelines = [
|
|
196
|
+
preferenceGuideline(flags),
|
|
197
|
+
...base.guidelines
|
|
198
|
+
.filter((guideline) => flags.copyMoveEnabled || guideline.id !== UNDO_CROSS_FILE_MOVE_ID)
|
|
199
|
+
.map((guideline) => (guideline.id === UNDO_WRITE_CLEARS_ID && !flags.autoRead ? NEUTRAL_UNDO_WRITE_CLEARS : guideline.text)),
|
|
200
|
+
...script,
|
|
201
|
+
];
|
|
185
202
|
if (ops.length !== 5) {
|
|
186
|
-
description = description.replaceAll(
|
|
187
|
-
|
|
188
|
-
guidelines = guidelines.map((guideline) => guideline.replaceAll("`replace`/`replace_match`/`insert`/`copy`/`move`", joinOps(ops, { backtick: true, separator: "/" })));
|
|
203
|
+
description = description.replaceAll(UNDO_OPS_PHRASE, joinOps(ops));
|
|
204
|
+
guidelines = guidelines.map((guideline) => guideline.replaceAll(UNDO_GUIDELINE_OPS_PHRASE, joinOps(ops, { backtick: true, separator: "/" })));
|
|
189
205
|
}
|
|
190
|
-
|
|
191
|
-
guidelines = guidelines.filter((guideline) => !guideline.includes("cross-file `move`"));
|
|
192
|
-
}
|
|
193
|
-
return { description, snippet, guidelines };
|
|
206
|
+
return { description, snippet: base.snippet, guidelines };
|
|
194
207
|
}
|
|
195
208
|
|
|
196
|
-
export function withTransferPrompts(base: { description: string; snippet: string; guidelines:
|
|
197
|
-
const guidelines = [preferenceGuideline(flags), ...base.guidelines, ...(flags.codemode ? [SCRIPT_TRANSFER_GUIDELINE] : [])];
|
|
209
|
+
export function withTransferPrompts(base: { description: string; snippet: string; guidelines: Guideline[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
|
210
|
+
const guidelines = [preferenceGuideline(flags), ...base.guidelines.map((guideline) => guideline.text), ...(flags.codemode ? [SCRIPT_TRANSFER_GUIDELINE] : [])];
|
|
198
211
|
return finalizePrompts(base.description, base.snippet, guidelines, flags, { stringPayload: false });
|
|
199
212
|
}
|
|
200
213
|
|
|
@@ -246,24 +259,43 @@ export interface PathRequirementInput {
|
|
|
246
259
|
anchorTarget?: string;
|
|
247
260
|
}
|
|
248
261
|
|
|
249
|
-
export
|
|
262
|
+
export interface PathOptionInput {
|
|
263
|
+
providedPath: unknown;
|
|
264
|
+
cwd: string;
|
|
265
|
+
targets: readonly string[];
|
|
266
|
+
expected: string;
|
|
267
|
+
requestLabel: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function assertPathOption(input: PathOptionInput): Promise<void> {
|
|
250
271
|
const { requirePath } = await readConfig();
|
|
251
|
-
if (
|
|
252
|
-
|
|
272
|
+
if (input.providedPath === undefined || (requirePath && input.providedPath === "")) {
|
|
273
|
+
if (requirePath) {
|
|
274
|
+
throw new Error(`[E_BAD_SHAPE] ${input.requestLabel} requires a non-empty "path" string when require-path mode is on. Provide \`path\` matching ${input.expected}.`);
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (typeof input.providedPath !== "string" || input.providedPath.length === 0) {
|
|
279
|
+
throw new Error(`[E_BAD_SHAPE] ${input.requestLabel} field "path" must be a non-empty string when provided.`);
|
|
253
280
|
}
|
|
254
|
-
|
|
255
|
-
|
|
281
|
+
const { resolved } = await resolveInCwd(input.providedPath, input.cwd);
|
|
282
|
+
if (!input.targets.includes(resolved)) {
|
|
283
|
+
throw new Error(`[E_BAD_SHAPE] Provided "path" "${input.providedPath}" does not match ${input.expected}.`);
|
|
256
284
|
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function resolveEditTargetWithRequirement(input: PathRequirementInput): Promise<string> {
|
|
257
288
|
const anchorTarget = input.anchorTarget
|
|
258
289
|
?? (typeof input.anchor === "string"
|
|
259
290
|
? resolveEditTarget(input.anchor)
|
|
260
291
|
: resolveEditTarget(input.removeFrom as string, input.removeTo));
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
|
|
292
|
+
await assertPathOption({
|
|
293
|
+
providedPath: input.providedPath,
|
|
294
|
+
cwd: input.cwd,
|
|
295
|
+
targets: [anchorTarget],
|
|
296
|
+
expected: `anchor ownership "${anchorTarget}"`,
|
|
297
|
+
requestLabel: "Edit request",
|
|
298
|
+
});
|
|
267
299
|
return anchorTarget;
|
|
268
300
|
}
|
|
269
301
|
|