pi-hashline-edit-pro 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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[];
@@ -89,15 +78,15 @@ export function formatMismatchError(
89
78
 
90
79
  if (notFound.length > 0) {
91
80
  const refList = notFound.map((m) => `"${m.ref.hash}"`).join(", ");
92
- out.push(
93
- `[E_STALE_ANCHOR] ${notFound.length} stale anchor${notFound.length > 1 ? "s" : ""}: ${refList}. Re-read the file to refresh.`
94
- );
81
+ out.push(
82
+ `[E_STALE_ANCHOR] ${notFound.length} stale anchor${notFound.length > 1 ? "s" : ""}: ${refList}. Call read() to get fresh anchors, then copy the 4-character HASH from each line into your next replace call.`
83
+ );
95
84
  }
96
85
  if (ambiguous.length > 0) {
97
86
  if (out.length > 0) out.push("");
98
- out.push(
99
- `[E_AMBIGUOUS_ANCHOR] ${ambiguous.length} ambiguous anchor${ambiguous.length > 1 ? "s" : ""}. Re-read the file to refresh.`
100
- );
87
+ out.push(
88
+ `[E_AMBIGUOUS_ANCHOR] ${ambiguous.length} ambiguous anchor${ambiguous.length > 1 ? "s" : ""}. Call read() to get fresh anchors, then copy the 4-character HASH from each line into your next replace call.`
89
+ );
101
90
  for (const m of ambiguous) {
102
91
  const sample = (m.candidates ?? []).slice(0, 5);
103
92
  const more =
@@ -116,21 +105,12 @@ export function formatMismatchError(
116
105
  }
117
106
  }
118
107
 
119
- out.push("");
120
- out.push("Current state (first lines):");
121
- const sampleSize = Math.min(fileLines.length, 5);
122
- for (let i = 0; i < sampleSize; i++) {
123
- out.push(`>>> ${fileHashes[i]}│${fileLines[i]}`);
124
- }
125
- if (fileLines.length > sampleSize) {
126
- out.push(`... ${fileLines.length - sampleSize} more.`);
127
- }
128
108
 
129
109
  return out.join("\n");
130
110
  }
131
111
 
132
112
 
133
- const ITEM_KEYS = new Set(["op", "pos", "start", "end", "lines"]);
113
+ const ITEM_KEYS = new Set(["start", "end", "lines"]);
134
114
  function isStringArray(value: unknown): value is string[] {
135
115
  return (
136
116
  Array.isArray(value) && value.every((item) => typeof item === "string")
@@ -145,23 +125,6 @@ function assertEditItem(edit: Record<string, unknown>, index: number): void {
145
125
  );
146
126
  }
147
127
 
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
128
  if ("start" in edit && typeof edit.start !== "string") {
166
129
  throw new Error(
167
130
  `[E_BAD_SHAPE] Edit ${index} field "start" must be a string when provided.`,
@@ -176,29 +139,17 @@ function assertEditItem(edit: Record<string, unknown>, index: number): void {
176
139
  if ("lines" in edit && !isStringArray(edit.lines)) {
177
140
  throw new Error(`[E_BAD_SHAPE] Edit ${index} field "lines" must be a string array.`);
178
141
  }
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
- }
142
+ if (typeof edit.start !== "string") {
143
+ throw new Error(
144
+ `[E_BAD_OP] Edit ${index} requires a "start" anchor string.`,
145
+ );
195
146
  }
196
-
197
- if ((edit.op === "append" || edit.op === "prepend") && "end" in edit) {
147
+ if (typeof edit.end !== "string") {
198
148
  throw new Error(
199
- `[E_BAD_OP] Edit ${index} op "${edit.op}" does not support "end". Use "pos" or omit.`
149
+ `[E_BAD_OP] Edit ${index} requires an "end" anchor string.`,
200
150
  );
201
151
  }
152
+
202
153
  }
203
154
 
204
155
  export function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
@@ -206,45 +157,21 @@ export function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
206
157
  for (const [index, edit] of edits.entries()) {
207
158
  assertEditItem(edit as Record<string, unknown>, index);
208
159
 
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
- }
160
+ // Normalize lines: [""] to lines: [] for deletion.
161
+ const replaceLines = hashlineParseText(edit.lines ?? null);
162
+ const normalizedLines =
163
+ replaceLines.length === 1 && replaceLines[0] === ""
164
+ ? []
165
+ : replaceLines;
166
+ result.push({
167
+ start: parseHashRef(edit.start!),
168
+ end: parseHashRef(edit.end!),
169
+ lines: normalizedLines,
170
+ });
243
171
  }
244
172
  return result;
245
173
  }
246
174
 
247
-
248
175
  function maybeWarnSuspiciousUnicodeEscapePlaceholder(
249
176
  edits: HashlineEdit[],
250
177
  warnings: string[],
@@ -262,7 +189,6 @@ export function assertNoBareHashPrefixLines(
262
189
  edits: HashlineEdit[],
263
190
  fileLines: string[],
264
191
  fileHashes: string[],
265
- filePath?: string,
266
192
  ): string[] {
267
193
  if (fileHashes.length !== fileLines.length) {
268
194
  throw new Error(
@@ -280,18 +206,11 @@ export function assertNoBareHashPrefixLines(
280
206
  }
281
207
  if (suspects.length === 0) return [];
282
208
 
283
- const isPython = filePath?.endsWith('.py');
284
209
  const fileHashSet = new Set(fileHashes);
285
210
  const matched = suspects.filter((s) => fileHashSet.has(s.hash));
286
211
  const matchedCount = matched.length;
287
212
  const exampleLine = `${suspects[0]!.hash}│${suspects[0]!.line}`;
288
213
 
289
- if (isPython) {
290
- const hint = matchedCount > 0
291
- ? `${matchedCount} prefix(es) match file line hashes.`
292
- : `None match file line hashes — likely Python syntax.`;
293
- return [`[W_BARE_HASH_PREFIX] ${suspects.length} edit line(s) start with a hash-like prefix (e.g. ${JSON.stringify(exampleLine)}). ${hint}`];
294
- }
295
214
 
296
215
  const linesHint =
297
216
  matchedCount === 0
@@ -308,14 +227,7 @@ export function assertNoBareHashPrefixLines(
308
227
  * Human-readable label for a resolved edit (used in warnings and conflict errors).
309
228
  */
310
229
  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
- }
230
+ return `replace ${edit.start.hash}-${edit.end.hash}`;
319
231
  }
320
232
 
321
233
  export function validateAnchorEdits(
@@ -345,102 +257,56 @@ export function validateAnchorEdits(
345
257
 
346
258
  for (const edit of edits) {
347
259
  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
- }
260
+ const startResolved = tryResolve(edit.start);
261
+ const endResolved = tryResolve(edit.end);
262
+ if (!startResolved || !endResolved) {
263
+ continue;
264
+ }
265
+ if (startResolved.line > endResolved.line) {
266
+ throw new Error(
267
+ `[E_BAD_OP] Range start line ${startResolved.line} must be <= end line ${endResolved.line} (anchors ${edit.start.hash} and ${edit.end.hash}).`,
268
+ );
269
+ }
270
+ const endLine = endResolved.line;
271
+ const nextLine = fileLines[endLine];
272
+ const replacementLastLine = edit.lines.at(-1)?.trim();
273
+ if (
274
+ nextLine !== undefined &&
275
+ replacementLastLine &&
276
+ /[\p{L}\p{N}]/u.test(replacementLastLine) &&
277
+ replacementLastLine === nextLine.trim()
278
+ ) {
279
+ const resolvedEdit: ResolvedHashlineEdit = {
280
+ start: startResolved,
281
+ end: endResolved,
282
+ lines: edit.lines,
283
+ };
284
+ warnings.push(
285
+ `Potential boundary duplication after ${describeEdit(resolvedEdit)}: the replacement ends with a line that matches the next surviving line after trim.`,
286
+ );
287
+ }
288
+ const prevLine = fileLines[startResolved.line - 2];
289
+ const replacementFirstLine = edit.lines[0]?.trim();
290
+ if (
291
+ prevLine !== undefined &&
292
+ replacementFirstLine &&
293
+ /[\p{L}\p{N}]/u.test(replacementFirstLine) &&
294
+ replacementFirstLine === prevLine.trim()
295
+ ) {
296
+ const resolvedEdit: ResolvedHashlineEdit = {
297
+ start: startResolved,
298
+ end: endResolved,
299
+ lines: edit.lines,
300
+ };
301
+ warnings.push(
302
+ `Potential boundary duplication before ${describeEdit(resolvedEdit)}: the replacement starts with a line that matches the preceding surviving line after trim.`,
303
+ );
443
304
  }
305
+ resolved.push({
306
+ start: startResolved,
307
+ end: endResolved,
308
+ lines: edit.lines,
309
+ });
444
310
  }
445
311
 
446
312
  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()]