pi-hashline-edit-pro 0.13.3 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -12
- package/index.ts +47 -5
- package/package.json +1 -1
- package/prompts/replace-bulk-guidelines.md +4 -0
- package/prompts/replace-bulk-snippet.md +1 -0
- package/prompts/replace-bulk.md +130 -0
- package/prompts/replace-flat-guidelines.md +4 -0
- package/prompts/replace-flat-snippet.md +1 -0
- package/prompts/replace-flat.md +113 -0
- package/prompts/replace-guidelines.md +3 -2
- package/prompts/replace-snippet.md +1 -1
- package/prompts/replace.md +49 -4
- package/src/config.ts +97 -0
- package/src/hashline/apply.ts +6 -4
- package/src/prompts.ts +19 -13
- package/src/replace-flat.ts +313 -0
- package/src/replace-normalize.ts +44 -24
- package/src/replace-response.ts +5 -2
- package/src/replace.ts +256 -241
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# pi-hashline-edit-pro
|
|
2
2
|
|
|
3
|
-
A [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) extension that replaces the built-in `read` and `edit` tools with a hash-anchored line-replacing workflow. Strict semantics, no silent relocation, no autocorrection, no fuzzy fallback.
|
|
3
|
+
A [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) extension that replaces the built-in `read` and `edit` tools with a hash-anchored line-replacing workflow. Strict semantics, no silent relocation, no autocorrection, no fuzzy fallback. Every line gets a unique content hash, so edits stay precise and stale anchors are caught before they reach the file.
|
|
4
4
|
|
|
5
|
-
Fork of [pi-hashline-edit](https://github.com/RimuruW/pi-hashline-edit) by RimuruW. The strict-semantics policy is unchanged. This fork extends the upstream design
|
|
5
|
+
Fork of [pi-hashline-edit](https://github.com/RimuruW/pi-hashline-edit) by RimuruW. The strict-semantics policy is unchanged. This fork extends the upstream design with 3-character hashes and collision resolution for unique per-line anchors.
|
|
6
6
|
|
|
7
7
|
Every line returned by `read` carries a short content hash. Edits reference those hashes instead of raw text, so the tool can detect stale context and reject outdated changes before they reach the file.
|
|
8
8
|
|
|
@@ -12,8 +12,8 @@ The original uses 2-character hashes of a 16-character alphabet, with the hash b
|
|
|
12
12
|
|
|
13
13
|
This fork makes two changes that compound:
|
|
14
14
|
|
|
15
|
-
1. **
|
|
16
|
-
2. **Perfect hashing (collision resolution).** When computing hashes for a file, if a line's base hash collides with an already-assigned hash, the hash is incremented (using a retry counter: `:R{retry}`) until a unique hash is found. This ensures every line
|
|
15
|
+
1. **3-character hash length** over a 64-char URL-safe base64 alphabet (up from 2 characters in the upstream), expanding the hash space from 256 to 262,144 buckets.
|
|
16
|
+
2. **Perfect hashing (collision resolution).** When computing hashes for a file, if a line's base hash collides with an already-assigned hash, the hash is incremented (using a retry counter: `:R{retry}`) until a unique hash is found. This ensures every line gets a unique anchor, even within a 3-character hash space. Two byte-identical lines (e.g. repeated `}` or repeated `import` statements) get different hashes automatically.
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
19
19
|
|
|
@@ -49,7 +49,7 @@ szJ│ console.log("world");
|
|
|
49
49
|
_zl│}
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
- `HASH` is a 3-character content hash from the URL-safe base64 alphabet `A-Za-z0-9-_` (e.g. `aB3`).
|
|
52
|
+
- `HASH` is a 3-character content hash from the URL-safe base64 alphabet `A-Za-z0-9-_` (e.g. `aB3`). See [Hashing](#hashing) for details.
|
|
53
53
|
|
|
54
54
|
Optional parameters:
|
|
55
55
|
|
|
@@ -60,7 +60,9 @@ Images (JPEG, PNG, GIF, WebP) are passed through as attachments and do not parti
|
|
|
60
60
|
|
|
61
61
|
### `replace` -- hash-anchored modifications
|
|
62
62
|
|
|
63
|
-
Replaces using the `HASH│content` anchors from `read` output to target lines precisely:
|
|
63
|
+
Replaces using the `HASH│content` anchors from `read` output to target lines precisely. Two modes are available, toggled via `/toggle-replace-mode` (persists across sessions):
|
|
64
|
+
|
|
65
|
+
**Bulk mode (default):** `hash_range_inclusive` and `content_lines` go inside a `changes` array, supporting multiple edits in one call.
|
|
64
66
|
|
|
65
67
|
```json
|
|
66
68
|
{
|
|
@@ -71,18 +73,28 @@ Replaces using the `HASH│content` anchors from `read` output to target lines p
|
|
|
71
73
|
}
|
|
72
74
|
```
|
|
73
75
|
|
|
76
|
+
**Flat mode:** `hash_range_inclusive` and `content_lines` sit at the top level. Only one edit per call.
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"path": "src/main.ts",
|
|
81
|
+
"hash_range_inclusive": ["ve7", "ve7"],
|
|
82
|
+
"content_lines": [" console.log('hashline');"]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
74
86
|
| Field | Description |
|
|
75
87
|
| --- | --- |
|
|
76
88
|
| `hash_range_inclusive` | Inclusive line range `[start_hash, end_hash]` (required). |
|
|
77
89
|
| `content_lines` | Literal replacement content, one string per line (use `[]` to delete the range). |
|
|
78
90
|
|
|
79
|
-
- **Request structure validation.** The request envelope (`path`, `changes`) and individual edit items are validated before any file I/O. Unknown fields, missing required fields, invalid types, and malformed anchors are rejected with `[E_BAD_SHAPE]` or `[E_BAD_REF]`.
|
|
91
|
+
- **Request structure validation.** The request envelope (`path`, `changes` in bulk mode; `path`, `hash_range_inclusive`, `content_lines` in flat mode) and individual edit items are validated before any file I/O. Unknown fields, missing required fields, invalid types, and malformed anchors are rejected with `[E_BAD_SHAPE]` or `[E_BAD_REF]`.
|
|
80
92
|
- **Legacy dialect rejected.** The native top-level `oldText`/`newText` (and `old_text`/`new_text`) dialect is rejected with `[E_LEGACY_SHAPE]`. The error message tells the model to call `read` first and send `{hash_range_inclusive: ["<START>", "<END>"], content_lines: [...]}`.
|
|
81
|
-
- **Batched atomicity.** All edits in a single call validate against the same pre-edit snapshot and apply bottom-up, so the hashes from a single `read` call remain valid across all edits in the batch.
|
|
93
|
+
- **Batched atomicity (bulk mode).** All edits in a single call validate against the same pre-edit snapshot and apply bottom-up, so the hashes from a single `read` call remain valid across all edits in the batch.
|
|
82
94
|
|
|
83
95
|
### Chained edits
|
|
84
96
|
|
|
85
|
-
After a successful replace, the response
|
|
97
|
+
After a successful replace, the response confirms with `Successfully replaced in {path}.` (warnings are still shown if present). To get fresh anchors for follow-up edits, call `read` on the file first. This avoids token overhead from re-displaying content the model already knows.
|
|
86
98
|
|
|
87
99
|
### Auto-read after write
|
|
88
100
|
|
|
@@ -91,7 +103,7 @@ Auto-read is **disabled by default**. When enabled, after a successful `write` t
|
|
|
91
103
|
1. `write` a file, result includes hashline anchors
|
|
92
104
|
2. `replace` using those anchors directly
|
|
93
105
|
|
|
94
|
-
Toggle at runtime with the `/toggle-auto-read` command. The
|
|
106
|
+
Toggle at runtime with the `/toggle-auto-read` command. The setting persists across sessions in the config file (`~/.config/pi-hashline-edit-pro/config.json`). Set `PI_HASHLINE_AUTO_READ=1` to enable by default on first run.
|
|
95
107
|
|
|
96
108
|
For large files (>2000 lines), the auto-read output is truncated with a pagination hint. Use `read` with `offset` to see more.
|
|
97
109
|
|
|
@@ -99,6 +111,26 @@ For large files (>2000 lines), the auto-read output is truncated with a paginati
|
|
|
99
111
|
|
|
100
112
|
The post-edit diff (with `+`/`-` markers) is exposed to the host UI via `details.diff`. It is intentionally not in the LLM-visible text. The model already knows what it changed and can call `read` for fresh anchors when needed.
|
|
101
113
|
|
|
114
|
+
### Commands
|
|
115
|
+
|
|
116
|
+
| Command | Description |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `/toggle-replace-mode` | Switch between bulk mode (`changes` array) and flat mode (top-level fields). Persists across sessions. |
|
|
119
|
+
| `/toggle-auto-read` | Toggle automatic hashline anchors after write operations. Persists across sessions. |
|
|
120
|
+
|
|
121
|
+
### Config file
|
|
122
|
+
|
|
123
|
+
Settings are stored in `~/.config/pi-hashline-edit-pro/config.json`:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"replaceMode": "bulk",
|
|
128
|
+
"autoRead": false
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The file is created automatically when any setting is toggled. Both fields are independent — toggling one never clobbers the other.
|
|
133
|
+
|
|
102
134
|
## Design Decisions
|
|
103
135
|
|
|
104
136
|
- **Stale anchors fail (per-line).** A hash mismatch means that specific line's content changed since the last `read`; the error tells the model to call `read()` to get fresh anchors, then copy the 3-character HASH of the start and end of the range being replaced into `hash_range_inclusive` of the next replace call. Because staleness is per-line, editing or appending lines does **not** invalidate anchors for lines whose content is unchanged — anchors for untouched regions stay valid across edits to other regions.
|
|
@@ -107,6 +139,7 @@ The post-edit diff (with `+`/`-` markers) is exposed to the host UI via `details
|
|
|
107
139
|
- **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.
|
|
108
140
|
- **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.
|
|
109
141
|
- **Boundary duplication warnings.** When the last line of a replacement matches the next surviving line (or the first line matches the preceding one), a warning is emitted. This catches a common LLM pattern where closing delimiters like `}`, `});`, or `} else {` are accidentally duplicated. The warning is a short header followed by a hashline-anchored window: the duplicated pair plus 2 lines of context before and after, shown as plain `HASH│content` rows with post-edit hashes. Seeing the duplication in context (rather than just being told a hash) is what prompts the model to act on it — and since the hashes are current and staleness is per-line, the model can remove the duplicate in one follow-up `replace` without re-reading. No autocorrection is applied — the duplicate stays in the file and the model decides whether to remove it. Raw line comparison (not trimmed) avoids false positives when indentation differs.
|
|
142
|
+
- **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.
|
|
110
143
|
|
|
111
144
|
## Hashing
|
|
112
145
|
|
|
@@ -122,7 +155,7 @@ The runtime always precomputes the full per-line hash array for a file via `line
|
|
|
122
155
|
|
|
123
156
|
### Bare-prefix detector
|
|
124
157
|
|
|
125
|
-
With the `│` delimiter format, the bare-prefix detector regex `^\s*[A-Za-z0-9_\-]{3}│` is highly specific. It only matches lines starting with
|
|
158
|
+
With the `│` delimiter format, the bare-prefix detector regex `^\s*[A-Za-z0-9_\-]{3}│` is highly specific. It only matches lines starting with a hash-like prefix. This eliminates false positives from common code patterns like `init:`, `data:`, `else:`, etc. The detector rejects edit lines matching this pattern with `[E_BARE_HASH_PREFIX]` to prevent the model from accidentally pasting hash anchors into file content.
|
|
126
159
|
|
|
127
160
|
## Development
|
|
128
161
|
|
|
@@ -135,7 +168,7 @@ npm test
|
|
|
135
168
|
|
|
136
169
|
Set `PI_HASHLINE_DEBUG=1` to show an "active" notification at session start.
|
|
137
170
|
|
|
138
|
-
Set `PI_HASHLINE_AUTO_READ=1` to enable auto-read after write by default (can still be toggled at runtime with `/toggle-auto-read
|
|
171
|
+
Set `PI_HASHLINE_AUTO_READ=1` to enable auto-read after write by default on first run (can still be toggled at runtime with `/toggle-auto-read`; the setting persists across sessions once toggled).
|
|
139
172
|
|
|
140
173
|
## Credits
|
|
141
174
|
|
package/index.ts
CHANGED
|
@@ -3,33 +3,76 @@ import { readFile } from "fs/promises";
|
|
|
3
3
|
import { join, isAbsolute } from "path";
|
|
4
4
|
import { initHasher } from "./src/hashline";
|
|
5
5
|
import { regReplace } from "./src/replace";
|
|
6
|
+
import { regReplaceFlat } from "./src/replace-flat";
|
|
6
7
|
import { regRead, fmtReadPreview } from "./src/read";
|
|
7
8
|
import { toLF, stripBOM } from "./src/replace-diff";
|
|
8
9
|
import { visLines } from "./src/utils";
|
|
9
10
|
import { AUTO_READ_MAX } from "./src/constants";
|
|
11
|
+
import {
|
|
12
|
+
readReplaceMode,
|
|
13
|
+
toggleReplaceMode,
|
|
14
|
+
readAutoRead,
|
|
15
|
+
toggleAutoRead,
|
|
16
|
+
} from "./src/config";
|
|
10
17
|
|
|
11
18
|
export default function (pi: ExtensionAPI): void {
|
|
12
19
|
regRead(pi);
|
|
20
|
+
|
|
21
|
+
// Register the bulk-mode replace tool by default. The session_start handler
|
|
22
|
+
// will re-register with the correct mode from the persisted config.
|
|
13
23
|
regReplace(pi);
|
|
14
24
|
|
|
15
25
|
const debugValue = process.env.PI_HASHLINE_DEBUG;
|
|
26
|
+
// Initial auto-read from env var; session_start overrides with persisted value
|
|
27
|
+
const autoReadValue = process.env.PI_HASHLINE_AUTO_READ;
|
|
28
|
+
let autoRead = autoReadValue === "1" || autoReadValue === "true";
|
|
16
29
|
|
|
17
30
|
pi.on("session_start", async (_event, ctx) => {
|
|
18
31
|
const active = pi.getActiveTools();
|
|
19
32
|
pi.setActiveTools(active.filter((t) => t !== "edit"));
|
|
20
33
|
await initHasher();
|
|
34
|
+
|
|
35
|
+
// Re-register the replace tool according to the persisted mode
|
|
36
|
+
const mode = await readReplaceMode();
|
|
37
|
+
if (mode === "flat") {
|
|
38
|
+
regReplaceFlat(pi);
|
|
39
|
+
} else {
|
|
40
|
+
regReplace(pi);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Read the persisted auto-read setting (overrides env var default)
|
|
44
|
+
autoRead = await readAutoRead();
|
|
45
|
+
|
|
21
46
|
if (debugValue === "1" || debugValue === "true") {
|
|
22
|
-
ctx.ui.notify(
|
|
47
|
+
ctx.ui.notify(`Hashline Edit mode active (${mode} replace)`, "info");
|
|
23
48
|
}
|
|
24
49
|
});
|
|
25
50
|
|
|
26
|
-
|
|
27
|
-
|
|
51
|
+
pi.registerCommand("toggle-replace-mode", {
|
|
52
|
+
description: "Toggle replace tool between bulk (changes array) and flat (single edit at top level) mode",
|
|
53
|
+
handler: async (_args, ctx) => {
|
|
54
|
+
const mode = await toggleReplaceMode();
|
|
55
|
+
// Re-register the tool with the new mode
|
|
56
|
+
if (mode === "flat") {
|
|
57
|
+
regReplaceFlat(pi);
|
|
58
|
+
} else {
|
|
59
|
+
regReplace(pi);
|
|
60
|
+
}
|
|
61
|
+
ctx.ui.notify(`Replace mode switched to: ${mode}`, "info");
|
|
62
|
+
},
|
|
63
|
+
});
|
|
28
64
|
|
|
29
65
|
pi.registerCommand("toggle-auto-read", {
|
|
30
66
|
description: "Toggle automatic hashline anchors after write operations",
|
|
31
67
|
handler: async (_args, ctx) => {
|
|
32
|
-
autoRead =
|
|
68
|
+
autoRead = await toggleAutoRead();
|
|
69
|
+
// Re-register the tool so prompts reflect the new auto-read setting
|
|
70
|
+
const mode = await readReplaceMode();
|
|
71
|
+
if (mode === "flat") {
|
|
72
|
+
regReplaceFlat(pi);
|
|
73
|
+
} else {
|
|
74
|
+
regReplace(pi);
|
|
75
|
+
}
|
|
33
76
|
const state = autoRead ? "enabled" : "disabled";
|
|
34
77
|
ctx.ui.notify(`Auto-read after write: ${state}`, "info");
|
|
35
78
|
},
|
|
@@ -63,5 +106,4 @@ export default function (pi: ExtensionAPI): void {
|
|
|
63
106
|
console.error("Auto-read after write failed:", error);
|
|
64
107
|
}
|
|
65
108
|
});
|
|
66
|
-
|
|
67
109
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,4 @@
|
|
|
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 text is empty (warnings only). {{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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Replace lines in a text file via HASH anchors from read, batching all changes to a file in one call
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Replace lines in a text file using HASH anchors from `read`.
|
|
2
|
+
|
|
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
|
+
|
|
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
|
+
{ "path": "src/main.ts", "changes": [
|
|
15
|
+
{ "hash_range_inclusive": ["MQX", "MQX"], "content_lines": ["const x = 99;"] }
|
|
16
|
+
] }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
|
|
21
|
+
1. Single line replace:
|
|
22
|
+
```json
|
|
23
|
+
{ "path": "src/main.ts", "changes": [
|
|
24
|
+
{ "hash_range_inclusive": ["MQX", "MQX"], "content_lines": ["const x = 1;"] }
|
|
25
|
+
] }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
2. Range replace (3 lines → 3 new lines):
|
|
29
|
+
```json
|
|
30
|
+
{ "path": "src/main.ts", "changes": [
|
|
31
|
+
{ "hash_range_inclusive": ["ZPM", "VRW"], "content_lines": [
|
|
32
|
+
"function greet(name) {",
|
|
33
|
+
" return `Hello, ${name}`;",
|
|
34
|
+
"}"
|
|
35
|
+
] }
|
|
36
|
+
] }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
3. Multiple regions in one call (delete two non-adjacent ranges):
|
|
40
|
+
```json
|
|
41
|
+
{ "path": "src/server.ts", "changes": [
|
|
42
|
+
{ "hash_range_inclusive": ["aB3", "xY7"], "content_lines": [] },
|
|
43
|
+
{ "hash_range_inclusive": ["MQX", "ZPM"], "content_lines": [] }
|
|
44
|
+
] }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
4. Append after the last line (include the old last line so the new line is added after it):
|
|
48
|
+
```json
|
|
49
|
+
{ "path": "src/main.ts", "changes": [
|
|
50
|
+
{ "hash_range_inclusive": ["ZPM", "ZPM"], "content_lines": ["old last line", "new line"] }
|
|
51
|
+
] }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
5. Seed content into an empty file (replace the single empty-line hash returned by read):
|
|
55
|
+
```json
|
|
56
|
+
{ "path": "src/main.ts", "changes": [
|
|
57
|
+
{ "hash_range_inclusive": ["aB3", "aB3"], "content_lines": ["first line", "second line"] }
|
|
58
|
+
] }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
⚠️ Common mistake: do not copy the `HASH│` prefix into `content_lines`.
|
|
62
|
+
|
|
63
|
+
Wrong:
|
|
64
|
+
```json
|
|
65
|
+
{ "hash_range_inclusive": ["F4T", "F4T"], "content_lines": ["F4T│import { x } from \"./x\";"] }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Right:
|
|
69
|
+
```json
|
|
70
|
+
{ "hash_range_inclusive": ["F4T", "F4T"], "content_lines": ["import { x } from \"./x\";"] }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`hash_range_inclusive` uses the hash anchor. `content_lines` uses literal file content only — the same text that appears after the `│` in `read` output.
|
|
74
|
+
|
|
75
|
+
⚠️ Common mistake: `hash_range_inclusive` is only the 3-character HASH, not the full `HASH│content` line.
|
|
76
|
+
|
|
77
|
+
Wrong:
|
|
78
|
+
```json
|
|
79
|
+
{ "hash_range_inclusive": ["F4T│import { x } from \"./x\";", "F4T│import { x } from \"./x\";"], "content_lines": [...] }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Right:
|
|
83
|
+
```json
|
|
84
|
+
{ "hash_range_inclusive": ["F4T", "F4T"], "content_lines": [...] }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Rules:
|
|
88
|
+
- `hash_range_inclusive` is a pair `[start, end]`. A single-line replace is `hash_range_inclusive: ["X", "X"]`.
|
|
89
|
+
- To delete a range, use `content_lines: []`.
|
|
90
|
+
- `hash_range_inclusive` elements are HASH anchors only (e.g. `aB3`). Do not include `│` or line content.
|
|
91
|
+
- `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]`).
|
|
92
|
+
- Don't add `""` for spacing unless you actually want a new blank line.
|
|
93
|
+
- Copy anchors from the most recent `read` of the file. Do not guess or construct them.
|
|
94
|
+
- All changes in one call must be non-conflicting. The runtime rejects with `[E_EDIT_CONFLICT]` if two ranges overlap.
|
|
95
|
+
- If `content_lines` matches current content, the replace is classified as `noop` (file unchanged).
|
|
96
|
+
- 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.
|
|
97
|
+
On success, the response text is empty (or contains only warnings if present). {{AUTO_READ_GUIDANCE}}
|
|
98
|
+
|
|
99
|
+
⚠️ 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.
|
|
100
|
+
|
|
101
|
+
Wrong: To replace lines 2-3 in this function:
|
|
102
|
+
```
|
|
103
|
+
function greet() {
|
|
104
|
+
const x = 1;
|
|
105
|
+
return x;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
A model might write:
|
|
109
|
+
```json
|
|
110
|
+
{ "hash_range_inclusive": ["X", "Y"], "content_lines": [" const y = 2;", " return y;", "}"] }
|
|
111
|
+
```
|
|
112
|
+
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`.
|
|
113
|
+
|
|
114
|
+
Right: Only include the new lines that belong in the range:
|
|
115
|
+
```json
|
|
116
|
+
{ "hash_range_inclusive": ["X", "Y"], "content_lines": [" const y = 2;", " return y;"] }
|
|
117
|
+
```
|
|
118
|
+
The `}` on line 4 is outside the range and stays in place.
|
|
119
|
+
|
|
120
|
+
Error recovery:
|
|
121
|
+
- `[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.)
|
|
122
|
+
- `[E_BAD_REF]` — malformed HASH. Re-read and try again.
|
|
123
|
+
- `[E_BAD_OP]` — invalid operation (e.g. start line > end line).
|
|
124
|
+
- `[E_BAD_SHAPE]` — malformed request or change item (missing fields, wrong types, unknown fields).
|
|
125
|
+
- `[E_LEGACY_SHAPE]` — old `oldText`/`newText` or `old_text`/`new_text` format detected. Use `{hash_range_inclusive, content_lines}` instead.
|
|
126
|
+
- `[E_EDIT_CONFLICT]` — two changes overlap on the same line range. Make changes non-overlapping.
|
|
127
|
+
- `[E_AMBIGUOUS_ANCHOR]` — hash collision. Call `read` to get fresh anchors.
|
|
128
|
+
- `[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. `hash_range_inclusive` uses hashes, `content_lines` does not.
|
|
129
|
+
- `[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.)
|
|
130
|
+
- `[E_WOULD_EMPTY]` — edit would empty a non-empty file.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
- Use `replace` with HASH anchors for all file changes. Only one edit per call (flat mode — no `changes` array).
|
|
2
|
+
- After a successful `replace`, the response text is empty (warnings only). {{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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Replace lines in a text file via HASH anchors from read, one edit per call (flat mode)
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
|
|
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
|
+
{ "path": "src/main.ts", "hash_range_inclusive": ["MQX", "MQX"], "content_lines": ["const x = 99;"] }
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
|
|
17
|
+
1. Single line replace:
|
|
18
|
+
```json
|
|
19
|
+
{ "path": "src/main.ts", "hash_range_inclusive": ["MQX", "MQX"], "content_lines": ["const x = 1;"] }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
2. Range replace (3 lines → 3 new lines):
|
|
23
|
+
```json
|
|
24
|
+
{ "path": "src/main.ts", "hash_range_inclusive": ["ZPM", "VRW"], "content_lines": [
|
|
25
|
+
"function greet(name) {",
|
|
26
|
+
" return `Hello, ${name}`;",
|
|
27
|
+
"}"
|
|
28
|
+
] }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
3. Delete a range:
|
|
32
|
+
```json
|
|
33
|
+
{ "path": "src/server.ts", "hash_range_inclusive": ["aB3", "xY7"], "content_lines": [] }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
4. Append after the last line (include the old last line so the new line is added after it):
|
|
37
|
+
```json
|
|
38
|
+
{ "path": "src/main.ts", "hash_range_inclusive": ["ZPM", "ZPM"], "content_lines": ["old last line", "new line"] }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
5. Seed content into an empty file (replace the single empty-line hash returned by read):
|
|
42
|
+
```json
|
|
43
|
+
{ "path": "src/main.ts", "hash_range_inclusive": ["aB3", "aB3"], "content_lines": ["first line", "second line"] }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
⚠️ Common mistake: do not copy the `HASH│` prefix into `content_lines`.
|
|
47
|
+
|
|
48
|
+
Wrong:
|
|
49
|
+
```json
|
|
50
|
+
{ "hash_range_inclusive": ["F4T", "F4T"], "content_lines": ["F4T│import { x } from \"./x\";"] }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Right:
|
|
54
|
+
```json
|
|
55
|
+
{ "hash_range_inclusive": ["F4T", "F4T"], "content_lines": ["import { x } from \"./x\";"] }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`hash_range_inclusive` uses the hash anchor. `content_lines` uses literal file content only — the same text that appears after the `│` in `read` output.
|
|
59
|
+
|
|
60
|
+
⚠️ Common mistake: `hash_range_inclusive` is only the 3-character HASH, not the full `HASH│content` line.
|
|
61
|
+
|
|
62
|
+
Wrong:
|
|
63
|
+
```json
|
|
64
|
+
{ "hash_range_inclusive": ["F4T│import { x } from \"./x\";", "F4T│import { x } from \"./x\";"], "content_lines": [...] }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Right:
|
|
68
|
+
```json
|
|
69
|
+
{ "hash_range_inclusive": ["F4T", "F4T"], "content_lines": [...] }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Rules:
|
|
73
|
+
- `hash_range_inclusive` is a pair `[start, end]`. A single-line replace is `hash_range_inclusive: ["X", "X"]`.
|
|
74
|
+
- To delete a range, use `content_lines: []`.
|
|
75
|
+
- `hash_range_inclusive` elements are HASH anchors only (e.g. `aB3`). Do not include `│` or line content.
|
|
76
|
+
- `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]`).
|
|
77
|
+
- Don't add `""` for spacing unless you actually want a new blank line.
|
|
78
|
+
- Copy anchors from the most recent `read` of the file. Do not guess or construct them.
|
|
79
|
+
- If `content_lines` matches current content, the replace is classified as `noop` (file unchanged).
|
|
80
|
+
- 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.
|
|
81
|
+
On success, the response text is empty (or contains only warnings if present). {{AUTO_READ_GUIDANCE}}
|
|
82
|
+
|
|
83
|
+
⚠️ 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.
|
|
84
|
+
|
|
85
|
+
Wrong: To replace lines 2-3 in this function:
|
|
86
|
+
```
|
|
87
|
+
function greet() {
|
|
88
|
+
const x = 1;
|
|
89
|
+
return x;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
A model might write:
|
|
93
|
+
```json
|
|
94
|
+
{ "hash_range_inclusive": ["X", "Y"], "content_lines": [" const y = 2;", " return y;", "}"] }
|
|
95
|
+
```
|
|
96
|
+
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`.
|
|
97
|
+
|
|
98
|
+
Right: Only include the new lines that belong in the range:
|
|
99
|
+
```json
|
|
100
|
+
{ "hash_range_inclusive": ["X", "Y"], "content_lines": [" const y = 2;", " return y;"] }
|
|
101
|
+
```
|
|
102
|
+
The `}` on line 4 is outside the range and stays in place.
|
|
103
|
+
|
|
104
|
+
Error recovery:
|
|
105
|
+
- `[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.)
|
|
106
|
+
- `[E_BAD_REF]` — malformed HASH. Re-read and try again.
|
|
107
|
+
- `[E_BAD_OP]` — invalid operation (e.g. start line > end line).
|
|
108
|
+
- `[E_BAD_SHAPE]` — malformed request or change item (missing fields, wrong types, unknown fields).
|
|
109
|
+
- `[E_LEGACY_SHAPE]` — old `oldText`/`newText` or `old_text`/`new_text` format detected. Use `{hash_range_inclusive, content_lines}` instead.
|
|
110
|
+
- `[E_AMBIGUOUS_ANCHOR]` — hash collision. Call `read` to get fresh anchors.
|
|
111
|
+
- `[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. `hash_range_inclusive` uses hashes, `content_lines` does not.
|
|
112
|
+
- `[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.)
|
|
113
|
+
- `[E_WOULD_EMPTY]` — edit would empty a non-empty file.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
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 text is empty (warnings only).
|
|
2
|
+
- After a successful `replace`, the response text is empty (warnings only). {{AUTO_READ_GUIDANCE}}
|
|
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
|
-
- `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.
|
|
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
|
+
- Two modes are available: bulk (default, uses `changes` array) and flat (top-level `hash_range_inclusive`/`content_lines`). Toggle with `/toggle-replace-mode`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Replace lines in a text file via HASH anchors from read, batching all changes to a file in one call
|
|
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
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
Replace lines in a text file using HASH anchors from `read`.
|
|
2
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
|
+
|
|
3
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.
|
|
4
12
|
|
|
5
13
|
How to use:
|
|
@@ -10,33 +18,58 @@ read({ path: "src/main.ts" })
|
|
|
10
18
|
```
|
|
11
19
|
|
|
12
20
|
2. Copy the 3-character HASH (before `│`) into `hash_range_inclusive`:
|
|
21
|
+
|
|
22
|
+
**Bulk mode:**
|
|
13
23
|
```json
|
|
14
24
|
{ "path": "src/main.ts", "changes": [
|
|
15
25
|
{ "hash_range_inclusive": ["MQX", "MQX"], "content_lines": ["const x = 99;"] }
|
|
16
26
|
] }
|
|
17
27
|
```
|
|
18
28
|
|
|
29
|
+
**Flat mode:**
|
|
30
|
+
```json
|
|
31
|
+
{ "path": "src/main.ts", "hash_range_inclusive": ["MQX", "MQX"], "content_lines": ["const x = 99;"] }
|
|
32
|
+
```
|
|
33
|
+
|
|
19
34
|
Examples:
|
|
20
35
|
|
|
21
36
|
1. Single line replace:
|
|
37
|
+
|
|
38
|
+
Bulk:
|
|
22
39
|
```json
|
|
23
40
|
{ "path": "src/main.ts", "changes": [
|
|
24
41
|
{ "hash_range_inclusive": ["MQX", "MQX"], "content_lines": ["const x = 1;"] }
|
|
25
42
|
] }
|
|
26
43
|
```
|
|
27
44
|
|
|
45
|
+
Flat:
|
|
46
|
+
```json
|
|
47
|
+
{ "path": "src/main.ts", "hash_range_inclusive": ["MQX", "MQX"], "content_lines": ["const x = 1;"] }
|
|
48
|
+
```
|
|
49
|
+
|
|
28
50
|
2. Range replace (3 lines → 3 new lines):
|
|
51
|
+
|
|
52
|
+
Bulk:
|
|
29
53
|
```json
|
|
30
54
|
{ "path": "src/main.ts", "changes": [
|
|
31
55
|
{ "hash_range_inclusive": ["ZPM", "VRW"], "content_lines": [
|
|
32
56
|
"function greet(name) {",
|
|
33
57
|
" return `Hello, ${name}`;",
|
|
34
58
|
"}"
|
|
35
|
-
}
|
|
59
|
+
] }
|
|
60
|
+
] }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Flat:
|
|
64
|
+
```json
|
|
65
|
+
{ "path": "src/main.ts", "hash_range_inclusive": ["ZPM", "VRW"], "content_lines": [
|
|
66
|
+
"function greet(name) {",
|
|
67
|
+
" return `Hello, ${name}`;",
|
|
68
|
+
"}"
|
|
36
69
|
] }
|
|
37
70
|
```
|
|
38
71
|
|
|
39
|
-
3. Multiple regions in one call (
|
|
72
|
+
3. Multiple regions in one call (bulk mode only — flat mode supports one edit per call):
|
|
40
73
|
```json
|
|
41
74
|
{ "path": "src/server.ts", "changes": [
|
|
42
75
|
{ "hash_range_inclusive": ["aB3", "xY7"], "content_lines": [] },
|
|
@@ -46,20 +79,32 @@ Examples:
|
|
|
46
79
|
|
|
47
80
|
4. Append after the last line (include the old last line so the new line is added after it):
|
|
48
81
|
|
|
82
|
+
Bulk:
|
|
49
83
|
```json
|
|
50
84
|
{ "path": "src/main.ts", "changes": [
|
|
51
85
|
{ "hash_range_inclusive": ["ZPM", "ZPM"], "content_lines": ["old last line", "new line"] }
|
|
52
86
|
] }
|
|
53
87
|
```
|
|
54
88
|
|
|
89
|
+
Flat:
|
|
90
|
+
```json
|
|
91
|
+
{ "path": "src/main.ts", "hash_range_inclusive": ["ZPM", "ZPM"], "content_lines": ["old last line", "new line"] }
|
|
92
|
+
```
|
|
93
|
+
|
|
55
94
|
5. Seed content into an empty file (replace the single empty-line hash returned by read):
|
|
56
95
|
|
|
96
|
+
Bulk:
|
|
57
97
|
```json
|
|
58
98
|
{ "path": "src/main.ts", "changes": [
|
|
59
99
|
{ "hash_range_inclusive": ["aB3", "aB3"], "content_lines": ["first line", "second line"] }
|
|
60
100
|
] }
|
|
61
101
|
```
|
|
62
102
|
|
|
103
|
+
Flat:
|
|
104
|
+
```json
|
|
105
|
+
{ "path": "src/main.ts", "hash_range_inclusive": ["aB3", "aB3"], "content_lines": ["first line", "second line"] }
|
|
106
|
+
```
|
|
107
|
+
|
|
63
108
|
⚠️ Common mistake: do not copy the `HASH│` prefix into `content_lines`.
|
|
64
109
|
|
|
65
110
|
Wrong:
|
|
@@ -96,7 +141,7 @@ Rules:
|
|
|
96
141
|
- All changes in one call must be non-conflicting. The runtime rejects with `[E_EDIT_CONFLICT]` if two ranges overlap.
|
|
97
142
|
- If `content_lines` matches current content, the replace is classified as `noop` (file unchanged).
|
|
98
143
|
- 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.
|
|
99
|
-
On success, the response text is empty (or contains only warnings if present).
|
|
144
|
+
On success, the response text is empty (or contains only warnings if present). {{AUTO_READ_GUIDANCE}}
|
|
100
145
|
|
|
101
146
|
⚠️ 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.
|
|
102
147
|
|
|
@@ -129,4 +174,4 @@ Error recovery:
|
|
|
129
174
|
- `[E_AMBIGUOUS_ANCHOR]` — hash collision. Call `read` to get fresh anchors.
|
|
130
175
|
- `[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. `hash_range_inclusive` uses hashes, `content_lines` does not.
|
|
131
176
|
- `[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.)
|
|
132
|
-
- `[E_WOULD_EMPTY]` — edit would empty a non-empty file.
|
|
177
|
+
- `[E_WOULD_EMPTY]` — edit would empty a non-empty file.
|