pi-hashline-edit-pro 4.5.3 → 5.1.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 +55 -39
- package/index.ts +13 -1
- package/package.json +1 -1
- package/prompts/copy-guidelines.md +1 -0
- package/prompts/insert-guidelines.md +1 -1
- package/prompts/insert-snippet.md +1 -1
- package/prompts/insert.md +2 -2
- package/prompts/move-guidelines.md +2 -1
- package/prompts/replace-guidelines.md +4 -4
- package/prompts/replace-snippet.md +1 -1
- package/prompts/replace-within-guidelines.md +3 -0
- package/prompts/replace-within-snippet.md +1 -0
- package/prompts/replace-within.md +5 -0
- package/prompts/replace.md +4 -3
- package/prompts/undo-last-change-guidelines.md +1 -1
- package/prompts/undo-last-change-snippet.md +1 -1
- package/prompts/undo-last-change.md +1 -1
- package/src/batch.ts +16 -8
- package/src/config-ui.ts +2 -1
- package/src/config.ts +6 -1
- package/src/constants.ts +7 -1
- package/src/copy-move.ts +17 -13
- package/src/edit-common.ts +76 -19
- package/src/edit-fidelity.ts +517 -0
- package/src/grep.ts +6 -5
- package/src/hashline/index.ts +3 -0
- package/src/hashline/parse.ts +7 -0
- package/src/hashline/resolve.ts +88 -38
- package/src/insert.ts +17 -34
- package/src/payload-contract.ts +155 -28
- package/src/prompts.ts +12 -1
- package/src/replace-render.ts +5 -5
- package/src/replace-response.ts +6 -3
- package/src/replace-within.ts +224 -0
- package/src/replace.ts +20 -20
- package/src/utils.ts +38 -17
- package/prompts/grep-guidelines.md +0 -2
package/src/config-ui.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Key, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
|
|
|
2
2
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { readConfig, type Config } from "./config";
|
|
4
4
|
|
|
5
|
-
export type ConfigToggleKey = "autoRead" | "autoReadAll" | "autoReadAllIgnore" | "anchorGrepEnabled" | "copyMoveEnabled" | "requirePath" | "strictInput" | "diffContextLines";
|
|
5
|
+
export type ConfigToggleKey = "autoRead" | "autoReadAll" | "autoReadAllIgnore" | "anchorGrepEnabled" | "copyMoveEnabled" | "replaceWithinEnabled" | "requirePath" | "strictInput" | "diffContextLines";
|
|
6
6
|
|
|
7
7
|
export interface ConfigRow {
|
|
8
8
|
key: ConfigToggleKey;
|
|
@@ -26,6 +26,7 @@ export function configRows(config: Config): ConfigRow[] {
|
|
|
26
26
|
{ key: "copyMoveEnabled", label: "Copy/move", hint: "copy and move tools (both off while disabled)", enabled: config.copyMoveEnabled !== false },
|
|
27
27
|
{ key: "requirePath", label: "Require path", hint: "replace, insert, copy, move need path (RPC visibility)", enabled: config.requirePath === true },
|
|
28
28
|
{ key: "strictInput", label: "Strict input", hint: "Reject auto-fixable slips instead of warnings", enabled: config.strictInput === true },
|
|
29
|
+
{ key: "replaceWithinEnabled", label: "Replace within", hint: "replace_within tool (off while disabled)", enabled: config.replaceWithinEnabled !== false },
|
|
29
30
|
];
|
|
30
31
|
}
|
|
31
32
|
|
package/src/config.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface Config {
|
|
|
14
14
|
autoRead: boolean;
|
|
15
15
|
anchorGrepEnabled: boolean;
|
|
16
16
|
copyMoveEnabled?: boolean;
|
|
17
|
+
replaceWithinEnabled?: boolean;
|
|
17
18
|
autoReadAll?: AutoReadAllMode;
|
|
18
19
|
autoReadAllIgnore?: string[];
|
|
19
20
|
requirePath?: boolean;
|
|
@@ -25,6 +26,7 @@ const DEFAULT_CONFIG: Config = {
|
|
|
25
26
|
autoRead: true,
|
|
26
27
|
anchorGrepEnabled: true,
|
|
27
28
|
copyMoveEnabled: true,
|
|
29
|
+
replaceWithinEnabled: true,
|
|
28
30
|
autoReadAll: "off",
|
|
29
31
|
autoReadAllIgnore: [],
|
|
30
32
|
requirePath: false,
|
|
@@ -75,6 +77,7 @@ function parseConfig(content: string): Config {
|
|
|
75
77
|
const autoRead = parsed.autoRead;
|
|
76
78
|
const anchorGrepEnabled = parsed.anchorGrepEnabled;
|
|
77
79
|
const copyMoveEnabled = parsed.copyMoveEnabled;
|
|
80
|
+
const replaceWithinEnabled = parsed.replaceWithinEnabled;
|
|
78
81
|
const autoReadAll = parsed.autoReadAll;
|
|
79
82
|
const requirePath = parsed.requirePath;
|
|
80
83
|
const strictInput = parsed.strictInput;
|
|
@@ -84,6 +87,7 @@ function parseConfig(content: string): Config {
|
|
|
84
87
|
autoRead: typeof autoRead === "boolean" ? autoRead : DEFAULT_CONFIG.autoRead,
|
|
85
88
|
anchorGrepEnabled: typeof anchorGrepEnabled === "boolean" ? anchorGrepEnabled : DEFAULT_CONFIG.anchorGrepEnabled,
|
|
86
89
|
copyMoveEnabled: typeof copyMoveEnabled === "boolean" ? copyMoveEnabled : DEFAULT_CONFIG.copyMoveEnabled,
|
|
90
|
+
replaceWithinEnabled: typeof replaceWithinEnabled === "boolean" ? replaceWithinEnabled : DEFAULT_CONFIG.replaceWithinEnabled,
|
|
87
91
|
autoReadAll: parseAutoReadAllMode(autoReadAll),
|
|
88
92
|
requirePath: typeof requirePath === "boolean" ? requirePath : DEFAULT_CONFIG.requirePath,
|
|
89
93
|
strictInput: typeof strictInput === "boolean" ? strictInput : DEFAULT_CONFIG.strictInput,
|
|
@@ -196,7 +200,7 @@ export async function writeConfig(config: Config): Promise<void> {
|
|
|
196
200
|
}
|
|
197
201
|
|
|
198
202
|
|
|
199
|
-
type ToggleKey = "autoRead" | "anchorGrepEnabled" | "copyMoveEnabled" | "requirePath" | "strictInput";
|
|
203
|
+
type ToggleKey = "autoRead" | "anchorGrepEnabled" | "copyMoveEnabled" | "replaceWithinEnabled" | "requirePath" | "strictInput";
|
|
200
204
|
|
|
201
205
|
async function toggleFlag(key: ToggleKey): Promise<boolean> {
|
|
202
206
|
const config = await updateConfig((c) => { c[key] = !(c[key] === true); });
|
|
@@ -205,6 +209,7 @@ async function toggleFlag(key: ToggleKey): Promise<boolean> {
|
|
|
205
209
|
export const toggleAutoRead = (): Promise<boolean> => toggleFlag("autoRead");
|
|
206
210
|
export const toggleAnchorGrep = (): Promise<boolean> => toggleFlag("anchorGrepEnabled");
|
|
207
211
|
export const toggleCopyMove = (): Promise<boolean> => toggleFlag("copyMoveEnabled");
|
|
212
|
+
export const toggleReplaceWithin = (): Promise<boolean> => toggleFlag("replaceWithinEnabled");
|
|
208
213
|
export async function cycleAutoReadAllMode(): Promise<AutoReadAllMode> {
|
|
209
214
|
let next: AutoReadAllMode = "off";
|
|
210
215
|
await updateConfig((c) => {
|
package/src/constants.ts
CHANGED
|
@@ -13,8 +13,14 @@ export const HASH_STORE_VERSION = 9;
|
|
|
13
13
|
export const NEW_CONTENT_NOT_ARRAY_MSG =
|
|
14
14
|
`[E_BAD_SHAPE] "replacement_lines" must be an array of strings, one per line (use [] to delete).`;
|
|
15
15
|
|
|
16
|
+
export const NEW_CONTENT_NOT_STRING_MSG =
|
|
17
|
+
`[E_BAD_SHAPE] "replacement_lines" must be a string holding the exact text to write. Use "" to delete the range and "\\n" for one blank line; line breaks inside the string separate lines.`;
|
|
18
|
+
|
|
19
|
+
export const LINES_NOT_STRING_MSG =
|
|
20
|
+
`[E_BAD_SHAPE] "lines" must be a string holding the exact text to insert. Use "\\n" for one blank line; line breaks inside the string separate lines.`;
|
|
21
|
+
|
|
16
22
|
export const NUL_CONTENT_MSG =
|
|
17
|
-
`[E_BAD_SHAPE] Content contains a NUL byte (U+0000); a text file cannot contain NUL, and writing it would break further reads and edits. Remove the NUL byte and retry. An empty replacement ([]) deletes a range
|
|
23
|
+
`[E_BAD_SHAPE] Content contains a NUL byte (U+0000); a text file cannot contain NUL, and writing it would break further reads and edits. Remove the NUL byte and retry. An empty replacement ([]) deletes a range.`;
|
|
18
24
|
|
|
19
25
|
export const ANCHOR_POOL_EXHAUSTED_PREFIX =
|
|
20
26
|
"[E_FILE_TOO_LARGE] The session's anchor pool is exhausted";
|
package/src/copy-move.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-age
|
|
|
2
2
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { constants } from "node:fs";
|
|
5
|
-
import { execPipeline, noteAnchorError, previewFromPipe, previewError, type PipelineResult, type
|
|
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
8
|
import {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
resolveAnchorLine,
|
|
15
15
|
stripAnchorRow,
|
|
16
16
|
type Anchor,
|
|
17
|
+
type HTEdit,
|
|
17
18
|
} from "./hashline";
|
|
18
19
|
import { formatAnchorReclaimNotice, servedForPath, takeReclaimedPaths, withAnchorSession } from "./anchor-registry";
|
|
19
20
|
import { loadP, loadGuide } from "./prompts";
|
|
@@ -49,7 +50,7 @@ interface TransferRefs {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export interface TransferPlan {
|
|
52
|
-
editParams:
|
|
53
|
+
editParams: HTEdit;
|
|
53
54
|
foldedAnchorLines: number;
|
|
54
55
|
anchorCarry?: number;
|
|
55
56
|
servedOverride?: ReadonlyMap<string, string>;
|
|
@@ -157,7 +158,8 @@ export function buildTransferEdit(input: {
|
|
|
157
158
|
);
|
|
158
159
|
}
|
|
159
160
|
assertRangeVerified(fileLines, preload.fileHashes, insertLine, insertLine, served, displayPath);
|
|
160
|
-
assertRangeVerified(fileLines, preload.fileHashes, sourceStart,
|
|
161
|
+
assertRangeVerified(fileLines, preload.fileHashes, sourceStart, sourceStart, served, displayPath);
|
|
162
|
+
assertRangeVerified(fileLines, preload.fileHashes, sourceEnd, sourceEnd, served, displayPath);
|
|
161
163
|
const sourceLines = fileLines.slice(sourceStart - 1, sourceEnd);
|
|
162
164
|
const sourceEndings = endingsForRange(preload.endingSeparators, sourceStart, sourceEnd);
|
|
163
165
|
if (kind === "copy") {
|
|
@@ -224,10 +226,10 @@ interface CrossTransferPreparation {
|
|
|
224
226
|
destinationPreload: NormFile;
|
|
225
227
|
sourceDisplay: string;
|
|
226
228
|
destinationDisplay: string;
|
|
227
|
-
destinationEdit:
|
|
229
|
+
destinationEdit: HTEdit;
|
|
228
230
|
destinationFolded: number;
|
|
229
231
|
endingOverrides: (LineEnding | undefined)[];
|
|
230
|
-
sourceEdit?:
|
|
232
|
+
sourceEdit?: HTEdit;
|
|
231
233
|
}
|
|
232
234
|
|
|
233
235
|
async function prepareCrossTransfer(input: {
|
|
@@ -271,7 +273,8 @@ async function prepareCrossTransfer(input: {
|
|
|
271
273
|
throw error;
|
|
272
274
|
}
|
|
273
275
|
try {
|
|
274
|
-
assertRangeVerified(sourceLines, sourcePreload.fileHashes, sourceStart,
|
|
276
|
+
assertRangeVerified(sourceLines, sourcePreload.fileHashes, sourceStart, sourceStart, sourceServed, sourceDisplay);
|
|
277
|
+
assertRangeVerified(sourceLines, sourcePreload.fileHashes, sourceEnd, sourceEnd, sourceServed, sourceDisplay);
|
|
275
278
|
} catch (error) {
|
|
276
279
|
await adopt(sourcePreload.absolutePath, error);
|
|
277
280
|
throw error;
|
|
@@ -292,7 +295,7 @@ async function prepareCrossTransfer(input: {
|
|
|
292
295
|
}
|
|
293
296
|
|
|
294
297
|
const moved = sourceLines.slice(sourceStart - 1, sourceEnd);
|
|
295
|
-
const destinationEdit:
|
|
298
|
+
const destinationEdit: HTEdit = destinationPreload.normalized.length === 0
|
|
296
299
|
? {
|
|
297
300
|
remove_from: destinationPreload.fileHashes[0]!,
|
|
298
301
|
remove_to: destinationPreload.fileHashes[0]!,
|
|
@@ -309,7 +312,7 @@ async function prepareCrossTransfer(input: {
|
|
|
309
312
|
if (kind === "copy") {
|
|
310
313
|
return { sourcePreload, destinationPreload, sourceDisplay, destinationDisplay, destinationEdit, destinationFolded, endingOverrides };
|
|
311
314
|
}
|
|
312
|
-
const sourceEdit:
|
|
315
|
+
const sourceEdit: HTEdit = {
|
|
313
316
|
remove_from: sourcePreload.fileHashes[sourceStart - 1]!,
|
|
314
317
|
remove_to: sourcePreload.fileHashes[sourceEnd - 1]!,
|
|
315
318
|
replacement_lines: [],
|
|
@@ -566,6 +569,7 @@ async function executeCrossFile(
|
|
|
566
569
|
signal,
|
|
567
570
|
preloadedNorm: prepared.sourcePreload,
|
|
568
571
|
allowEmpty: true,
|
|
572
|
+
preserveDeletionSeparators: false,
|
|
569
573
|
});
|
|
570
574
|
return commitMovePair({
|
|
571
575
|
source: { pipe: sourcePipe, displayPath: prepared.sourceDisplay, absolutePath: source.absolute, mutationTargetPath: source.resolved, foldedAnchorLines: 0 },
|
|
@@ -630,6 +634,7 @@ export async function transferPreview(kind: TransferKind, request: unknown, cwd:
|
|
|
630
634
|
noPersist: true,
|
|
631
635
|
preloadedNorm: prepared.sourcePreload,
|
|
632
636
|
allowEmpty: true,
|
|
637
|
+
preserveDeletionSeparators: false,
|
|
633
638
|
signal,
|
|
634
639
|
});
|
|
635
640
|
const destinationPreview = previewFromPipe(destinationPipe);
|
|
@@ -671,19 +676,19 @@ function getTransferInput(args: unknown): { path?: string; source_from?: string;
|
|
|
671
676
|
|
|
672
677
|
const transferSourceFromSchema = Type.String({
|
|
673
678
|
description:
|
|
674
|
-
"
|
|
679
|
+
"4-char anchor of the FIRST source line to copy (never the row content).",
|
|
675
680
|
});
|
|
676
681
|
const transferSourceToSchema = Type.String({
|
|
677
682
|
description:
|
|
678
|
-
"
|
|
683
|
+
"4-char anchor of the LAST source line to copy.",
|
|
679
684
|
});
|
|
680
685
|
const transferInsertAfterSchema = Type.String({
|
|
681
686
|
description:
|
|
682
|
-
"
|
|
687
|
+
"4-char anchor of the destination line; the block goes after it and may live in another file.",
|
|
683
688
|
});
|
|
684
689
|
const transferPathRequiredSchema = Type.String({
|
|
685
690
|
description:
|
|
686
|
-
"Path to the source or destination file the anchors were served for; required and must match anchor ownership.
|
|
691
|
+
"Path to the source or destination file the anchors were served for; required and must match anchor ownership.",
|
|
687
692
|
});
|
|
688
693
|
|
|
689
694
|
const transferToolSchema = Type.Object(
|
|
@@ -718,7 +723,6 @@ export function buildTransferToolDef(kind: TransferKind, flags: EditToolFlags =
|
|
|
718
723
|
guidelines: loadGuide(`../prompts/${kind}-guidelines.md`),
|
|
719
724
|
},
|
|
720
725
|
flags,
|
|
721
|
-
kind,
|
|
722
726
|
);
|
|
723
727
|
return {
|
|
724
728
|
name: kind,
|
package/src/edit-common.ts
CHANGED
|
@@ -13,13 +13,17 @@ export interface EditToolFlags {
|
|
|
13
13
|
strictInput: boolean;
|
|
14
14
|
autoRead: boolean;
|
|
15
15
|
autoReadAllActive: boolean;
|
|
16
|
+
replaceWithinEnabled: boolean;
|
|
17
|
+
copyMoveEnabled: boolean;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export const DEFAULT_EDIT_FLAGS: EditToolFlags = {
|
|
19
21
|
requirePath: false,
|
|
20
22
|
strictInput: false,
|
|
21
23
|
autoRead: true,
|
|
22
|
-
autoReadAllActive: false
|
|
24
|
+
autoReadAllActive: false,
|
|
25
|
+
replaceWithinEnabled: true,
|
|
26
|
+
copyMoveEnabled: true
|
|
23
27
|
};
|
|
24
28
|
|
|
25
29
|
export async function currentEditFlags(): Promise<EditToolFlags> {
|
|
@@ -28,44 +32,52 @@ export async function currentEditFlags(): Promise<EditToolFlags> {
|
|
|
28
32
|
requirePath: config.requirePath === true,
|
|
29
33
|
strictInput: config.strictInput === true,
|
|
30
34
|
autoRead: config.autoRead !== false,
|
|
31
|
-
autoReadAllActive: (config.autoReadAll ?? "off") !== "off"
|
|
35
|
+
autoReadAllActive: (config.autoReadAll ?? "off") !== "off",
|
|
36
|
+
replaceWithinEnabled: config.replaceWithinEnabled !== false,
|
|
37
|
+
copyMoveEnabled: config.copyMoveEnabled !== false
|
|
32
38
|
};
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
function preferenceGuideline(flags: EditToolFlags): string {
|
|
42
|
+
const tools = gatedEditOps(["read", "replace", "replace_within", "insert", "copy", "move", "undo_last_change"], flags);
|
|
43
|
+
return `Prefer the hashline tools for anything that touches files: ${joinOps(tools, { backtick: true })}.`;
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
export function withReplacePrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
|
36
47
|
let description = base.description;
|
|
37
48
|
const snippetParts = [base.snippet];
|
|
38
|
-
let guidelines = [...base.guidelines];
|
|
49
|
+
let guidelines = [preferenceGuideline(flags), ...base.guidelines];
|
|
39
50
|
if (!flags.autoRead) {
|
|
40
51
|
description = description.replace(/\n\nExample:[\s\S]*$/, "");
|
|
41
52
|
guidelines = guidelines.filter((guideline) => !guideline.includes("post-edit diff"));
|
|
42
53
|
description = description.replace("and the last call shows the combined diff,", "and the last call shows the combined result,");
|
|
43
54
|
}
|
|
55
|
+
if (!flags.replaceWithinEnabled) {
|
|
56
|
+
description = description.replace(/\n?To change only part of a line without retyping the rest, use `replace_within` instead; it preserves every character the request does not name\./, "");
|
|
57
|
+
}
|
|
44
58
|
const descriptionParts = [description];
|
|
45
59
|
if (flags.requirePath) {
|
|
46
60
|
descriptionParts.push("Also give `path` matching the file the anchors were served for; it is required and must match anchor ownership.");
|
|
47
61
|
snippetParts.push("; include `path` (required)");
|
|
48
|
-
guidelines.push("`replace`: include `path` matching the file the anchors were served for; it is required.");
|
|
49
62
|
} else {
|
|
50
63
|
descriptionParts.push("Path resolution is anchor-only; do not pass `path`.");
|
|
51
|
-
guidelines.push("`replace`: path resolution is anchor-only; don't pass `path`.");
|
|
52
64
|
}
|
|
53
65
|
if (flags.strictInput) {
|
|
54
66
|
descriptionParts.push("Strict-input mode is on: auto-fixable slips are rejected instead of fixed with warnings.");
|
|
55
|
-
guidelines.push("`replace`: strict-input is on: auto-fixable slips are rejected instead of fixed.");
|
|
56
67
|
}
|
|
57
68
|
return { description: descriptionParts.join(" "), snippet: snippetParts.join(""), guidelines };
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
export function withReadPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
|
72
|
+
const preference = preferenceGuideline(flags);
|
|
61
73
|
if (flags.autoReadAllActive) {
|
|
62
74
|
const rewritten = base.guidelines
|
|
63
75
|
.filter((guideline) => !guideline.includes("call again after an edit"))
|
|
64
|
-
return { description: base.description, snippet: base.snippet, guidelines: [...rewritten] };
|
|
76
|
+
return { description: base.description, snippet: base.snippet, guidelines: [preference, ...rewritten] };
|
|
65
77
|
}
|
|
66
78
|
const withoutAutoReadAll = base.guidelines.filter((guideline) => !guideline.includes("E_AUTO_READ_ALL"))
|
|
67
|
-
if (flags.autoRead) return { description: base.description, snippet: base.snippet, guidelines: [...withoutAutoReadAll] };
|
|
68
|
-
const guidelines = [...withoutAutoReadAll];
|
|
79
|
+
if (flags.autoRead) return { description: base.description, snippet: base.snippet, guidelines: [preference, ...withoutAutoReadAll] };
|
|
80
|
+
const guidelines = [preference, ...withoutAutoReadAll];
|
|
69
81
|
const mapped = guidelines.map((guideline) => guideline.startsWith("`read`: call again after an edit") ? "`read`: call again after an edit when you need anchors you lack." : guideline);
|
|
70
82
|
return { description: base.description, snippet: base.snippet, guidelines: mapped };
|
|
71
83
|
}
|
|
@@ -78,39 +90,84 @@ export function withInsertPrompts(base: { description: string; snippet: string;
|
|
|
78
90
|
if (flags.requirePath) {
|
|
79
91
|
descriptionParts.push("Also give `path` matching the file the anchor was served for; it is required and must match anchor ownership.");
|
|
80
92
|
snippetParts.push("; include `path` (required)");
|
|
81
|
-
guidelines.push("`insert`: include `path` matching the file the anchor was served for; it is required.");
|
|
82
93
|
} else {
|
|
83
94
|
descriptionParts.push("Path resolution is anchor-only; do not pass `path`.");
|
|
84
|
-
guidelines.push("`insert`: path resolution is anchor-only; don't pass `path`.");
|
|
85
95
|
}
|
|
86
96
|
if (flags.strictInput) {
|
|
87
97
|
descriptionParts.push("Strict-input mode is on: auto-fixable slips are rejected instead of fixed with warnings.");
|
|
88
|
-
guidelines.push("`insert`: strict-input is on: auto-fixable slips are rejected instead of fixed.");
|
|
89
98
|
}
|
|
90
99
|
return { description: descriptionParts.join(" "), snippet: snippetParts.join(""), guidelines };
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
export function withReplaceWithinPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
|
103
|
+
const descriptionParts = [base.description];
|
|
104
|
+
const snippetParts = [base.snippet];
|
|
105
|
+
const guidelines = [...base.guidelines];
|
|
106
|
+
if (flags.requirePath) {
|
|
107
|
+
descriptionParts.push("Also give `path` matching the file the anchors were served for; it is required and must match anchor ownership.");
|
|
108
|
+
snippetParts.push("; include `path` (required)");
|
|
109
|
+
} else {
|
|
110
|
+
descriptionParts.push("Path resolution is anchor-only; do not pass `path`.");
|
|
111
|
+
}
|
|
112
|
+
if (flags.strictInput) {
|
|
113
|
+
descriptionParts.push("Strict-input mode is on: auto-fixable slips are rejected instead of fixed with warnings.");
|
|
114
|
+
}
|
|
115
|
+
return { description: descriptionParts.join(" "), snippet: snippetParts.join(""), guidelines };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function gatedEditOps(ops: string[], flags: EditToolFlags): string[] {
|
|
119
|
+
return ops.filter((op) => {
|
|
120
|
+
if (op === "replace_within") return flags.replaceWithinEnabled;
|
|
121
|
+
if (op === "copy" || op === "move") return flags.copyMoveEnabled;
|
|
122
|
+
return true;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function joinOps(ops: string[], options?: { backtick?: boolean; separator?: "/" }): string {
|
|
127
|
+
const formatted = ops.map((op) => (options?.backtick ? `\`${op}\`` : op));
|
|
128
|
+
if (options?.separator === "/") return formatted.join("/");
|
|
129
|
+
if (formatted.length <= 1) return formatted[0] ?? "";
|
|
130
|
+
const last = formatted[formatted.length - 1]!;
|
|
131
|
+
const head = formatted.slice(0, -1).join(", ");
|
|
132
|
+
return formatted.length === 2 ? `${head} or ${last}` : `${head}, or ${last}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function withGrepPrompts(base: { description: string; snippet: string }, flags: EditToolFlags): { description: string; snippet: string } {
|
|
136
|
+
if (flags.copyMoveEnabled) return base;
|
|
137
|
+
return { ...base, description: base.description.replaceAll("replace, insert, copy, or move", joinOps(gatedEditOps(["replace", "insert", "copy", "move"], flags))) };
|
|
138
|
+
}
|
|
139
|
+
|
|
93
140
|
export function withUndoPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
141
|
+
const ops = gatedEditOps(["replace", "replace_within", "insert", "copy", "move"], flags);
|
|
142
|
+
let description = base.description;
|
|
143
|
+
let snippet = base.snippet;
|
|
144
|
+
let guidelines = [...base.guidelines];
|
|
145
|
+
if (!flags.autoRead) {
|
|
146
|
+
guidelines = guidelines.map((guideline) => guideline.includes("bad diff") ? "`undo_last_change`: only the last `replace`/`replace_within`/`insert`/`copy`/`move` per file is undoable; a `write` clears it, so undo right after a bad edit — review what you're restoring." : guideline);
|
|
147
|
+
}
|
|
148
|
+
if (ops.length !== 5) {
|
|
149
|
+
description = description.replaceAll("replace, replace_within, insert, copy, or move", joinOps(ops));
|
|
150
|
+
snippet = snippet.replaceAll("`replace`, `replace_within`, `insert`, `copy`, or `move`", joinOps(ops, { backtick: true }));
|
|
151
|
+
guidelines = guidelines.map((guideline) => guideline.replaceAll("`replace`/`replace_within`/`insert`/`copy`/`move`", joinOps(ops, { backtick: true, separator: "/" })));
|
|
152
|
+
}
|
|
153
|
+
if (!flags.copyMoveEnabled) {
|
|
154
|
+
guidelines = guidelines.filter((guideline) => !guideline.includes("cross-file `move`"));
|
|
155
|
+
}
|
|
156
|
+
return { description, snippet, guidelines };
|
|
97
157
|
}
|
|
98
158
|
|
|
99
|
-
export function withTransferPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags
|
|
159
|
+
export function withTransferPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
|
100
160
|
const descriptionParts = [base.description];
|
|
101
161
|
const snippetParts = [base.snippet];
|
|
102
162
|
const guidelines = [...base.guidelines];
|
|
103
163
|
if (flags.requirePath) {
|
|
104
164
|
descriptionParts.push("Also give `path` matching the source or destination file the anchors were served for; it is required and must match anchor ownership.");
|
|
105
165
|
snippetParts.push("; include `path` (required)");
|
|
106
|
-
guidelines.push(`\`${toolName}\`: include \`path\` matching the source or destination file the anchors were served for; it is required.`);
|
|
107
166
|
} else {
|
|
108
167
|
descriptionParts.push("Path resolution is anchor-only; do not pass `path`.");
|
|
109
|
-
guidelines.push(`\`${toolName}\`: path resolution is anchor-only; don't pass \`path\`.`);
|
|
110
168
|
}
|
|
111
169
|
if (flags.strictInput) {
|
|
112
170
|
descriptionParts.push("Strict-input mode is on: auto-fixable slips are rejected instead of fixed with warnings.");
|
|
113
|
-
guidelines.push(`\`${toolName}\`: strict-input is on: auto-fixable slips are rejected instead of fixed.`);
|
|
114
171
|
}
|
|
115
172
|
return { description: descriptionParts.join(" "), snippet: snippetParts.join(""), guidelines };
|
|
116
173
|
}
|