pi-hashline-edit-pro 6.3.1 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -31
- package/index.ts +32 -16
- package/package.json +1 -1
- package/src/batch.ts +6 -4
- package/src/config-ui.ts +1 -1
- package/src/constants.ts +3 -0
- package/src/copy-move.ts +90 -65
- package/src/edit-common.ts +86 -54
- package/src/edit-fidelity.ts +16 -6
- package/src/grep.ts +22 -11
- package/src/insert.ts +5 -4
- package/src/model-gate.ts +1 -1
- package/src/payload-contract.ts +13 -9
- package/src/prompts.ts +32 -0
- package/src/read.ts +5 -10
- 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 +2 -2
- package/src/replace.ts +2 -2
- 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
|
@@ -20,11 +20,11 @@ import {
|
|
|
20
20
|
} from "./hashline";
|
|
21
21
|
import { formatAnchorReclaimNotice, servedForPath, takeReclaimedPaths, withAnchorSession } from "./anchor-registry";
|
|
22
22
|
import { batchMemberFor, batchServedFor, ensureBatchBase, executeBatchMember, noteBatchFailure, pendingBatchMemberFor, registerBatchPlanner, type BatchBase, type BatchMemberPlanner, type PlannedMember } from "./batch";
|
|
23
|
-
import { loadP,
|
|
23
|
+
import { loadP, loadGuidelines } from "./prompts";
|
|
24
24
|
import { assertTransferReq, normReq, type TransferReq } from "./payload-contract";
|
|
25
25
|
import { abortIf, assertByteLimit, isRec, splitLines } from "./utils";
|
|
26
26
|
import { toDisplayPath } from "./paths";
|
|
27
|
-
import { getDiffContextLines
|
|
27
|
+
import { getDiffContextLines } from "./config";
|
|
28
28
|
import { resolveInCwd, writeAtomic } from "./fs-write";
|
|
29
29
|
import { restoreEndings, type LineEnding } from "./normalize";
|
|
30
30
|
import { applyEndingOverrides, endingsForRange, joinSeparators, separatorsForSpans } from "./line-endings";
|
|
@@ -33,6 +33,7 @@ import { buildChanged, buildMetrics, type TResult } from "./replace-response";
|
|
|
33
33
|
import { servedHashesFromDiff, serveRows } from "./served";
|
|
34
34
|
import {
|
|
35
35
|
assertBoundaryLinesServed,
|
|
36
|
+
assertPathOption,
|
|
36
37
|
DEFAULT_EDIT_FLAGS,
|
|
37
38
|
editRenderResultWrapper,
|
|
38
39
|
editToolBase,
|
|
@@ -52,7 +53,8 @@ export type TransferKind = "copy" | "move";
|
|
|
52
53
|
interface TransferRefs {
|
|
53
54
|
sourceFrom: Anchor;
|
|
54
55
|
sourceTo: Anchor;
|
|
55
|
-
|
|
56
|
+
anchor: Anchor;
|
|
57
|
+
direction: "before" | "after";
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
export interface TransferPlan {
|
|
@@ -69,6 +71,7 @@ interface PairFileCommit {
|
|
|
69
71
|
absolutePath: string;
|
|
70
72
|
mutationTargetPath: string;
|
|
71
73
|
foldedAnchorLines: number;
|
|
74
|
+
anchorCarry?: number;
|
|
72
75
|
endingOverrides?: (LineEnding | undefined)[];
|
|
73
76
|
}
|
|
74
77
|
|
|
@@ -87,12 +90,13 @@ export function parseTransferAnchors(req: TransferReq): { refs: TransferRefs; wa
|
|
|
87
90
|
const warnings: string[] = [];
|
|
88
91
|
const from = stripAnchorRow(req.source_from.trim(), "source_from entry", warnings);
|
|
89
92
|
const to = stripAnchorRow(req.source_to.trim(), "source_to entry", warnings);
|
|
90
|
-
const
|
|
93
|
+
const destination = stripAnchorRow(req.anchor.trim(), "anchor entry", warnings);
|
|
91
94
|
return {
|
|
92
95
|
refs: {
|
|
93
96
|
sourceFrom: parseHashRef(from),
|
|
94
97
|
sourceTo: parseHashRef(to),
|
|
95
|
-
|
|
98
|
+
anchor: parseHashRef(destination),
|
|
99
|
+
direction: req.direction,
|
|
96
100
|
},
|
|
97
101
|
warnings,
|
|
98
102
|
};
|
|
@@ -118,13 +122,14 @@ export function buildTransferEdit(input: {
|
|
|
118
122
|
const toLine = resolveAnchorLine(refs.sourceTo, fileLines, preload.fileHashes, displayPath);
|
|
119
123
|
const sourceStart = Math.min(fromLine, toLine);
|
|
120
124
|
const sourceEnd = Math.max(fromLine, toLine);
|
|
121
|
-
const
|
|
125
|
+
const anchorLine = resolveAnchorLine(refs.anchor, fileLines, preload.fileHashes, displayPath);
|
|
126
|
+
const insertLine = refs.direction === "after" ? anchorLine : anchorLine - 1;
|
|
122
127
|
if (insertLine >= sourceStart && insertLine < sourceEnd) {
|
|
123
128
|
throw new Error(
|
|
124
|
-
`[E_BAD_SHAPE] "
|
|
129
|
+
`[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
130
|
);
|
|
126
131
|
}
|
|
127
|
-
assertBoundaryLinesServed(fileLines, preload.fileHashes, served,
|
|
132
|
+
assertBoundaryLinesServed(fileLines, preload.fileHashes, served, anchorLine, anchorLine, displayPath);
|
|
128
133
|
assertBoundaryLinesServed(fileLines, preload.fileHashes, served, sourceStart, sourceStart, displayPath);
|
|
129
134
|
assertBoundaryLinesServed(fileLines, preload.fileHashes, served, sourceEnd, sourceEnd, displayPath);
|
|
130
135
|
const sourceLines = fileLines.slice(sourceStart - 1, sourceEnd);
|
|
@@ -132,13 +137,17 @@ export function buildTransferEdit(input: {
|
|
|
132
137
|
if (kind === "copy") {
|
|
133
138
|
return {
|
|
134
139
|
editParams: {
|
|
135
|
-
remove_from: preload.fileHashes[
|
|
136
|
-
remove_to: preload.fileHashes[
|
|
137
|
-
text:
|
|
140
|
+
remove_from: preload.fileHashes[anchorLine - 1]!,
|
|
141
|
+
remove_to: preload.fileHashes[anchorLine - 1]!,
|
|
142
|
+
text: refs.direction === "after"
|
|
143
|
+
? [fileLines[anchorLine - 1]!, ...sourceLines]
|
|
144
|
+
: [...sourceLines, fileLines[anchorLine - 1]!],
|
|
138
145
|
},
|
|
139
146
|
foldedAnchorLines: 1,
|
|
140
|
-
anchorCarry: 0,
|
|
141
|
-
endingOverrides:
|
|
147
|
+
anchorCarry: refs.direction === "after" ? 0 : sourceLines.length,
|
|
148
|
+
endingOverrides: refs.direction === "after"
|
|
149
|
+
? [undefined, ...sourceEndings]
|
|
150
|
+
: [...sourceEndings, undefined],
|
|
142
151
|
};
|
|
143
152
|
}
|
|
144
153
|
if (insertLine < sourceStart) {
|
|
@@ -169,40 +178,34 @@ export function buildTransferEdit(input: {
|
|
|
169
178
|
};
|
|
170
179
|
}
|
|
171
180
|
|
|
181
|
+
function describeTransferTargets(sourcePath: string, destinationPath: string): string {
|
|
182
|
+
return sourcePath === destinationPath
|
|
183
|
+
? `the file "${sourcePath}"`
|
|
184
|
+
: `the source file "${sourcePath}" or the destination file "${destinationPath}"`;
|
|
185
|
+
}
|
|
186
|
+
|
|
172
187
|
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
188
|
const sourcePath = resolveEditTarget(req.source_from, req.source_to);
|
|
181
|
-
const destinationPath = resolveEditTarget(req.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
189
|
+
const destinationPath = resolveEditTarget(req.anchor);
|
|
190
|
+
await assertPathOption({
|
|
191
|
+
providedPath: req.path,
|
|
192
|
+
cwd,
|
|
193
|
+
targets: [sourcePath, destinationPath],
|
|
194
|
+
expected: describeTransferTargets(sourcePath, destinationPath),
|
|
195
|
+
requestLabel: "Copy/move request",
|
|
196
|
+
});
|
|
188
197
|
return { sourcePath, destinationPath };
|
|
189
198
|
}
|
|
190
199
|
|
|
191
200
|
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
|
-
}
|
|
201
|
+
const sourceTarget = member.sourceTarget ?? member.target;
|
|
202
|
+
await assertPathOption({
|
|
203
|
+
providedPath: req.path,
|
|
204
|
+
cwd,
|
|
205
|
+
targets: [sourceTarget, member.target],
|
|
206
|
+
expected: describeTransferTargets(sourceTarget, member.target),
|
|
207
|
+
requestLabel: "Copy/move request",
|
|
208
|
+
});
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
interface CrossTransferPreparation {
|
|
@@ -212,6 +215,7 @@ interface CrossTransferPreparation {
|
|
|
212
215
|
destinationDisplay: string;
|
|
213
216
|
destinationEdit: HTEdit;
|
|
214
217
|
destinationFolded: number;
|
|
218
|
+
anchorCarry?: number;
|
|
215
219
|
endingOverrides: (LineEnding | undefined)[];
|
|
216
220
|
sourceEdit?: HTEdit;
|
|
217
221
|
}
|
|
@@ -268,15 +272,15 @@ async function prepareCrossTransfer(input: {
|
|
|
268
272
|
throw error;
|
|
269
273
|
}
|
|
270
274
|
|
|
271
|
-
let
|
|
275
|
+
let anchorLine = 0;
|
|
272
276
|
try {
|
|
273
|
-
|
|
277
|
+
anchorLine = resolveAnchorLine(refs.anchor, destinationLines, destinationPreload.fileHashes, destinationDisplay);
|
|
274
278
|
} catch (error) {
|
|
275
279
|
await adopt(destinationPreload.absolutePath, error);
|
|
276
280
|
throw error;
|
|
277
281
|
}
|
|
278
282
|
try {
|
|
279
|
-
assertBoundaryLinesServed(destinationLines, destinationPreload.fileHashes, destinationServed,
|
|
283
|
+
assertBoundaryLinesServed(destinationLines, destinationPreload.fileHashes, destinationServed, anchorLine, anchorLine, destinationDisplay);
|
|
280
284
|
} catch (error) {
|
|
281
285
|
await adopt(destinationPreload.absolutePath, error);
|
|
282
286
|
throw error;
|
|
@@ -289,23 +293,33 @@ async function prepareCrossTransfer(input: {
|
|
|
289
293
|
remove_to: destinationPreload.fileHashes[0]!,
|
|
290
294
|
text: [...moved],
|
|
291
295
|
}
|
|
292
|
-
:
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
296
|
+
: refs.direction === "after"
|
|
297
|
+
? {
|
|
298
|
+
remove_from: destinationPreload.fileHashes[anchorLine - 1]!,
|
|
299
|
+
remove_to: destinationPreload.fileHashes[anchorLine - 1]!,
|
|
300
|
+
text: [destinationLines[anchorLine - 1]!, ...moved],
|
|
301
|
+
}
|
|
302
|
+
: {
|
|
303
|
+
remove_from: destinationPreload.fileHashes[anchorLine - 1]!,
|
|
304
|
+
remove_to: destinationPreload.fileHashes[anchorLine - 1]!,
|
|
305
|
+
text: [...moved, destinationLines[anchorLine - 1]!],
|
|
306
|
+
};
|
|
297
307
|
const destinationFolded = destinationPreload.normalized.length === 0 ? 0 : 1;
|
|
298
308
|
const sourceEndings = endingsForRange(sourcePreload.endingSeparators, sourceStart, sourceEnd);
|
|
299
|
-
const endingOverrides = destinationFolded ===
|
|
309
|
+
const endingOverrides = destinationFolded === 0
|
|
310
|
+
? sourceEndings
|
|
311
|
+
: refs.direction === "after"
|
|
312
|
+
? offsetEndings(1, sourceEndings)
|
|
313
|
+
: sourceEndings;
|
|
300
314
|
if (kind === "copy") {
|
|
301
|
-
return { sourcePreload, destinationPreload, sourceDisplay, destinationDisplay, destinationEdit, destinationFolded, endingOverrides };
|
|
315
|
+
return { sourcePreload, destinationPreload, sourceDisplay, destinationDisplay, destinationEdit, destinationFolded, ...(destinationFolded === 1 ? { anchorCarry: refs.direction === "after" ? 0 : moved.length } : {}), endingOverrides };
|
|
302
316
|
}
|
|
303
317
|
const sourceEdit: HTEdit = {
|
|
304
318
|
remove_from: sourcePreload.fileHashes[sourceStart - 1]!,
|
|
305
319
|
remove_to: sourcePreload.fileHashes[sourceEnd - 1]!,
|
|
306
320
|
text: [],
|
|
307
321
|
};
|
|
308
|
-
return { sourcePreload, destinationPreload, sourceDisplay, destinationDisplay, destinationEdit, destinationFolded, sourceEdit, endingOverrides };
|
|
322
|
+
return { sourcePreload, destinationPreload, sourceDisplay, destinationDisplay, destinationEdit, destinationFolded, ...(destinationFolded === 1 ? { anchorCarry: refs.direction === "after" ? 0 : moved.length } : {}), sourceEdit, endingOverrides };
|
|
309
323
|
}
|
|
310
324
|
|
|
311
325
|
function pairEntry(pipe: PipelineResult, separators: LineEnding[] | undefined): UndoEntry {
|
|
@@ -333,6 +347,7 @@ async function commitMovePair(input: {
|
|
|
333
347
|
]);
|
|
334
348
|
const destinationSpan = input.destination.pipe.spans?.[0];
|
|
335
349
|
const sourceSpan = input.source.pipe.spans?.[0];
|
|
350
|
+
if (destinationSpan && input.destination.anchorCarry !== undefined) destinationSpan.carry = input.destination.anchorCarry;
|
|
336
351
|
const destinationSeparators = destinationSpan
|
|
337
352
|
? 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
353
|
: undefined;
|
|
@@ -558,7 +573,7 @@ async function executeCrossFile(
|
|
|
558
573
|
absolutePath: destination.absolute,
|
|
559
574
|
mutationTargetPath: destination.resolved,
|
|
560
575
|
editAnchors: [prepared.destinationEdit.remove_from, prepared.destinationEdit.remove_to],
|
|
561
|
-
...(prepared.
|
|
576
|
+
...(prepared.anchorCarry !== undefined ? { anchorCarry: prepared.anchorCarry } : {}),
|
|
562
577
|
endingOverrides: prepared.endingOverrides,
|
|
563
578
|
signal,
|
|
564
579
|
verb: "copied",
|
|
@@ -581,6 +596,7 @@ async function executeCrossFile(
|
|
|
581
596
|
absolutePath: destination.absolute,
|
|
582
597
|
mutationTargetPath: destination.resolved,
|
|
583
598
|
foldedAnchorLines: prepared.destinationFolded,
|
|
599
|
+
...(prepared.anchorCarry !== undefined ? { anchorCarry: prepared.anchorCarry } : {}),
|
|
584
600
|
endingOverrides: prepared.endingOverrides,
|
|
585
601
|
},
|
|
586
602
|
signal,
|
|
@@ -624,6 +640,7 @@ function planTransferBatchMember(kind: TransferKind): BatchMemberPlanner {
|
|
|
624
640
|
warnings: [...warnings, ...resWarnings],
|
|
625
641
|
planned,
|
|
626
642
|
foldedLines: transferPlan.foldedAnchorLines,
|
|
643
|
+
direction: refs.direction,
|
|
627
644
|
...(transferPlan.anchorCarry !== undefined ? { carryIndex: transferPlan.anchorCarry } : {}),
|
|
628
645
|
...(transferPlan.servedOverride !== undefined ? { servedOverride: transferPlan.servedOverride } : {}),
|
|
629
646
|
...(transferPlan.endingOverrides !== undefined ? { contentSeparators: transferPlan.endingOverrides } : {}),
|
|
@@ -686,7 +703,7 @@ async function executeBatchCrossCopy(
|
|
|
686
703
|
hedit,
|
|
687
704
|
warnings: [...warnings, ...resWarnings],
|
|
688
705
|
foldedLines: prepared.destinationFolded,
|
|
689
|
-
...(prepared.
|
|
706
|
+
...(prepared.anchorCarry !== undefined ? { carryIndex: prepared.anchorCarry } : {}),
|
|
690
707
|
contentSeparators: prepared.endingOverrides,
|
|
691
708
|
},
|
|
692
709
|
});
|
|
@@ -751,7 +768,7 @@ async function executeBatchCrossMove(
|
|
|
751
768
|
hedit,
|
|
752
769
|
warnings: [...warnings, ...resWarnings, ...sourceWarnings],
|
|
753
770
|
foldedLines: prepared.destinationFolded,
|
|
754
|
-
...(prepared.
|
|
771
|
+
...(prepared.anchorCarry !== undefined ? { carryIndex: prepared.anchorCarry } : {}),
|
|
755
772
|
contentSeparators: prepared.endingOverrides,
|
|
756
773
|
sourceMove: {
|
|
757
774
|
displayPath: prepared.sourceDisplay,
|
|
@@ -825,7 +842,7 @@ export async function transferPreview(kind: TransferKind, request: unknown, cwd:
|
|
|
825
842
|
}
|
|
826
843
|
}
|
|
827
844
|
|
|
828
|
-
function getTransferInput(args: unknown): { path?: string; source_from?: string; source_to?: string;
|
|
845
|
+
function getTransferInput(args: unknown): { path?: string; source_from?: string; source_to?: string; anchor?: string; direction?: string } | null {
|
|
829
846
|
let normalized: unknown;
|
|
830
847
|
try {
|
|
831
848
|
normalized = normReq(args);
|
|
@@ -836,7 +853,8 @@ function getTransferInput(args: unknown): { path?: string; source_from?: string;
|
|
|
836
853
|
if (
|
|
837
854
|
typeof normalized.source_from !== "string" ||
|
|
838
855
|
typeof normalized.source_to !== "string" ||
|
|
839
|
-
typeof normalized.
|
|
856
|
+
typeof normalized.anchor !== "string" ||
|
|
857
|
+
typeof normalized.direction !== "string"
|
|
840
858
|
) {
|
|
841
859
|
return null;
|
|
842
860
|
}
|
|
@@ -844,22 +862,27 @@ function getTransferInput(args: unknown): { path?: string; source_from?: string;
|
|
|
844
862
|
...(typeof normalized.path === "string" ? { path: normalized.path } : {}),
|
|
845
863
|
source_from: normalized.source_from,
|
|
846
864
|
source_to: normalized.source_to,
|
|
847
|
-
|
|
865
|
+
anchor: normalized.anchor,
|
|
866
|
+
direction: normalized.direction,
|
|
848
867
|
};
|
|
849
868
|
}
|
|
850
869
|
|
|
851
870
|
const transferSourceFromSchema = Type.String({
|
|
852
871
|
description:
|
|
853
|
-
"4-char anchor of the FIRST source line to copy
|
|
872
|
+
"4-char anchor of the FIRST source line to copy.",
|
|
854
873
|
});
|
|
855
874
|
const transferSourceToSchema = Type.String({
|
|
856
875
|
description:
|
|
857
876
|
"4-char anchor of the LAST source line to copy.",
|
|
858
877
|
});
|
|
859
|
-
const
|
|
878
|
+
const transferAnchorSchema = Type.String({
|
|
860
879
|
description:
|
|
861
|
-
"4-char anchor of the destination line; the block
|
|
880
|
+
"4-char anchor of the destination line; the block lands before or after it and it may live in another file.",
|
|
862
881
|
});
|
|
882
|
+
const transferDirectionSchema = Type.Union(
|
|
883
|
+
[Type.Literal("after"), Type.Literal("before")],
|
|
884
|
+
{ description: '"after" places the block below the anchor line, "before" above it' },
|
|
885
|
+
);
|
|
863
886
|
const transferPathRequiredSchema = Type.String({
|
|
864
887
|
description:
|
|
865
888
|
"Path to the source or destination file the anchors were served for; required and must match anchor ownership.",
|
|
@@ -869,7 +892,8 @@ const transferToolSchema = Type.Object(
|
|
|
869
892
|
{
|
|
870
893
|
source_from: transferSourceFromSchema,
|
|
871
894
|
source_to: transferSourceToSchema,
|
|
872
|
-
|
|
895
|
+
anchor: transferAnchorSchema,
|
|
896
|
+
direction: transferDirectionSchema,
|
|
873
897
|
},
|
|
874
898
|
{ additionalProperties: true },
|
|
875
899
|
);
|
|
@@ -881,7 +905,8 @@ export function buildTransferToolSchema(requirePath: boolean): typeof transferTo
|
|
|
881
905
|
path: transferPathRequiredSchema,
|
|
882
906
|
source_from: transferSourceFromSchema,
|
|
883
907
|
source_to: transferSourceToSchema,
|
|
884
|
-
|
|
908
|
+
anchor: transferAnchorSchema,
|
|
909
|
+
direction: transferDirectionSchema,
|
|
885
910
|
},
|
|
886
911
|
{ additionalProperties: true },
|
|
887
912
|
) as typeof transferToolSchema;
|
|
@@ -895,8 +920,8 @@ export function buildTransferToolDef(kind: TransferKind, flags: EditToolFlags =
|
|
|
895
920
|
description: loadP(`../tool-prompts/${kind}.md`),
|
|
896
921
|
snippet: loadP(`../tool-prompts/${kind}-snippet.md`),
|
|
897
922
|
guidelines: [
|
|
898
|
-
...
|
|
899
|
-
...
|
|
923
|
+
...loadGuidelines(`../tool-prompts/${kind}-guidelines.md`),
|
|
924
|
+
...loadGuidelines("../tool-prompts/transfer-guidelines.md"),
|
|
900
925
|
],
|
|
901
926
|
},
|
|
902
927
|
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
|
|