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/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 or inserts nothing.`;
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 ReqParams, type ReplaceDetails } from "./replace";
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: ReqParams;
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, sourceEnd, served, displayPath);
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: ReqParams;
229
+ destinationEdit: HTEdit;
228
230
  destinationFolded: number;
229
231
  endingOverrides: (LineEnding | undefined)[];
230
- sourceEdit?: ReqParams;
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, sourceEnd, sourceServed, sourceDisplay);
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: ReqParams = destinationPreload.normalized.length === 0
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: ReqParams = {
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
- "Bare 4-char anchor from a served anchor│content row (the text before the `│` separator), never the row content. Marks the FIRST source line (inclusive); this anchor and `source_to` resolve the source file.",
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
- "Bare 4-char anchor from a served anchor│content row (the text before the `│` separator), never the row content. Marks the LAST source line (inclusive); this anchor and `source_from` resolve the source file.",
683
+ "4-char anchor of the LAST source line to copy.",
679
684
  });
680
685
  const transferInsertAfterSchema = Type.String({
681
686
  description:
682
- "Bare 4-char anchor of the destination line after which the block goes; it may live in another file than the source, and the destination file is the one this anchor belongs to. The anchor line is preserved.",
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. The anchors still resolve both files.",
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,
@@ -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
- if (flags.autoRead) return { description: base.description, snippet: base.snippet, guidelines: [...base.guidelines] };
95
- const guidelines = base.guidelines.map((guideline) => guideline.includes("bad diff") ? "`undo_last_change`: only the last `replace`/`insert` per file is undoable; a `write` clears it, so undo right after a bad edit — review what you're restoring." : guideline);
96
- return { description: base.description, snippet: base.snippet, guidelines };
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, toolName: "copy" | "move"): { description: string; snippet: string; guidelines: string[] } {
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
  }