pi-hashline-edit-pro 0.16.5 → 0.16.7
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 +1 -1
- package/package.json +1 -1
- package/prompts/replace-bulk-guidelines.md +1 -2
- package/prompts/replace-bulk.md +0 -34
- package/prompts/replace-flat-guidelines.md +1 -2
- package/prompts/replace-flat.md +0 -29
- package/src/hashline/apply.ts +45 -4
- package/src/hashline/index.ts +1 -1
- package/src/hashline/resolve.ts +7 -0
- package/src/replace.ts +3 -37
- package/prompts/replace-guidelines.md +0 -8
- package/prompts/replace-snippet.md +0 -1
- package/prompts/replace.md +0 -188
package/README.md
CHANGED
|
@@ -143,7 +143,7 @@ The file is created automatically when any setting is toggled. Both fields are i
|
|
|
143
143
|
- **Strict patch content.** If `content_lines` contains `+HASH│` display prefixes (or `-N ` numbered deletion rows), the edit is rejected with `[E_INVALID_PATCH]`. This narrowly guards against pasting the tool's own diff-preview rows back as content; standard unified-diff lines (`+x`, `-x`, ` x`, `@@ … @@`) are **not** rejected — they are written literally, since literal content must never be silently altered. Bare `HASH│` content (the first 4 chars of a `content_lines` entry looking like 3 base64 chars + `│`) is rejected with `[E_BARE_HASH_PREFIX]`. When the suspect's prefix happens to match a real file-line anchor, the error message flags that as strong evidence the model copied an anchor from the read output.
|
|
144
144
|
- **Atomic writes.** Files are written via temp-file-then-rename to avoid corruption from interrupted writes. Symlink chains are resolved so the target file is updated without replacing the symlink. Hard-linked files are updated in place to preserve the shared inode. File permissions are preserved across atomic renames.
|
|
145
145
|
- **Per-file mutation queue.** Edits queue by the canonical write target, so concurrent edits through different symlink paths still serialize onto the same underlying file.
|
|
146
|
-
- **Boundary duplication
|
|
146
|
+
- **Boundary duplication auto-fix.** When the last line of a replacement matches the next surviving line (or the first line matches the preceding one), the runtime automatically strips the duplicate from `content_lines` before applying the edit. This catches a common LLM pattern where closing delimiters like `}`, `});`, or `} else {` are accidentally duplicated. The auto-fix is completely silent — the model sees a normal successful edit. The duplicate never reaches the file. Raw line comparison (not trimmed) avoids false positives when indentation differs.
|
|
147
147
|
- **Flat mode normalization.** When flat mode is active, the tool's `execute` function wraps the top-level `hash_range_inclusive` and `content_lines` into a single-element `changes` array internally, then runs the same pipeline as bulk mode. The `normReq` function in `replace-normalize.ts` also handles flat format directly, so any code path that normalizes input (e.g. `compPreview`) works with both formats.
|
|
148
148
|
- **Persistent hash store.** `lineHashes` is async and uses a persistent store to preserve hashes for unchanged lines across edits. The store is at `~/.config/pi-hashline-edit-pro/hash-store.json` and is auto-created on first use. It contains per-file snapshots (last known content+hashes). When called from the replace pipeline, it maps old vs new content and copies hashes for unchanged lines. When called from read, it returns saved hashes if the content matches, otherwise computes fresh hashes via `_lineHashesPure`. Stale snapshots are pruned on session start. This ensures that editing one part of a file does not cascade to change hashes of unrelated lines.
|
|
149
149
|
## Hashing
|
package/package.json
CHANGED
|
@@ -3,5 +3,4 @@
|
|
|
3
3
|
- On `[E_STALE_ANCHOR]`, call `read` to get fresh anchors, copy the 3-character HASH of the start and end of the range you are replacing into `hash_range_inclusive`, and retry.
|
|
4
4
|
- `hash_range_inclusive` replaces the ENTIRE range inclusively. Every line from the first anchor through the second anchor is deleted. Only put the replacement lines in `content_lines` — do not include lines that already exist outside the range.
|
|
5
5
|
- **Preserve leading whitespace (indentation) exactly.** The content after `│` in read output includes all leading spaces and tabs — copy them into `content_lines` unchanged.
|
|
6
|
-
- **`content_lines` must be a native JSON array of strings**, not a JSON string. Do NOT serialize it: `"content_lines": "[\"line1\", \"line2\"]"` is WRONG. Use `"content_lines": ["line1", "line2"]` instead.
|
|
7
|
-
- If the response includes a "Boundary duplication" warning, the edge line of your `content_lines` matches a line outside the replaced range. Remove that line from `content_lines` — the original is still in the file outside the range.
|
|
6
|
+
- **`content_lines` must be a native JSON array of strings**, not a JSON string. Do NOT serialize it: `"content_lines": "[\"line1\", \"line2\"]"` is WRONG. Use `"content_lines": ["line1", "line2"]` instead.
|
package/prompts/replace-bulk.md
CHANGED
|
@@ -2,20 +2,6 @@ Replace lines in a text file using HASH anchors from `read`.
|
|
|
2
2
|
|
|
3
3
|
Put all operations on one file in a single `replace` call. Stack every region into the `changes` array, even when they are far apart. Anchors within one call must all come from the same pre-edit read; the runtime applies them atomically against that one snapshot.
|
|
4
4
|
|
|
5
|
-
How to use:
|
|
6
|
-
|
|
7
|
-
1. Call `read` to get HASH anchors:
|
|
8
|
-
```
|
|
9
|
-
read({ path: "src/main.ts" })
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
2. Copy the 3-character HASH (before `│`) into `hash_range_inclusive`:
|
|
13
|
-
```json
|
|
14
|
-
{ "changes": [
|
|
15
|
-
{ "content_lines": ["const x = 99;"], "hash_range_inclusive": ["MQX", "MQX"] }
|
|
16
|
-
], "path": "src/main.ts" }
|
|
17
|
-
```
|
|
18
|
-
|
|
19
5
|
Examples:
|
|
20
6
|
|
|
21
7
|
1. Single line replace:
|
|
@@ -51,13 +37,6 @@ Examples:
|
|
|
51
37
|
], "path": "src/main.ts" }
|
|
52
38
|
```
|
|
53
39
|
|
|
54
|
-
5. Seed content into an empty file (replace the single empty-line hash returned by read):
|
|
55
|
-
```json
|
|
56
|
-
{ "changes": [
|
|
57
|
-
{ "content_lines": ["first line", "second line"], "hash_range_inclusive": ["aB3", "aB3"] }
|
|
58
|
-
], "path": "src/main.ts" }
|
|
59
|
-
```
|
|
60
|
-
|
|
61
40
|
⚠️ Common mistake: do not copy the `HASH│` prefix into `content_lines`.
|
|
62
41
|
|
|
63
42
|
Wrong:
|
|
@@ -123,17 +102,4 @@ Right: Only include the new lines that belong in the range:
|
|
|
123
102
|
{ "content_lines": [" const y = 2;", " return y;"], "hash_range_inclusive": ["X", "Y"] }
|
|
124
103
|
The `}` on line 4 is outside the range and stays in place.
|
|
125
104
|
|
|
126
|
-
Error recovery:
|
|
127
|
-
- `[E_STALE_ANCHOR]` — the anchored line's content changed since the last read. Call `read` to get fresh anchors, then copy the 3-char HASH of the start and end of the range you are replacing into `hash_range_inclusive` and retry. (Staleness is per-line: editing or appending lines does not invalidate anchors for lines whose content is unchanged, so anchors for untouched regions stay valid across edits.)
|
|
128
|
-
- `[E_BAD_REF]` — malformed HASH. Re-read and try again.
|
|
129
|
-
- `[E_BAD_OP]` — invalid operation (e.g. start line > end line).
|
|
130
|
-
- `[E_BAD_SHAPE]` — malformed request or change item (missing fields, wrong types, unknown fields).
|
|
131
|
-
- `[E_LEGACY_SHAPE]` — old `oldText`/`newText` or `old_text`/`new_text` format detected. Use `{content_lines, hash_range_inclusive}` instead.
|
|
132
|
-
- `[E_EDIT_CONFLICT]` — two changes overlap on the same line range. Make changes non-overlapping.
|
|
133
|
-
- `[E_AMBIGUOUS_ANCHOR]` — hash collision. Call `read` to get fresh anchors.
|
|
134
|
-
- `[E_BARE_HASH_PREFIX]` — a `content_lines` entry starts with `HASH│`. Remove the hash prefix; keep only the literal line content that appears after `│` in `read` output. `content_lines` uses file content only, `hash_range_inclusive` uses hash anchors.
|
|
135
|
-
- `[E_INVALID_PATCH]` — a `content_lines` entry matches the diff preview's `+HASH│…` addition-row form. Use literal file content. (Plain `+`/`-` lines are not rejected — they are written literally.)
|
|
136
|
-
- `[E_WOULD_EMPTY]` — edit would empty a non-empty file.
|
|
137
|
-
- `[E_FILE_TOO_LARGE]` — file exceeds the 1,000,000-line edit limit. Use `write` or a non-line-based approach for very large files.
|
|
138
|
-
|
|
139
105
|
**Undo:** If a replace produced incorrect results, call `undo_last_replace` with the file path to revert the last replace. The tool reports how many lines were removed and restored. After undoing, call `read` to get fresh anchors for a corrected replace.
|
|
@@ -3,5 +3,4 @@
|
|
|
3
3
|
- On `[E_STALE_ANCHOR]`, call `read` to get fresh anchors, copy the 3-character HASH of the start and end of the range you are replacing into `hash_range_inclusive`, and retry.
|
|
4
4
|
- `hash_range_inclusive` replaces the ENTIRE range inclusively. Every line from the first anchor through the second anchor is deleted. Only put the replacement lines in `content_lines` — do not include lines that already exist outside the range.
|
|
5
5
|
- **Preserve leading whitespace (indentation) exactly.** The content after `│` in read output includes all leading spaces and tabs — copy them into `content_lines` unchanged.
|
|
6
|
-
- **`content_lines` must be a native JSON array of strings**, not a JSON string. Do NOT serialize it: `"content_lines": "[\"line1\", \"line2\"]"` is WRONG. Use `"content_lines": ["line1", "line2"]` instead.
|
|
7
|
-
- If the response includes a "Boundary duplication" warning, the edge line of your `content_lines` matches a line outside the replaced range. Remove that line from `content_lines` — the original is still in the file outside the range.
|
|
6
|
+
- **`content_lines` must be a native JSON array of strings**, not a JSON string. Do NOT serialize it: `"content_lines": "[\"line1\", \"line2\"]"` is WRONG. Use `"content_lines": ["line1", "line2"]` instead.
|
package/prompts/replace-flat.md
CHANGED
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
Replace lines in a text file using HASH anchors from `read`. Only one edit per call (no bulk `changes` array — `hash_range_inclusive` and `content_lines` sit at the top level).
|
|
2
2
|
|
|
3
|
-
How to use:
|
|
4
|
-
|
|
5
|
-
1. Call `read` to get HASH anchors:
|
|
6
|
-
```
|
|
7
|
-
read({ path: "src/main.ts" })
|
|
8
|
-
```
|
|
9
|
-
|
|
10
|
-
2. Copy the 3-character HASH (before `│`) into `hash_range_inclusive`:
|
|
11
|
-
```json
|
|
12
|
-
{ "content_lines": ["const x = 99;"], "hash_range_inclusive": ["MQX", "MQX"], "path": "src/main.ts" }
|
|
13
|
-
```
|
|
14
|
-
|
|
15
3
|
Examples:
|
|
16
4
|
|
|
17
5
|
1. Single line replace:
|
|
@@ -38,11 +26,6 @@ Examples:
|
|
|
38
26
|
{ "content_lines": ["old last line", "new line"], "hash_range_inclusive": ["ZPM", "ZPM"], "path": "src/main.ts" }
|
|
39
27
|
```
|
|
40
28
|
|
|
41
|
-
5. Seed content into an empty file (replace the single empty-line hash returned by read):
|
|
42
|
-
```json
|
|
43
|
-
{ "content_lines": ["first line", "second line"], "hash_range_inclusive": ["aB3", "aB3"], "path": "src/main.ts" }
|
|
44
|
-
```
|
|
45
|
-
|
|
46
29
|
⚠️ Common mistake: do not copy the `HASH│` prefix into `content_lines`.
|
|
47
30
|
|
|
48
31
|
Wrong:
|
|
@@ -109,16 +92,4 @@ Right: Only include the new lines that belong in the range:
|
|
|
109
92
|
{ "content_lines": [" const y = 2;", " return y;"], "hash_range_inclusive": ["X", "Y"] }
|
|
110
93
|
The `}` on line 4 is outside the range and stays in place.
|
|
111
94
|
|
|
112
|
-
Error recovery:
|
|
113
|
-
- `[E_STALE_ANCHOR]` — the anchored line's content changed since the last read. Call `read` to get fresh anchors, then copy the 3-char HASH of the start and end of the range you are replacing into `hash_range_inclusive` and retry. (Staleness is per-line: editing or appending lines does not invalidate anchors for lines whose content is unchanged, so anchors for untouched regions stay valid across edits.)
|
|
114
|
-
- `[E_BAD_REF]` — malformed HASH. Re-read and try again.
|
|
115
|
-
- `[E_BAD_OP]` — invalid operation (e.g. start line > end line).
|
|
116
|
-
- `[E_BAD_SHAPE]` — malformed request or change item (missing fields, wrong types, unknown fields).
|
|
117
|
-
- `[E_LEGACY_SHAPE]` — old `oldText`/`newText` or `old_text`/`new_text` format detected. Use `{content_lines, hash_range_inclusive}` instead.
|
|
118
|
-
- `[E_AMBIGUOUS_ANCHOR]` — hash collision. Call `read` to get fresh anchors.
|
|
119
|
-
- `[E_BARE_HASH_PREFIX]` — a `content_lines` entry starts with `HASH│`. Remove the hash prefix; keep only the literal line content that appears after `│` in `read` output. `content_lines` uses file content only, `hash_range_inclusive` uses hash anchors.
|
|
120
|
-
- `[E_INVALID_PATCH]` — a `content_lines` entry matches the diff preview's `+HASH│…` addition-row form. Use literal file content. (Plain `+`/`-` lines are not rejected — they are written literally.)
|
|
121
|
-
- `[E_WOULD_EMPTY]` — edit would empty a non-empty file.
|
|
122
|
-
- `[E_FILE_TOO_LARGE]` — file exceeds the 1,000,000-line edit limit. Use `write` or a non-line-based approach for very large files.
|
|
123
|
-
|
|
124
95
|
**Undo:** If a replace produced incorrect results, call `undo_last_replace` with the file path to revert the last replace. The tool reports how many lines were removed and restored. After undoing, call `read` to get fresh anchors for a corrected replace.
|
package/src/hashline/apply.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type NEdit,
|
|
11
11
|
type HEdit,
|
|
12
12
|
type BDupWarn,
|
|
13
|
+
type AutoFix,
|
|
13
14
|
} from "./resolve";
|
|
14
15
|
import { cntLines } from "../utils";
|
|
15
16
|
|
|
@@ -262,7 +263,7 @@ export function applyEdits(
|
|
|
262
263
|
lastChangedLine: number | undefined;
|
|
263
264
|
warnings?: string[];
|
|
264
265
|
noopEdits?: NEdit[];
|
|
265
|
-
|
|
266
|
+
autoFixes?: AutoFix[];
|
|
266
267
|
} {
|
|
267
268
|
abortIf(signal);
|
|
268
269
|
if (!edits.length)
|
|
@@ -272,13 +273,12 @@ export function applyEdits(
|
|
|
272
273
|
lastChangedLine: undefined,
|
|
273
274
|
};
|
|
274
275
|
|
|
275
|
-
|
|
276
276
|
const lineIndex = buildIdx(content);
|
|
277
277
|
const fileHashes = precomputedHashes ?? _lineHashesPure(content);
|
|
278
278
|
const noopEdits: NEdit[] = [];
|
|
279
279
|
const warnings: string[] = [];
|
|
280
280
|
|
|
281
|
-
const { resolved, mismatches, boundaryWarnings } = valEdits(
|
|
281
|
+
const { resolved: initialResolved, mismatches, boundaryWarnings } = valEdits(
|
|
282
282
|
edits,
|
|
283
283
|
lineIndex.fileLines,
|
|
284
284
|
fileHashes,
|
|
@@ -294,6 +294,47 @@ export function applyEdits(
|
|
|
294
294
|
assertNoBarePrefix(edits, lineIndex.fileLines, fileHashes);
|
|
295
295
|
warnUnicodeEsc(edits, warnings);
|
|
296
296
|
|
|
297
|
+
// Auto-fix boundary duplications: strip the offending line from content_lines
|
|
298
|
+
let resolved = initialResolved;
|
|
299
|
+
let autoFixes: AutoFix[] | undefined;
|
|
300
|
+
if (boundaryWarnings.length > 0) {
|
|
301
|
+
autoFixes = [];
|
|
302
|
+
// Deep-copy edits so we don't mutate the originals
|
|
303
|
+
const correctedEdits: import("./resolve").HEdit[] = edits.map(e => ({
|
|
304
|
+
...e,
|
|
305
|
+
content_lines: [...e.content_lines],
|
|
306
|
+
}));
|
|
307
|
+
for (const bw of boundaryWarnings) {
|
|
308
|
+
const edit = correctedEdits[bw.editIndex];
|
|
309
|
+
if (!edit) continue;
|
|
310
|
+
if (bw.kind === "trailing") {
|
|
311
|
+
const removed = edit.content_lines.pop();
|
|
312
|
+
if (removed !== undefined) {
|
|
313
|
+
autoFixes.push({ kind: "trailing", editIndex: bw.editIndex, removedLine: removed });
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
const removed = edit.content_lines.shift();
|
|
317
|
+
if (removed !== undefined) {
|
|
318
|
+
autoFixes.push({ kind: "leading", editIndex: bw.editIndex, removedLine: removed });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Re-validate with corrected edits
|
|
323
|
+
const correctedResult = valEdits(
|
|
324
|
+
correctedEdits,
|
|
325
|
+
lineIndex.fileLines,
|
|
326
|
+
fileHashes,
|
|
327
|
+
warnings,
|
|
328
|
+
signal,
|
|
329
|
+
);
|
|
330
|
+
if (correctedResult.mismatches.length) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
fmtMismatch(correctedResult.mismatches, lineIndex.fileLines, fileHashes, filePath),
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
resolved = correctedResult.resolved;
|
|
336
|
+
}
|
|
337
|
+
|
|
297
338
|
const orderedSpans = resSpans(
|
|
298
339
|
resolved,
|
|
299
340
|
content,
|
|
@@ -312,7 +353,7 @@ export function applyEdits(
|
|
|
312
353
|
lastChangedLine: range?.lastChangedLine,
|
|
313
354
|
...(warnings.length ? { warnings } : {}),
|
|
314
355
|
...(noopEdits.length ? { noopEdits } : {}),
|
|
315
|
-
...(
|
|
356
|
+
...(autoFixes ? { autoFixes } : {}),
|
|
316
357
|
};
|
|
317
358
|
}
|
|
318
359
|
|
package/src/hashline/index.ts
CHANGED
package/src/hashline/resolve.ts
CHANGED
package/src/replace.ts
CHANGED
|
@@ -18,7 +18,6 @@ import { resolveTarget, writeAtomic } from "./fs-write";
|
|
|
18
18
|
import {
|
|
19
19
|
applyEdits,
|
|
20
20
|
lineHashes,
|
|
21
|
-
fmtBoundaryWarning,
|
|
22
21
|
resEdits,
|
|
23
22
|
type HTEdit,
|
|
24
23
|
} from "./hashline";
|
|
@@ -195,30 +194,7 @@ export async function execPipeline(
|
|
|
195
194
|
removedHashes,
|
|
196
195
|
});
|
|
197
196
|
|
|
198
|
-
const resultLines = result.split("\n");
|
|
199
197
|
const warnings = [...(anchorResult.warnings ?? [])];
|
|
200
|
-
for (const bw of anchorResult.boundaryWarnings ?? []) {
|
|
201
|
-
let seen = 0;
|
|
202
|
-
let matchIndex = -1;
|
|
203
|
-
for (let i = 0; i < resultLines.length; i++) {
|
|
204
|
-
if (resultLines[i] === bw.survivingLineContent) {
|
|
205
|
-
if (seen === bw.occurrence) { matchIndex = i; break; }
|
|
206
|
-
seen++;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (matchIndex >= 0) {
|
|
210
|
-
warnings.push(
|
|
211
|
-
fmtBoundaryWarning({
|
|
212
|
-
kind: bw.kind,
|
|
213
|
-
survivingContent: bw.survivingLineContent,
|
|
214
|
-
matchIndex,
|
|
215
|
-
resultLines,
|
|
216
|
-
resultHashes,
|
|
217
|
-
}),
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
198
|
return {
|
|
223
199
|
path,
|
|
224
200
|
toolEdits,
|
|
@@ -297,7 +273,7 @@ export function buildToolDef(opts: { flat: boolean }): ToolDef {
|
|
|
297
273
|
AUTO_READ_GUIDANCE: readGuidance,
|
|
298
274
|
});
|
|
299
275
|
|
|
300
|
-
const parameters =
|
|
276
|
+
const parameters = editToolSchema;
|
|
301
277
|
|
|
302
278
|
return {
|
|
303
279
|
name: "replace",
|
|
@@ -311,7 +287,7 @@ export function buildToolDef(opts: { flat: boolean }): ToolDef {
|
|
|
311
287
|
if (!isRec(args)) return args as any;
|
|
312
288
|
const record = { ...args };
|
|
313
289
|
normalizeFilePath(record);
|
|
314
|
-
return record as any;
|
|
290
|
+
return normReq(record) as any;
|
|
315
291
|
}
|
|
316
292
|
: (args: unknown) =>
|
|
317
293
|
normReq(args) as ReqParams,
|
|
@@ -404,17 +380,7 @@ export function buildToolDef(opts: { flat: boolean }): ToolDef {
|
|
|
404
380
|
},
|
|
405
381
|
|
|
406
382
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
407
|
-
const canonical =
|
|
408
|
-
? normReq({
|
|
409
|
-
content_lines: (params as any).content_lines,
|
|
410
|
-
hash_range_inclusive: (params as any).hash_range_inclusive,
|
|
411
|
-
path: (params as any).path,
|
|
412
|
-
changes: [{
|
|
413
|
-
content_lines: (params as any).content_lines,
|
|
414
|
-
hash_range_inclusive: (params as any).hash_range_inclusive,
|
|
415
|
-
}],
|
|
416
|
-
})
|
|
417
|
-
: normReq(params);
|
|
383
|
+
const canonical = normReq(params);
|
|
418
384
|
|
|
419
385
|
|
|
420
386
|
const normalizedParams = canonical as { path: string; changes: HTEdit[] };
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
- Use `replace` with HASH anchors for all file changes; batch every change to one file into a single `replace` call.
|
|
2
|
-
- After a successful `replace`, the response shows the change summary. {{AUTO_READ_GUIDANCE}}
|
|
3
|
-
- On `[E_STALE_ANCHOR]`, call `read` to get fresh anchors, copy the 3-character HASH of the start and end of the range you are replacing into `hash_range_inclusive`, and retry.
|
|
4
|
-
- `hash_range_inclusive` replaces the ENTIRE range inclusively. Every line from the first anchor through the second anchor is deleted. Only put the replacement lines in `content_lines` — do not include lines that already exist outside the range.
|
|
5
|
-
- **Preserve leading whitespace (indentation) exactly.** The content after `│` in read output includes all leading spaces and tabs — copy them into `content_lines` unchanged.
|
|
6
|
-
- **`content_lines` must be a native JSON array of strings**, not a JSON string. Do NOT serialize it: `"content_lines": "[\"line1\", \"line2\"]"` is WRONG. Use `"content_lines": ["line1", "line2"]` instead.
|
|
7
|
-
- Two modes are available: bulk (default, uses `changes` array) and flat (top-level `hash_range_inclusive`/`content_lines`). Toggle with `/toggle-replace-mode`.
|
|
8
|
-
- If the response includes a "Boundary duplication" warning, the edge line of your `content_lines` matches a line outside the replaced range. Remove that line from `content_lines` — the original is still in the file outside the range.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Replace lines in a text file via HASH anchors from read, batching all changes to a file in one call. Supports bulk mode (changes array) and flat mode (top-level fields), toggled via /toggle-replace-mode.
|
package/prompts/replace.md
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
Replace lines in a text file using HASH anchors from `read`.
|
|
2
|
-
|
|
3
|
-
Two modes are available, toggled via `/toggle-replace-mode` (persists across sessions):
|
|
4
|
-
|
|
5
|
-
**Bulk mode (default):** `hash_range_inclusive` and `content_lines` go inside a `changes` array, supporting multiple edits in one call.
|
|
6
|
-
|
|
7
|
-
**Flat mode:** `hash_range_inclusive` and `content_lines` sit at the top level. Only one edit per call.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
Put all operations on one file in a single `replace` call. Stack every region into the `changes` array, even when they are far apart. Anchors within one call must all come from the same pre-edit read; the runtime applies them atomically against that one snapshot.
|
|
12
|
-
|
|
13
|
-
How to use:
|
|
14
|
-
|
|
15
|
-
1. Call `read` to get HASH anchors:
|
|
16
|
-
```
|
|
17
|
-
read({ path: "src/main.ts" })
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
2. Copy the 3-character HASH (before `│`) into `hash_range_inclusive`:
|
|
21
|
-
|
|
22
|
-
**Bulk mode:**
|
|
23
|
-
```json
|
|
24
|
-
{ "changes": [
|
|
25
|
-
{ "content_lines": ["const x = 99;"], "hash_range_inclusive": ["MQX", "MQX"] }
|
|
26
|
-
], "path": "src/main.ts" }
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
**Flat mode:**
|
|
30
|
-
```json
|
|
31
|
-
{ "content_lines": ["const x = 99;"], "hash_range_inclusive": ["MQX", "MQX"], "path": "src/main.ts" }
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Examples:
|
|
35
|
-
|
|
36
|
-
1. Single line replace:
|
|
37
|
-
|
|
38
|
-
Bulk:
|
|
39
|
-
```json
|
|
40
|
-
{ "changes": [
|
|
41
|
-
{ "content_lines": ["const x = 1;"], "hash_range_inclusive": ["MQX", "MQX"] }
|
|
42
|
-
], "path": "src/main.ts" }
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Flat:
|
|
46
|
-
```json
|
|
47
|
-
{ "content_lines": ["const x = 1;"], "hash_range_inclusive": ["MQX", "MQX"], "path": "src/main.ts" }
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
2. Range replace (3 lines → 3 new lines):
|
|
51
|
-
|
|
52
|
-
Bulk:
|
|
53
|
-
```json
|
|
54
|
-
{ "changes": [
|
|
55
|
-
{ "content_lines": [
|
|
56
|
-
"function greet(name) {",
|
|
57
|
-
" return `Hello, ${name}`;",
|
|
58
|
-
"}"
|
|
59
|
-
], "hash_range_inclusive": ["ZPM", "VRW"] }
|
|
60
|
-
], "path": "src/main.ts" }
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Flat:
|
|
64
|
-
```json
|
|
65
|
-
{ "content_lines": [
|
|
66
|
-
"function greet(name) {",
|
|
67
|
-
" return `Hello, ${name}`;",
|
|
68
|
-
"}"
|
|
69
|
-
], "hash_range_inclusive": ["ZPM", "VRW"], "path": "src/main.ts" }
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
3. Multiple regions in one call (bulk mode only — flat mode supports one edit per call):
|
|
73
|
-
```json
|
|
74
|
-
{ "changes": [
|
|
75
|
-
{ "content_lines": [], "hash_range_inclusive": ["aB3", "xY7"] },
|
|
76
|
-
{ "content_lines": [], "hash_range_inclusive": ["MQX", "ZPM"] }
|
|
77
|
-
], "path": "src/server.ts" }
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
4. Append after the last line (include the old last line so the new line is added after it):
|
|
81
|
-
|
|
82
|
-
Bulk:
|
|
83
|
-
```json
|
|
84
|
-
{ "changes": [
|
|
85
|
-
{ "content_lines": ["old last line", "new line"], "hash_range_inclusive": ["ZPM", "ZPM"] }
|
|
86
|
-
], "path": "src/main.ts" }
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Flat:
|
|
90
|
-
```json
|
|
91
|
-
{ "content_lines": ["old last line", "new line"], "hash_range_inclusive": ["ZPM", "ZPM"], "path": "src/main.ts" }
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
5. Seed content into an empty file (replace the single empty-line hash returned by read):
|
|
95
|
-
|
|
96
|
-
Bulk:
|
|
97
|
-
```json
|
|
98
|
-
{ "changes": [
|
|
99
|
-
{ "content_lines": ["first line", "second line"], "hash_range_inclusive": ["aB3", "aB3"] }
|
|
100
|
-
], "path": "src/main.ts" }
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Flat:
|
|
104
|
-
```json
|
|
105
|
-
{ "content_lines": ["first line", "second line"], "hash_range_inclusive": ["aB3", "aB3"], "path": "src/main.ts" }
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
⚠️ Common mistake: do not copy the `HASH│` prefix into `content_lines`.
|
|
109
|
-
|
|
110
|
-
Wrong:
|
|
111
|
-
```json
|
|
112
|
-
{ "content_lines": ["F4T│import { x } from \"./x\";"], "hash_range_inclusive": ["F4T", "F4T"] }
|
|
113
|
-
|
|
114
|
-
Right:
|
|
115
|
-
```json
|
|
116
|
-
{ "content_lines": ["import { x } from \"./x\";"], "hash_range_inclusive": ["F4T", "F4T"] }
|
|
117
|
-
|
|
118
|
-
`hash_range_inclusive` uses the hash anchor. `content_lines` uses literal file content only — the same text that appears after the `│` in `read` output.
|
|
119
|
-
|
|
120
|
-
⚠️ Common mistake: `hash_range_inclusive` is only the 3-character HASH, not the full `HASH│content` line.
|
|
121
|
-
|
|
122
|
-
Wrong:
|
|
123
|
-
```json
|
|
124
|
-
{ "content_lines": [...], "hash_range_inclusive": ["F4T│import { x } from \"./x\";", "F4T│import { x } from \"./x\";"] }
|
|
125
|
-
|
|
126
|
-
Right:
|
|
127
|
-
```json
|
|
128
|
-
{ "content_lines": [...], "hash_range_inclusive": ["F4T", "F4T"] }
|
|
129
|
-
|
|
130
|
-
⚠️ Common mistake: do not serialize `content_lines` as a JSON string.
|
|
131
|
-
|
|
132
|
-
Wrong:
|
|
133
|
-
```json
|
|
134
|
-
{ "content_lines": "[\"line1\", \"line2\"]", "hash_range_inclusive": ["F4T", "F4T"] }
|
|
135
|
-
|
|
136
|
-
Right:
|
|
137
|
-
```json
|
|
138
|
-
{ "content_lines": ["line1", "line2"], "hash_range_inclusive": ["F4T", "F4T"] }
|
|
139
|
-
|
|
140
|
-
`content_lines` must be a native JSON array of strings, not a string that looks like an array. Pass it as a proper JSON array value.
|
|
141
|
-
|
|
142
|
-
Rules:
|
|
143
|
-
- `hash_range_inclusive` is a pair `[start, end]`. A single-line replace is `hash_range_inclusive: ["X", "X"]`.
|
|
144
|
-
- To delete a range, use `content_lines: []`.
|
|
145
|
-
- `hash_range_inclusive` elements are HASH anchors only (e.g. `aB3`). Do not include `│` or line content.
|
|
146
|
-
- `content_lines` is literal file content — each string becomes exactly one line in the file. No `HASH│` prefix. A line that happens to start with `+` or `-` is written as-is; the only rejected form is the diff preview's `+HASH│…` row (see `[E_INVALID_PATCH]`).
|
|
147
|
-
- **Preserve leading whitespace (indentation) exactly.** The content after `│` in read output includes all leading spaces and tabs — copy them into `content_lines` unchanged. Dropping indentation will produce broken code.
|
|
148
|
-
- Don't add `""` for spacing unless you actually want a new blank line.
|
|
149
|
-
- Copy anchors from the most recent `read` of the file. Do not guess or construct them.
|
|
150
|
-
- All changes in one call must be non-conflicting. The runtime rejects with `[E_EDIT_CONFLICT]` if two ranges overlap.
|
|
151
|
-
- If `content_lines` matches current content, the replace is classified as `noop` (file unchanged).
|
|
152
|
-
- The `hash_range_inclusive` is inclusive — the entire span from the first anchor through the second anchor is deleted and replaced with `content_lines`. The old lines in that span are gone. If your replacement content includes lines that already exist in the file (e.g. closing brackets), make sure those lines are within your range, otherwise they will appear twice.
|
|
153
|
-
- `changes` (in bulk mode), `hash_range_inclusive`, and `content_lines` must be native JSON values, not JSON strings. Do not serialize them — pass them as proper arrays and strings.
|
|
154
|
-
On success, the response text shows the line change summary (e.g. "Added 3 line(s), removed 1 line(s).") plus any warnings if present. {{AUTO_READ_GUIDANCE}}
|
|
155
|
-
|
|
156
|
-
⚠️ Common mistake: `hash_range_inclusive` replaces the ENTIRE range. Every line from the first anchor through the second anchor is deleted and replaced with `content_lines`. Do not include "context" or "surrounding" lines in `content_lines` — they are outside the range and will be preserved automatically.
|
|
157
|
-
|
|
158
|
-
Wrong: To replace lines 2-3 in this function:
|
|
159
|
-
```
|
|
160
|
-
function greet() {
|
|
161
|
-
const x = 1;
|
|
162
|
-
return x;
|
|
163
|
-
}
|
|
164
|
-
```
|
|
165
|
-
A model might write:
|
|
166
|
-
```json
|
|
167
|
-
{ "content_lines": [" const y = 2;", " return y;", "}"], "hash_range_inclusive": ["X", "Y"] }
|
|
168
|
-
This produces `function greet() {\n const y = 2;\n return y;\n}\n}` — the `}` appears twice because it was already on line 4 (outside the range) AND in `content_lines`.
|
|
169
|
-
|
|
170
|
-
Right: Only include the new lines that belong in the range:
|
|
171
|
-
```json
|
|
172
|
-
{ "content_lines": [" const y = 2;", " return y;"], "hash_range_inclusive": ["X", "Y"] }
|
|
173
|
-
The `}` on line 4 is outside the range and stays in place.
|
|
174
|
-
|
|
175
|
-
Error recovery:
|
|
176
|
-
- `[E_STALE_ANCHOR]` — the anchored line's content changed since the last read. Call `read` to get fresh anchors, then copy the 3-char HASH of the start and end of the range you are replacing into `hash_range_inclusive` and retry. (Staleness is per-line: editing or appending lines does not invalidate anchors for lines whose content is unchanged, so anchors for untouched regions stay valid across edits.)
|
|
177
|
-
- `[E_BAD_REF]` — malformed HASH. Re-read and try again.
|
|
178
|
-
- `[E_BAD_OP]` — invalid operation (e.g. start line > end line).
|
|
179
|
-
- `[E_BAD_SHAPE]` — malformed request or change item (missing fields, wrong types, unknown fields).
|
|
180
|
-
- `[E_LEGACY_SHAPE]` — old `oldText`/`newText` or `old_text`/`new_text` format detected. Use `{content_lines, hash_range_inclusive}` instead.
|
|
181
|
-
- `[E_EDIT_CONFLICT]` — two changes overlap on the same line range. Make changes non-overlapping.
|
|
182
|
-
- `[E_AMBIGUOUS_ANCHOR]` — hash collision. Call `read` to get fresh anchors.
|
|
183
|
-
- `[E_BARE_HASH_PREFIX]` — a `content_lines` entry starts with `HASH│`. Remove the hash prefix; keep only the literal line content that appears after `│` in `read` output. `content_lines` uses file content only, `hash_range_inclusive` uses hash anchors.
|
|
184
|
-
- `[E_INVALID_PATCH]` — a `content_lines` entry matches the diff preview's `+HASH│…` addition-row form. Use literal file content. (Plain `+`/`-` lines are not rejected — they are written literally.)
|
|
185
|
-
- `[E_WOULD_EMPTY]` — edit would empty a non-empty file.
|
|
186
|
-
- `[E_FILE_TOO_LARGE]` — file exceeds the 1,000,000-line edit limit. Use `write` or a non-line-based approach for very large files.
|
|
187
|
-
|
|
188
|
-
**Undo:** If a replace produced incorrect results, call `undo_last_replace` with the file path to revert the last replace. The tool reports how many lines were removed and restored. After undoing, call `read` to get fresh anchors for a corrected replace.
|