pi-hashline-edit-pro 0.4.3 → 0.6.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.
@@ -40,7 +40,7 @@ export const DIFF_MINUS_RE = /^-\s*\d+\s{4}/;
40
40
 
41
41
  export const HASHLINE_BARE_PREFIX_RE = new RegExp(`^\\s*(${HASH_CHARS_CLASS})│`);
42
42
 
43
- const RE_SIGNIFICANT = /[\p{L}\p{N}]/u;
43
+
44
44
 
45
45
  // Lazy-initialized xxhash-wasm hasher. Initialization starts at module load
46
46
  // time and completes in ~2ms. By the time any tool calls xxh32(), the hasher
@@ -64,50 +64,35 @@ hasherPromise = xxhash().then((h) => {
64
64
 
65
65
  // Export for tests that need to await readiness.
66
66
  export function ensureHasherReady(): Promise<Hasher> {
67
- return hasherPromise!;
67
+ return hasherPromise!
68
68
  }
69
69
 
70
70
  function xxh32(input: string, seed = 0): number {
71
71
  return getHasher().h32(input, seed) >>> 0;
72
72
  }
73
73
 
74
- const SYMBOL_DISCRIMINATOR = (lineNumber: number): string => `S${lineNumber}`;
75
- const CONTENT_DISCRIMINATOR = (occurrence: number): string => `C${occurrence}`;
74
+ const DISCRIMINATOR = (occurrence: number): string => `C${occurrence}`;
76
75
 
77
76
  function canonicalizeLine(line: string): string {
78
77
  return line.replace(/\r/g, "").trimEnd();
79
78
  }
80
79
 
81
- function isSymbolOnly(canonical: string): boolean {
82
- return !RE_SIGNIFICANT.test(canonical);
83
- }
84
-
85
80
  export function computeLineHashes(content: string): string[] {
86
81
  const lines = content.split("\n");
87
82
  const hashes = new Array<string>(lines.length);
88
83
  const counts = new Map<string, number>();
89
84
  for (let i = 0; i < lines.length; i++) {
90
- const lineNumber = i + 1;
91
85
  const canonical = canonicalizeLine(lines[i]!);
92
- let discriminator: string;
93
- if (isSymbolOnly(canonical)) {
94
- discriminator = SYMBOL_DISCRIMINATOR(lineNumber);
95
- } else {
96
- const occurrence = (counts.get(canonical) ?? 0) + 1;
97
- counts.set(canonical, occurrence);
98
- discriminator = CONTENT_DISCRIMINATOR(occurrence);
99
- }
100
- hashes[i] = hashToString(xxh32(`${discriminator}:${canonical}`));
86
+ const occurrence = (counts.get(canonical) ?? 0) + 1;
87
+ counts.set(canonical, occurrence);
88
+ hashes[i] = hashToString(xxh32(`${DISCRIMINATOR(occurrence)}:${canonical}`));
101
89
  }
102
90
  return hashes;
103
91
  }
104
92
 
105
93
  export function computeLineHash(idx: number, line: string): string {
106
94
  const canonical = canonicalizeLine(line);
107
- const discriminator = isSymbolOnly(canonical)
108
- ? SYMBOL_DISCRIMINATOR(idx)
109
- : CONTENT_DISCRIMINATOR(1);
110
- return hashToString(xxh32(`${discriminator}:${canonical}`));
95
+ return hashToString(xxh32(`${DISCRIMINATOR(1)}:${canonical}`));
111
96
  }
112
97
 
113
98
  export const HASH_FORMAT = {
@@ -10,21 +10,12 @@ export type ResolvedAnchor = {
10
10
  hashMatched: boolean;
11
11
  };
12
12
 
13
- export type HashlineEdit =
14
- | { op: "replace"; start: Anchor; end: Anchor; lines: string[] }
15
- | { op: "append"; pos?: Anchor; lines: string[] }
16
- | { op: "prepend"; pos?: Anchor; lines: string[] };
17
-
18
- export type ResolvedHashlineEdit =
19
- | {
20
- op: "replace";
21
- start: ResolvedAnchor;
22
- end: ResolvedAnchor;
23
- lines: string[];
24
- }
25
- | { op: "append"; pos?: ResolvedAnchor; lines: string[] }
26
- | { op: "prepend"; pos?: ResolvedAnchor; lines: string[] };
27
-
13
+ export type HashlineEdit = { start: Anchor; end: Anchor; lines: string[] };
14
+ export type ResolvedHashlineEdit = {
15
+ start: ResolvedAnchor;
16
+ end: ResolvedAnchor;
17
+ lines: string[];
18
+ };
28
19
  interface HashMismatch {
29
20
  ref: Anchor;
30
21
  kind: "not_found" | "ambiguous";
@@ -38,8 +29,6 @@ export interface NoopEdit {
38
29
  }
39
30
 
40
31
  export type HashlineToolEdit = {
41
- op: string;
42
- pos?: string;
43
32
  start?: string;
44
33
  end?: string;
45
34
  lines?: string[];
@@ -130,7 +119,7 @@ export function formatMismatchError(
130
119
  }
131
120
 
132
121
 
133
- const ITEM_KEYS = new Set(["op", "pos", "start", "end", "lines"]);
122
+ const ITEM_KEYS = new Set(["start", "end", "lines"]);
134
123
  function isStringArray(value: unknown): value is string[] {
135
124
  return (
136
125
  Array.isArray(value) && value.every((item) => typeof item === "string")
@@ -145,23 +134,6 @@ function assertEditItem(edit: Record<string, unknown>, index: number): void {
145
134
  );
146
135
  }
147
136
 
148
- if (typeof edit.op !== "string") {
149
- throw new Error(`[E_BAD_SHAPE] Edit ${index} requires an "op" string.`);
150
- }
151
- if (
152
- edit.op !== "replace" &&
153
- edit.op !== "append" &&
154
- edit.op !== "prepend"
155
- ) {
156
- throw new Error(
157
- `[E_BAD_OP] Edit ${index} uses unknown op "${edit.op}". Expected "replace", "append", or "prepend".`,
158
- );
159
- }
160
- if ("pos" in edit && typeof edit.pos !== "string" && edit.op !== "replace") {
161
- throw new Error(
162
- `[E_BAD_SHAPE] Edit ${index} field "pos" must be a string when provided.`,
163
- );
164
- }
165
137
  if ("start" in edit && typeof edit.start !== "string") {
166
138
  throw new Error(
167
139
  `[E_BAD_SHAPE] Edit ${index} field "start" must be a string when provided.`,
@@ -176,29 +148,17 @@ function assertEditItem(edit: Record<string, unknown>, index: number): void {
176
148
  if ("lines" in edit && !isStringArray(edit.lines)) {
177
149
  throw new Error(`[E_BAD_SHAPE] Edit ${index} field "lines" must be a string array.`);
178
150
  }
179
- if (edit.op === "replace") {
180
- if ("pos" in edit) {
181
- throw new Error(
182
- `[E_BAD_OP] Edit ${index} op "replace" uses "pos" — use "start" instead.`
183
- );
184
- }
185
- if (typeof edit.start !== "string") {
186
- throw new Error(
187
- `[E_BAD_OP] Edit ${index} with op "replace" requires a "start" anchor string.`,
188
- );
189
- }
190
- if (typeof edit.end !== "string") {
191
- throw new Error(
192
- `[E_BAD_OP] Edit ${index} with op "replace" requires an "end" anchor string.`,
193
- );
194
- }
151
+ if (typeof edit.start !== "string") {
152
+ throw new Error(
153
+ `[E_BAD_OP] Edit ${index} requires a "start" anchor string.`,
154
+ );
195
155
  }
196
-
197
- if ((edit.op === "append" || edit.op === "prepend") && "end" in edit) {
156
+ if (typeof edit.end !== "string") {
198
157
  throw new Error(
199
- `[E_BAD_OP] Edit ${index} op "${edit.op}" does not support "end". Use "pos" or omit.`
158
+ `[E_BAD_OP] Edit ${index} requires an "end" anchor string.`,
200
159
  );
201
160
  }
161
+
202
162
  }
203
163
 
204
164
  export function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
@@ -206,45 +166,21 @@ export function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
206
166
  for (const [index, edit] of edits.entries()) {
207
167
  assertEditItem(edit as Record<string, unknown>, index);
208
168
 
209
- const op = edit.op;
210
- switch (op) {
211
- case "replace": {
212
- // Normalize lines: [""] to lines: [] for deletion.
213
- const replaceLines = hashlineParseText(edit.lines ?? null);
214
- const normalizedLines =
215
- replaceLines.length === 1 && replaceLines[0] === ""
216
- ? []
217
- : replaceLines;
218
- result.push({
219
- op: "replace",
220
- start: parseHashRef(edit.start!),
221
- end: parseHashRef(edit.end!),
222
- lines: normalizedLines,
223
- });
224
- break;
225
- }
226
- case "append": {
227
- result.push({
228
- op: "append",
229
- ...(edit.pos ? { pos: parseHashRef(edit.pos) } : {}),
230
- lines: hashlineParseText(edit.lines ?? null),
231
- });
232
- break;
233
- }
234
- case "prepend": {
235
- result.push({
236
- op: "prepend",
237
- ...(edit.pos ? { pos: parseHashRef(edit.pos) } : {}),
238
- lines: hashlineParseText(edit.lines ?? null),
239
- });
240
- break;
241
- }
242
- }
169
+ // Normalize lines: [""] to lines: [] for deletion.
170
+ const replaceLines = hashlineParseText(edit.lines ?? null);
171
+ const normalizedLines =
172
+ replaceLines.length === 1 && replaceLines[0] === ""
173
+ ? []
174
+ : replaceLines;
175
+ result.push({
176
+ start: parseHashRef(edit.start!),
177
+ end: parseHashRef(edit.end!),
178
+ lines: normalizedLines,
179
+ });
243
180
  }
244
181
  return result;
245
182
  }
246
183
 
247
-
248
184
  function maybeWarnSuspiciousUnicodeEscapePlaceholder(
249
185
  edits: HashlineEdit[],
250
186
  warnings: string[],
@@ -308,14 +244,7 @@ export function assertNoBareHashPrefixLines(
308
244
  * Human-readable label for a resolved edit (used in warnings and conflict errors).
309
245
  */
310
246
  export function describeEdit(edit: ResolvedHashlineEdit): string {
311
- switch (edit.op) {
312
- case "replace":
313
- return `replace ${edit.start.hash}-${edit.end.hash}`;
314
- case "append":
315
- return edit.pos ? `append after ${edit.pos.hash}` : "append at EOF";
316
- case "prepend":
317
- return edit.pos ? `prepend before ${edit.pos.hash}` : "prepend at BOF";
318
- }
247
+ return `replace ${edit.start.hash}-${edit.end.hash}`;
319
248
  }
320
249
 
321
250
  export function validateAnchorEdits(
@@ -345,102 +274,56 @@ export function validateAnchorEdits(
345
274
 
346
275
  for (const edit of edits) {
347
276
  throwIfAborted(signal);
348
- switch (edit.op) {
349
- case "replace": {
350
- const startResolved = tryResolve(edit.start);
351
- const endResolved = tryResolve(edit.end);
352
- if (!startResolved || !endResolved) {
353
- continue;
354
- }
355
- if (startResolved.line > endResolved.line) {
356
- throw new Error(
357
- `[E_BAD_OP] Range start line ${startResolved.line} must be <= end line ${endResolved.line} (anchors ${edit.start.hash} and ${edit.end.hash}).`,
358
- );
359
- }
360
- const endLine = endResolved.line;
361
- const nextLine = fileLines[endLine];
362
- const replacementLastLine = edit.lines.at(-1)?.trim();
363
- if (
364
- nextLine !== undefined &&
365
- replacementLastLine &&
366
- /[\p{L}\p{N}]/u.test(replacementLastLine) &&
367
- replacementLastLine === nextLine.trim()
368
- ) {
369
- const resolvedEdit: ResolvedHashlineEdit = {
370
- op: "replace",
371
- start: startResolved,
372
- end: endResolved,
373
- lines: edit.lines,
374
- };
375
- warnings.push(
376
- `Potential boundary duplication after ${describeEdit(resolvedEdit)}: the replacement ends with a line that matches the next surviving line after trim.`,
377
- );
378
- }
379
- const prevLine = fileLines[startResolved.line - 2];
380
- const replacementFirstLine = edit.lines[0]?.trim();
381
- if (
382
- prevLine !== undefined &&
383
- replacementFirstLine &&
384
- /[\p{L}\p{N}]/u.test(replacementFirstLine) &&
385
- replacementFirstLine === prevLine.trim()
386
- ) {
387
- const resolvedEdit: ResolvedHashlineEdit = {
388
- op: "replace",
389
- start: startResolved,
390
- end: endResolved,
391
- lines: edit.lines,
392
- };
393
- warnings.push(
394
- `Potential boundary duplication before ${describeEdit(resolvedEdit)}: the replacement starts with a line that matches the preceding surviving line after trim.`,
395
- );
396
- }
397
- resolved.push({
398
- op: "replace",
399
- start: startResolved,
400
- end: endResolved,
401
- lines: edit.lines,
402
- });
403
- break;
404
- }
405
- case "append": {
406
- let posResolved: ResolvedAnchor | undefined;
407
- if (edit.pos) {
408
- const r = tryResolve(edit.pos);
409
- if (!r) continue;
410
- posResolved = r;
411
- }
412
- if (edit.lines.length === 0) {
413
- throw new Error(
414
- "[E_BAD_OP] Append with empty lines payload. Provide content to insert or remove the edit.",
415
- );
416
- }
417
- resolved.push({
418
- op: "append",
419
- ...(posResolved ? { pos: posResolved } : {}),
420
- lines: edit.lines,
421
- });
422
- break;
423
- }
424
- case "prepend": {
425
- let posResolved: ResolvedAnchor | undefined;
426
- if (edit.pos) {
427
- const r = tryResolve(edit.pos);
428
- if (!r) continue;
429
- posResolved = r;
430
- }
431
- if (edit.lines.length === 0) {
432
- throw new Error(
433
- "[E_BAD_OP] Prepend with empty lines payload. Provide content to insert or remove the edit.",
434
- );
435
- }
436
- resolved.push({
437
- op: "prepend",
438
- ...(posResolved ? { pos: posResolved } : {}),
439
- lines: edit.lines,
440
- });
441
- break;
442
- }
277
+ const startResolved = tryResolve(edit.start);
278
+ const endResolved = tryResolve(edit.end);
279
+ if (!startResolved || !endResolved) {
280
+ continue;
281
+ }
282
+ if (startResolved.line > endResolved.line) {
283
+ throw new Error(
284
+ `[E_BAD_OP] Range start line ${startResolved.line} must be <= end line ${endResolved.line} (anchors ${edit.start.hash} and ${edit.end.hash}).`,
285
+ );
286
+ }
287
+ const endLine = endResolved.line;
288
+ const nextLine = fileLines[endLine];
289
+ const replacementLastLine = edit.lines.at(-1)?.trim();
290
+ if (
291
+ nextLine !== undefined &&
292
+ replacementLastLine &&
293
+ /[\p{L}\p{N}]/u.test(replacementLastLine) &&
294
+ replacementLastLine === nextLine.trim()
295
+ ) {
296
+ const resolvedEdit: ResolvedHashlineEdit = {
297
+ start: startResolved,
298
+ end: endResolved,
299
+ lines: edit.lines,
300
+ };
301
+ warnings.push(
302
+ `Potential boundary duplication after ${describeEdit(resolvedEdit)}: the replacement ends with a line that matches the next surviving line after trim.`,
303
+ );
304
+ }
305
+ const prevLine = fileLines[startResolved.line - 2];
306
+ const replacementFirstLine = edit.lines[0]?.trim();
307
+ if (
308
+ prevLine !== undefined &&
309
+ replacementFirstLine &&
310
+ /[\p{L}\p{N}]/u.test(replacementFirstLine) &&
311
+ replacementFirstLine === prevLine.trim()
312
+ ) {
313
+ const resolvedEdit: ResolvedHashlineEdit = {
314
+ start: startResolved,
315
+ end: endResolved,
316
+ lines: edit.lines,
317
+ };
318
+ warnings.push(
319
+ `Potential boundary duplication before ${describeEdit(resolvedEdit)}: the replacement starts with a line that matches the preceding surviving line after trim.`,
320
+ );
443
321
  }
322
+ resolved.push({
323
+ start: startResolved,
324
+ end: endResolved,
325
+ lines: edit.lines,
326
+ });
444
327
  }
445
328
 
446
329
  return { resolved, mismatches };
package/src/read.ts CHANGED
@@ -11,7 +11,7 @@ import { Type } from "typebox";
11
11
  import { readFileSync } from "fs";
12
12
  import { access as fsAccess } from "fs/promises";
13
13
  import { constants } from "fs";
14
- import { normalizeToLF, stripBom } from "./edit-diff";
14
+ import { normalizeToLF, stripBom } from "./replace-diff";
15
15
  import { loadFileKindAndText } from "./file-kind";
16
16
  import { computeLineHashes, formatHashlineRegion } from "./hashline";
17
17
  import { resolveToCwd } from "./path-utils";
@@ -68,15 +68,14 @@ export function formatHashlineReadPreview(
68
68
  if (totalLines === 0) {
69
69
  if (startLine === 1) {
70
70
  return {
71
- text: "File is empty. Use edit with prepend or append and omit pos to insert content.",
71
+ text: "File is empty. Use edit to insert content.",
72
72
  };
73
73
  }
74
-
75
74
  return {
76
- text: `Offset ${startLine} is beyond end of file (0 lines total). The file is empty. Use edit with prepend or append and omit pos to insert content.`,
75
+ text: `Offset ${startLine} is beyond end of file (0 lines total). The file is empty. Use edit to insert content.`,
77
76
  };
78
- }
79
77
 
78
+ }
80
79
  if (startLine > totalLines) {
81
80
  return {
82
81
  text: `Offset ${startLine} is beyond end of file (${totalLines} lines total). Use offset=1 to read from the start, or offset=${totalLines} to read the last line.`,
@@ -197,7 +196,7 @@ export function registerReadTool(pi: ExtensionAPI): void {
197
196
  throwIfAborted(signal);
198
197
  const normalized = normalizeToLF(stripBom(file.text).text);
199
198
  // Compute hashes once for the whole file so the per-line anchors the
200
- // model sees here are byte-identical to what the edit tool will
199
+ // model sees here are byte-identical to what the replace tool will
201
200
  // compute when it later validates against this file.
202
201
  const fileHashes = computeLineHashes(normalized);
203
202
  const preview = formatHashlineReadPreview(
@@ -14,7 +14,7 @@ function coerceEditsArray(edits: unknown): unknown {
14
14
  }
15
15
 
16
16
 
17
- export function normalizeEditRequest(input: unknown): unknown {
17
+ export function normalizeReplaceRequest(input: unknown): unknown {
18
18
  if (!isRecord(input)) {
19
19
  return input;
20
20
  }
@@ -1,13 +1,13 @@
1
1
  /**
2
- * TUI rendering helpers for the edit tool.
2
+ * TUI rendering helpers for the replace tool.
3
3
  *
4
- * Extracted from `src/edit.ts` to separate presentation (color themes, diff
4
+ * Extracted from `src/replace.ts` to separate presentation (color themes, diff
5
5
  * formatting, Markdown rendering) from tool execution logic.
6
6
  */
7
7
 
8
8
  import type { Theme } from "@earendil-works/pi-coding-agent";
9
- import { normalizeEditRequest } from "./edit-normalize";
10
- import type { EditRequestParams, HashlineEditToolDetails } from "./edit";
9
+ import { normalizeReplaceRequest } from "./replace-normalize";
10
+ import type { ReplaceRequestParams, HashlineReplaceToolDetails } from "./replace";
11
11
  import { isRecord } from "./utils";
12
12
 
13
13
  // ─── Theme type aliases ─────────────────────────────────────────────────
@@ -21,11 +21,11 @@ export type RenderedMarkdownTheme = Pick<
21
21
 
22
22
  // ─── Render state ───────────────────────────────────────────────────────
23
23
 
24
- export type EditPreview = { diff: string } | { error: string };
24
+ export type ReplacePreview = { diff: string } | { error: string };
25
25
 
26
- export type EditRenderState = {
26
+ export type ReplaceRenderState = {
27
27
  argsKey?: string;
28
- preview?: EditPreview;
28
+ preview?: ReplacePreview;
29
29
  previewGeneration?: number;
30
30
  };
31
31
 
@@ -34,10 +34,10 @@ export type EditRenderState = {
34
34
 
35
35
  export function getRenderablePreviewInput(
36
36
  args: unknown,
37
- ): EditRequestParams | null {
37
+ ): ReplaceRequestParams | null {
38
38
  let normalized: unknown;
39
39
  try {
40
- normalized = normalizeEditRequest(args);
40
+ normalized = normalizeReplaceRequest(args);
41
41
  } catch {
42
42
  return null;
43
43
  }
@@ -45,7 +45,7 @@ export function getRenderablePreviewInput(
45
45
  return null;
46
46
  }
47
47
 
48
- const request: EditRequestParams = { path: normalized.path };
48
+ const request: ReplaceRequestParams = { path: normalized.path };
49
49
  if (Array.isArray(normalized.edits)) {
50
50
  request.edits = normalized.edits as any;
51
51
  }
@@ -91,8 +91,8 @@ export function formatResultDiff(diff: string, theme: FgTheme): string {
91
91
  // ─── Edit call formatting ───────────────────────────────────────────────
92
92
 
93
93
  export function formatEditCall(
94
- args: EditRequestParams | undefined,
95
- state: EditRenderState,
94
+ args: ReplaceRequestParams | undefined,
95
+ state: ReplaceRenderState,
96
96
  expanded: boolean,
97
97
  theme: CallTheme,
98
98
  ): string {
@@ -101,7 +101,7 @@ export function formatEditCall(
101
101
  typeof path === "string" && path.length > 0
102
102
  ? theme.fg("accent", path)
103
103
  : theme.fg("toolOutput", "...");
104
- let text = `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`;
104
+ let text = `${theme.fg("toolTitle", theme.bold("replace"))} ${pathDisplay}`;
105
105
 
106
106
  if (!state.preview) {
107
107
  return text;
@@ -139,7 +139,7 @@ export function extractRenderedWarnings(
139
139
  // ─── Result classification ──────────────────────────────────────────────
140
140
 
141
141
  export function isAppliedChangedResult(
142
- details: HashlineEditToolDetails | undefined,
142
+ details: HashlineReplaceToolDetails | undefined,
143
143
  ): boolean {
144
144
  const metrics = details?.metrics;
145
145
  return (
@@ -152,8 +152,8 @@ export function isAppliedChangedResult(
152
152
 
153
153
  export function buildAppliedChangedResultText(
154
154
  text: string | undefined,
155
- details: HashlineEditToolDetails | undefined,
156
- preview: EditPreview | undefined,
155
+ details: HashlineReplaceToolDetails | undefined,
156
+ preview: ReplacePreview | undefined,
157
157
  theme: FgTheme,
158
158
  ): string | undefined {
159
159
  const previewDiff =
@@ -1,5 +1,5 @@
1
1
 
2
- import { generateDiffString } from "./edit-diff";
2
+ import { generateDiffString } from "./replace-diff";
3
3
  import {
4
4
  computeAffectedLineRange,
5
5
  computeLineHashes,
@@ -37,7 +37,7 @@ export type FullContentPreview = {
37
37
  nextOffset?: number;
38
38
  };
39
39
 
40
- export type EditMetrics = {
40
+ export type ReplaceMetrics = {
41
41
  edits_attempted: number;
42
42
  edits_noop: number;
43
43
  warnings: number;
@@ -53,7 +53,7 @@ export type ReadMetrics = {
53
53
  next_offset?: number;
54
54
  };
55
55
 
56
- export type EditMeta = {
56
+ export type ReplaceMeta = {
57
57
  editsAttempted: number;
58
58
  noopEditsCount: number;
59
59
  firstChangedLine?: number;
@@ -75,7 +75,7 @@ export interface NoopResponseInput {
75
75
  noopEdits: NoopEditEntry[] | undefined;
76
76
  originalNormalized: string;
77
77
  snapshotId: string;
78
- editMeta: EditMeta;
78
+ editMeta: ReplaceMeta;
79
79
  warnings: string[] | undefined;
80
80
  }
81
81
 
@@ -92,7 +92,7 @@ export interface SuccessResponseInput {
92
92
  resultHashes?: string[];
93
93
  warnings: string[] | undefined;
94
94
  snapshotId: string;
95
- editMeta: EditMeta;
95
+ editMeta: ReplaceMeta;
96
96
  }
97
97
 
98
98
  // ─── Helpers ────────────────────────────────────────────────────────────
@@ -122,8 +122,8 @@ function buildMetrics(args: {
122
122
  lastChangedLine?: number;
123
123
  addedLines?: number;
124
124
  removedLines?: number;
125
- }): EditMetrics {
126
- const metrics: EditMetrics = {
125
+ }): ReplaceMetrics {
126
+ const metrics: ReplaceMetrics = {
127
127
  edits_attempted: args.editsAttempted,
128
128
  edits_noop: args.noopEditsCount,
129
129
  warnings: args.warningsCount,
@@ -471,7 +471,7 @@ export function buildChangedResponse(input: SuccessResponseInput): ToolResult {
471
471
  : "Anchors omitted; use read for subsequent edits.";
472
472
  })()
473
473
  : resultLines.length === 0
474
- ? "File is empty. Use edit with prepend or append and omit pos to insert content."
474
+ ? "File is empty. Use edit to insert content."
475
475
  : "Anchors omitted; use read for subsequent edits.";
476
476
 
477
477
  const text = [anchorsBlock, warningsBlock.trimStart()]