pi-hashline-edit-pro 3.0.4 → 4.0.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.
package/README.md CHANGED
@@ -2,24 +2,25 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/pi-hashline-edit-pro.svg)](https://www.npmjs.com/package/pi-hashline-edit-pro) [![npm downloads](https://img.shields.io/npm/dm/pi-hashline-edit-pro.svg)](https://www.npmjs.com/package/pi-hashline-edit-pro)
4
4
 
5
- Anchor-based `read`, `replace`, `insert`, and `anchor_grep` tools for [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent). Every line of a file gets a unique 4-character anchor, and you edit by anchor. There are no line numbers and no fuzzy matching, so edits land on the lines you meant.
5
+ pi-hashline-edit-pro is an extension for [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) that edits files by anchor. Every line a tool shows you is prefixed with a unique 4-character anchor, and you edit by anchor. There are no line numbers and no fuzzy matching, so an edit lands on the line you meant.
6
6
 
7
- Fork of [pi-hashline-edit](https://github.com/RimuruW/pi-hashline-edit) by RimuruW, extended with 4-character tokenizer-friendly anchors and collision resolution.
7
+ It is a fork of [pi-hashline-edit](https://github.com/RimuruW/pi-hashline-edit) by RimuruW, extended with 4-character tokenizer-friendly anchors and allocation-based anchor identity.
8
8
 
9
- ## Features
9
+ ## Installation
10
10
 
11
- - `read` returns every line as `anchor│content`. The anchor is the line's address.
12
- - `replace` targets a range of anchors, so edits land on the lines you meant.
13
- - `insert` adds lines after or before a line by anchor: the anchor line is preserved and the new lines are applied literally, never deduplicated.
14
- - `anchor_grep` returns matching lines (and requested context) with `anchor│content` rows that are served like read output, so search results are immediately editable.
15
- - Editing one part of a file leaves the anchors of the rest unchanged, so anchors from an earlier read stay valid across edits.
16
- - After a `write` you get the new anchors. After a `replace` or `insert` you get the diff with the new anchors.
17
- - The most recent replace or insert on a file can be reverted, even after a restart.
18
- - Permissions, line endings, BOMs, symlinks, and hard links survive every edit.
11
+ ```bash
12
+ pi install npm:pi-hashline-edit-pro
13
+ ```
19
14
 
20
- ## Quick start
15
+ To install from a local checkout:
21
16
 
22
- 1. Read a file:
17
+ ```bash
18
+ pi install /path/to/pi-hashline-edit-pro
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Read a file. Every line comes back as `anchor│content`:
23
24
 
24
25
  ```text
25
26
  Dafo│function hello() {
@@ -27,174 +28,153 @@ Emno│ console.log("world");
27
28
  HDtm│}
28
29
  ```
29
30
 
30
- 2. Replace a line by its anchor:
31
+ Replace a line by its anchor:
31
32
 
32
33
  ```json
33
34
  {
34
- "path": "src/main.ts",
35
35
  "remove_from": "Emno",
36
36
  "remove_to": "Emno",
37
37
  "replacement_lines": [" console.log('hi');"]
38
38
  }
39
39
  ```
40
40
 
41
- 3. Keep editing. Anchors for lines you didn't touch stay valid, and auto-read returns fresh anchors after each change.
41
+ The result is the post-edit diff with fresh anchors, so you can keep editing without re-reading. Lines you did not touch keep their anchors. After a `write`, an auto-read block gives you the new anchors. The most recent `replace` or `insert` on a file can be reverted, even after a restart.
42
42
 
43
- ## Installation
43
+ The extension registers five tools: `read`, `replace`, `insert`, `anchor_grep`, and `undo_last_change`. The built-in `edit` tool is disabled. `replace` and `insert` take no `path` parameter: the file is resolved from the anchors' session ownership alone, so an edit can only land on the file the anchors were served for.
44
44
 
45
- ```bash
46
- pi install npm:pi-hashline-edit-pro
47
- ```
48
-
49
- From a local checkout:
50
-
51
- ```bash
52
- pi install /path/to/pi-hashline-edit-pro
53
- ```
54
-
55
- ## The read tool
45
+ ### read
56
46
 
57
- `read` returns a text file with every line prefixed by `anchor│content`. The anchor is 4 characters from `A-Za-z0-9` (for example `Hasu`), drawn from a curated table of two-character tokens so every anchor costs 2 tokens in the major tokenizers.
47
+ `read` returns a text file with every line prefixed by `anchor│content`. The anchor is the line's address.
58
48
 
59
49
  | Parameter | Description |
60
50
  | --- | --- |
61
- | `offset` | Start reading from this line number (1-indexed). |
51
+ | `path` | Path to the file (relative or absolute). |
52
+ | `offset` | Line number to start reading from (1-indexed). |
62
53
  | `limit` | Maximum number of lines to return. |
63
54
 
64
- Paged output ends with a continuation hint, for example `[Showing lines 1-50 of 120. Use offset=51 to continue.]`.
55
+ Output is capped at 2000 lines and 50KB. Paged output ends with a continuation hint, for example `[Showing lines 1-50 of 120. Use offset=51 to continue.]`.
65
56
 
66
- Lines up to 50KB are shown in full. A larger line is replaced by a marker that keeps the line's anchor: `anchor│[Line N is 2.2MB, exceeds 50KB; content not shown. Use bash: sed -n 'Np' <path> | head -c 51200]`. The marker is served like a normal row, so the whole line can still be replaced via that anchor; `anchor_grep` shows an anchored fragment around a match on such a line instead.
57
+ A line over 50KB is replaced by a marker that keeps the line's anchor: `anchor│[Line N is 2.2MB, exceeds 50KB; content not shown. Use bash: sed -n 'Np' <path> | head -c 51200]`. The marker is served like a normal row, so the whole line can still be replaced through it.
67
58
 
68
59
  Edge cases:
69
60
 
70
- - Images (JPEG, PNG, GIF, WebP, BMP) come back as visual attachments. Other image formats (for example AVIF, HEIC/HEIF, TIFF, ICO, JPEG 2000, JPEG XL, PSD, APNG) are rejected as binary, since the built-in renderer cannot attach them.
71
- - Binary files and directories are rejected with a descriptive error. A magic-signature match is ignored when the sampled bytes contain no NUL bytes and decode as UTF-8, so a text file whose first bytes happen to match a binary or image signature (for example starting with `BM` or `8BPS`) is still read as text. The NUL-byte check covers the whole file, not just the sampled bytes: a file with a NUL byte anywhere is rejected as binary.
72
- - UTF-16 and UTF-32 text (detected via BOM) is rejected, since editing it would corrupt the file.
73
- - Empty files come back as a single empty-line anchor (`anchor│`); use `replace` on that anchor to insert content.
61
+ - Images (JPEG, PNG, GIF, WebP, BMP) come back as visual attachments. Other image formats (AVIF, HEIC/HEIF, TIFF, ICO, JPEG 2000, JPEG XL, PSD, APNG) are rejected as binary, since the built-in renderer cannot attach them.
62
+ - Binary files and directories are rejected. A magic-signature match is ignored when the sampled bytes contain no NUL and decode as UTF-8, so a text file that happens to start with `BM` or `8BPS` still reads as text. A NUL byte anywhere rejects the file.
63
+ - UTF-16 and UTF-32 text (detected by BOM) is rejected, since editing it would corrupt the file.
64
+ - An empty file comes back as one empty-line row (`anchor│`); replace that anchor to insert content.
74
65
  - BOMs are stripped for display. Non-UTF-8 bytes are shown as `U+FFFD`; editing such a file rewrites it as UTF-8, with a warning.
75
- - Files over 257,795 lines or 100MB are rejected with `[E_FILE_TOO_LARGE]`.
66
+ - Files over 1,353,139 lines or 100MB are rejected with `[E_FILE_TOO_LARGE]`.
76
67
 
77
- ## The replace tool
68
+ ### replace
78
69
 
79
- The built-in `edit` tool is disabled. `replace` and `insert` are the only edit paths, and both take the anchors from `read` output.
70
+ `replace` removes a range of lines and puts new lines in their place. One edit per call, with the fields at the top level:
71
+
72
+ | Field | Description |
73
+ | --- | --- |
74
+ | `remove_from` | 4-char anchor marking the FIRST line to remove (inclusive). |
75
+ | `remove_to` | 4-char anchor marking the LAST line to remove (inclusive). |
76
+ | `replacement_lines` | Replacement lines, one element per line. Mirror the removed lines exactly, blank lines included: `[]` deletes the range, `[""]` is a single blank line, `["a", ""]` is a line followed by a blank line. Never embed `\n` inside an element. |
80
77
 
81
- One edit per call, with `remove_from`, `remove_to`, and `replacement_lines` at the top level:
78
+ Example: read showed `Hasu│old` and `arvm│old2`; to replace both:
82
79
 
83
80
  ```json
84
81
  {
85
- "path": "src/main.ts",
86
- "remove_from": "Emno",
87
- "remove_to": "HDtm",
88
- "replacement_lines": [" console.log('hi');", "}"]
82
+ "remove_from": "Hasu",
83
+ "remove_to": "arvm",
84
+ "replacement_lines": ["new line 1", "new line 2"]
89
85
  }
90
86
  ```
91
87
 
92
- | Field | Description |
93
- | --- | --- |
94
- | `path` | Path to edit; always provide it explicitly — it is only auto-resolved from the anchors as a fallback. |
95
- | `remove_from` | 4-char anchor from `read` output marking the FIRST line to remove (inclusive). |
96
- | `remove_to` | 4-char anchor from `read` output marking the LAST line to remove (inclusive). |
97
- | `replacement_lines` | Replacement lines as an array of strings, one element per line. Mirror the removed lines exactly, blank lines included: use `[]` to delete the range, `[""]` for a single blank line, `["a", ""]` for a line followed by a blank line, and `["", ""]` for two blank lines. Do not embed `\n` inside an element: each element is exactly one line. |
88
+ Single line: use the same anchor for `remove_from` and `remove_to`. `replace_from`/`replace_to` work as aliases.
98
89
 
99
- Notes:
90
+ The request is checked before any file I/O, so a bad request never touches the file.
100
91
 
101
- - The request is checked before any file I/O, so a bad request never touches the file.
102
- - Common copy-paste slips are fixed automatically and reported: a leftover `anchor│` prefix (including a truncated or expanded prefix of up to 6 characters, e.g. `L3│` or `ab12│`) in `replacement_lines` or `remove_from`/`remove_to`, diff-preview rows pasted into the replacement, a reversed range, or a boundary line pasted twice. New lines that re-include a block adjacent to the range are stripped automatically when that block is unique in the file. The whole run is stripped as one unit (including repeated structural lines like `}`), so re-including an unchanged block next to the range never duplicates it. A missing `path` is resolved from the anchors when they identify known files in the hash store (reported as a warning); when the anchors match multiple known files the most recently touched file is picked, with up to 3 candidate paths named. `file_path` works as an alias for `path` in all five tools, and `replace_from`/`replace_to` work as aliases for `remove_from`/`remove_to` in `replace`.
103
- - An edit that produces identical content reports `No changes made` and leaves the anchors alone. When such a noop happened because a boundary anti-duplication cut removed lines from the replacement (the cut blocked a line that duplicates the block next to the range from being added), the same replacement sent once more runs with the edge anti-duplication turned off for that single call and is applied literally. The duplicated lines are kept, and the result carries a `[W_BOUNDARY_BYPASS]` notice. The pending bypass is per file and keyed to that payload; copied `anchor│` prefixes, diff markers, and stray whitespace in the resend are normalized before matching, so a copy-paste resend still hits it. Any applied edit clears it, and a successful `write` also clears it.
104
- - Every line in the removed range must match what was last shown to you. The extension records the `anchor│content` rows it serves (`read` output, the auto-read block after `write`, the `+anchor│`/` anchor│` rows of post-edit diffs (replace, insert, and undo), the current-range rows of `[E_RANGE_STALE]` feedback, and the context rows of stale/ambiguous-anchor feedback) and verifies the whole range against that record before writing. If an interior line changed on disk since it was shown (external editor, formatter-on-save, code generation) or was never shown, the edit is refused with `[E_RANGE_STALE]` and the current range is returned with fresh anchors, so the retry needs no `read`. Edits outside the served record are only possible for files that were never read (for example right after a `write` with auto-read disabled); once the file has been served, every replaced line must have been shown.
105
- - After a successful edit you get the post-edit diff with fresh anchors, so you can keep editing without re-reading. The diff is capped at 50KB: a row longer than 50KB is shown as a marker that keeps the row's anchor (so the line stays editable via the diff), and when the total cap is hit the diff ends with a truncation note. Only the rows shown in the capped diff are recorded as served. The same caps apply to the `insert` and `undo_last_change` diffs, to the interactive previews, and to `details.patch` (which is flagged with `details.patchTruncated` when it was cut and can no longer be applied as-is).
106
- - Do not issue multiple replace or insert calls on the same file in one message; parallel edits split attention across the post-edit diffs and removed lines are easy to miss. Verify each diff before the next edit on that file.
92
+ Common copy-paste slips are fixed automatically and reported as warnings: a leftover `anchor│` prefix in `replacement_lines` or the anchor fields (any prefix of 1 to 8 characters before `│`, for example `L3│` or `ab12│`), diff-preview rows pasted into the replacement, a reversed range, and a boundary line pasted twice. New lines that re-include a block adjacent to the range are stripped when that block is unique in the file. The whole run is stripped as one unit, so re-including an unchanged block next to the range never duplicates it.
107
93
 
108
- ## The insert tool
94
+ Every line in the removed range must match what was last shown to you. The extension records the `anchor│content` rows it serves (`read` output, `anchor_grep` output, the auto-read block after `write`, the `+anchor│` and ` anchor│` rows of post-edit diffs, the current-range rows of `[E_RANGE_STALE]` feedback, and the context rows of stale-anchor feedback) and verifies the whole range against that record before writing. A line that changed on disk since it was shown, or an anchor that is not owned in this session, refuses the edit with `[E_RANGE_STALE]` or `[E_STALE_ANCHOR]` and returns the current range with fresh anchors, so the retry needs no `read`. An owned anchor enters the served record when its row is shown (after a restart, restored ownership counts as shown), so a file with no owned anchors cannot be edited by anchor at all; call `read` first. An owned line that was never shown — for example beyond an auto-read preview's truncation cap — is refused with `[E_RANGE_STALE]` and returns the current range, so the retry still needs no `read`.
109
95
 
110
- `insert` adds lines after or before an existing line without removing anything. The anchor line is preserved, and the new lines go after it (`direction: "after"`) or before it (`direction: "before"`):
96
+ An edit that produces identical content reports `No changes made` and leaves the anchors alone. When a noop happened because the boundary anti-duplication cut a line from the replacement, sending the same replacement once more runs with that dedup turned off for the single call and applies the lines literally; the result carries a `[W_BOUNDARY_BYPASS]` notice. The pending bypass is per file and keyed to the payload; copied prefixes, diff markers, and stray whitespace are normalized before matching. Any applied edit or successful `write` clears it.
111
97
 
112
- ```json
113
- {
114
- "path": "src/main.ts",
115
- "anchor": "Emno",
116
- "direction": "after",
117
- "lines": [" console.log('hi');"]
118
- }
119
- ```
98
+ After a successful edit, the diff is capped at 50KB. A row over 50KB is shown as a marker that keeps the row's anchor, and only the rows shown in the capped diff are recorded as served. The same caps apply to the `insert` and `undo_last_change` diffs, to the interactive previews, and to `details.patch`.
99
+
100
+ Do not issue multiple `replace` or `insert` calls on the same file in one message. Parallel edits split attention across the post-edit diffs, and removed lines are easy to miss. Verify each diff before the next edit on that file.
101
+
102
+ ### insert
103
+
104
+ `insert` adds lines after or before an existing line without removing anything. Like `replace`, there is no `path` parameter.
120
105
 
121
106
  | Field | Description |
122
107
  | --- | --- |
123
- | `path` | Path to edit; always provide it explicitly it is only auto-resolved from the anchors as a fallback. |
124
- | `anchor` | 4-char anchor from `read` output marking the line next to which the lines go (inclusive; the line is preserved). A pasted diff row like `+Hasu│x` or an `anchor│` prefix is stripped automatically with a warning. |
125
- | `direction` | `"after"` to insert below the anchor line, `"before"` to insert above it. |
126
- | `lines` | Lines to insert as an array of strings, one element per line. Mirror `replacement_lines` semantics: use `[""]` for a blank line and do not embed `\n` inside an element. The anchor line is never part of `lines`. |
108
+ | `anchor` | 4-char anchor marking the line next to which the lines go. The anchor line is preserved. A pasted `+Hasu│x` diff row or `anchor│` prefix is stripped automatically with a warning. |
109
+ | `direction` | `"after"` inserts below the anchor line, `"before"` above it. |
110
+ | `lines` | Lines to insert, one element per line. `[""]` is a blank line. Never include the anchor line, and never embed `\n` inside an element. |
127
111
 
128
- Notes:
112
+ Lines are applied literally: nothing is removed, and a line that duplicates its neighbor is kept. `replace`'s boundary anti-duplication never runs for `insert`. Inserting nothing (`lines: []`) reports a noop. To seed an empty file, read it and insert after the `anchor│` empty-line row.
129
113
 
130
- - A missing `path` is resolved from the anchor the same way as `replace` (reported as a warning); when the anchor matches multiple known files the most recently touched file is picked, with up to 3 candidate paths named.
131
- - The anchor line must have been shown to you (read output, a post-edit diff row, anchor_grep output, or stale-range feedback). The same verification as `replace` applies: a stale or unshown anchor is rejected with `[E_STALE_ANCHOR]`, `[E_AMBIGUOUS_ANCHOR]`, or `[E_RANGE_STALE]` and the retry needs no `read`.
132
- - Lines are applied literally: nothing is removed, and a line that duplicates its neighbor is kept. `replace`'s boundary anti-duplication never runs for `insert`.
133
- - To seed an empty file, read it and insert after the `anchor│` empty-line row.
134
- - The same safety machinery as `replace` applies: undo is saved before the write (a failed write restores the previous undo record), line endings and BOMs survive, and an applied insert clears a pending boundary bypass.
135
- - Inserting nothing (`lines: []`) reports a noop and leaves the file unchanged; inserted lines are never deduplicated.
114
+ The same safety machinery as `replace` applies: undo is saved before the write (a failed write restores the previous undo record), line endings and BOMs survive, and an applied insert clears a pending boundary bypass.
136
115
 
137
- ## The anchor_grep tool
116
+ ### anchor_grep
138
117
 
139
- When enabled, `anchor_grep` replaces the built-in grep with an anchored search backed by ripgrep; it is disabled by default (see below). While `anchor_grep` is enabled, the built-in grep is disabled; disabling `anchor_grep` restores it if it was active before the extension loaded. Every matching line (and each requested context line) is returned as `lineNumber │ anchor│content` — the `anchor│content` part is served exactly like `read` output, so you can target it with `replace`/`insert` without a separate `read`, while the line-number gutter and `=== path ===` header give filename and line for navigation (press Return to jump).
118
+ `anchor_grep` is an anchored search backed by ripgrep. It is disabled by default; enable it with `/toggle-anchor-grep` (or set `anchorGrepEnabled` to `true` in the config file). While it is enabled, the built-in grep is disabled. Disabling it removes the tool and restores the built-in grep only if that was active before the extension loaded.
119
+
120
+ Every matching line, and each requested context line, is returned as `lineNumber │ anchor│content`. The `anchor│content` part is served exactly like `read` output, so you can target it with `replace` or `insert` without a separate `read`; the line-number gutter and `=== path ===` header give filename and line for navigation.
140
121
 
141
122
  | Field | Description |
142
123
  | --- | --- |
143
124
  | `pattern` | Search pattern (regex, or literal text when `literal` is true). |
144
- | `path` | File or directory to search (default: the current working directory). |
145
- | `glob` | Filter files by glob pattern; `*` matches across directories, e.g. `*.ts` or `**/*.spec.ts`. A leading `/` is ignored, and the pattern may be relative to the search root or to the current directory. |
125
+ | `path` | File or directory to search (default: the current working directory). `file_path` works as an alias. |
126
+ | `glob` | Filter files by glob; `*` matches across directories, for example `*.ts` or `**/*.spec.ts`. A leading `/` is ignored, and the pattern may be relative to the search root or the current directory. |
146
127
  | `ignoreCase` | Case-insensitive search (default: false). |
147
128
  | `literal` | Treat the pattern as literal text instead of a regex (default: false). |
148
- | `context` | Lines of context before and after each match; context rows carry anchors too (default: 0). |
129
+ | `context` | Lines of context before and after each match (default: 0). Context rows carry anchors too. |
149
130
  | `limit` | Maximum number of matched lines to return (default: 100). |
150
131
 
151
- Notes:
152
- - Results are grouped per file under a `=== path ===` header; every shown row is `lineNumber │ anchor│content` where `anchor│content` is the anchor it would have in `read` output.
153
- - Directory searches use ripgrep, respecting `.gitignore` (including parent directories); `.git` is always skipped, and hidden files are searched, so `node_modules`, `.tmp`, and `coverage` are skipped only when a `.gitignore` lists them. Binary, image, and oversized files are skipped silently.
154
- - Regex patterns with backreferences, nested quantifiers, quantified alternation, or multiple variable quantifiers are rejected with `[E_UNSAFE_REGEX]` before any files are scanned; use `literal: true` when regex behavior is unnecessary.
155
- - Output is capped at `limit` matched lines, 2000 rows, and 50KB of text (whichever comes first), with a hint naming the cap that cut results. A matched line longer than 500 bytes is shown as a fragment around the match with `...` marking the truncated sides, so the relevant part of the hit stays visible; a context line over 500 bytes is shown as its head with a trailing `...`. Fragments keep the line's anchor (long lines are hashed from their first 500 bytes) and are served like full rows, so a fragmented match is still editable with `replace` (which always replaces the whole line).
156
- - `file_path` works as an alias for `path`.
157
- - Line endings and BOMs survive every edit. The file's line ending is detected from its first newline and restored on write; a file that mixes LF and CRLF (for example a WSL-edited file) is normalized to the first-seen ending.
158
- - Files with multiple hard links (`nlink > 1`) are rewritten in place rather than via a temp-file rename, so every link keeps seeing the same content; that write is direct rather than atomic.
159
- - The anchor_grep tool is disabled by default. Enable it with `/toggle-anchor-grep` (or set `anchorGrepEnabled` to `true` in the config file); the setting persists across sessions. While enabled, the built-in grep is disabled; disabling anchor_grep removes it from the model's toolset and restores the built-in grep only if it was active before the extension loaded — a grep tool that was never enabled stays off.
132
+ Directory searches respect `.gitignore` (including parent directories); `.git` is always skipped, and hidden files are searched. `node_modules`, `.tmp`, and `coverage` are skipped only when a `.gitignore` lists them. Binary, image, and oversized files are skipped silently.
133
+
134
+ Regexes with backreferences, nested quantifiers, quantified alternation, or multiple variable quantifiers are rejected with `[E_UNSAFE_REGEX]` before any files are scanned. Use `literal: true` when regex behavior is unnecessary.
135
+
136
+ Output is capped at `limit` matched lines, 2000 rows, and 50KB of text, whichever comes first, with a note naming the cap that cut the results. A matched line over 500 bytes is shown as a fragment around the match, with `...` marking the truncated sides; a context line over 500 bytes is shown as its head with a trailing `...`. Fragments keep the line's anchor (long lines are hashed from their first 500 bytes) and are served like full rows, so a fragmented match is still editable, and `replace` always replaces the whole line.
160
137
 
161
- ## Undo
138
+ ### undo_last_change
162
139
 
163
140
  `undo_last_change` reverts the most recent successful `replace` or `insert` on a file, restoring the exact previous content, BOM and line endings included, plus the previous anchors.
164
141
 
165
- - History is per-file and single-level: only the most recent replace or insert can be reverted.
142
+ - History is per-file and single-level: only the most recent `replace` or `insert` can be reverted.
166
143
  - History is persisted and survives session restarts. A failed `write` does not clear it.
167
- - Every applied replace or insert is undoable: the undo record is saved before the edit is written.
144
+ - Every applied `replace` or `insert` is undoable; the undo record is saved before the edit is written.
168
145
  - A successful `write` clears the history for that file.
169
- - If the file was modified since the last replace or insert, the undo is refused rather than overwriting those changes. The undo record is kept: once the file matches the edited state again (for example you revert the external change), `undo_last_change` succeeds.
170
- - If the file was deleted since the last replace or insert, `undo_last_change` restores it from the recorded pre-edit content. Nothing is overwritten, since the file no longer exists.
171
- - Missing-file cleanup never touches the undo record: the per-session prune of the hash store removes the snapshots and served records of files that no longer exist (both are recomputed on the next read), but the undo history survives even when the file is temporarily absent, for example during a branch switch.
146
+ - If the file was modified since the last edit, the undo is refused with `[E_UNDO_STALE]` rather than overwriting those changes, and the record is kept. Once the file matches the edited state again, `undo_last_change` succeeds.
147
+ - If the file was deleted since the last edit, `undo_last_change` restores it from the recorded pre-edit content.
148
+ - Missing-file cleanup never touches the undo record. The per-session prune removes snapshots and served records of files that no longer exist (both are recomputed on the next read), but the undo history survives, even when the file is temporarily absent during a branch switch.
172
149
 
173
- ## Auto-read
150
+ ### Auto-read
174
151
 
175
- Enabled by default. After a successful `write`, the extension reads the file and appends an `--- Auto-read (hashline anchors) ---` block to the result, so you get fresh `anchor│content` anchors without a separate `read` call.
152
+ Auto-read is enabled by default. After a successful `write`, the extension reads the file and appends an `--- Auto-read (hashline anchors) ---` block, so you get fresh `anchor│content` anchors without a separate `read` call.
176
153
 
177
- - After `replace`, `insert`, and `undo_last_change`, the result shows the post-edit diff. The `+anchor│` and ` anchor│` rows carry the current anchors, so follow-up edits can anchor on the diff directly. The `-anchor│` rows show removed lines with their old anchors, so you can see exactly which anchors were deleted (those anchors are stale after the edit). When the context line touching a change is blank or whitespace-only, one more context line is shown in that direction, so the change stays anchored to visible content. Call `read` when you want the full file's anchors.
178
- - Auto-read keeps the same 50KB / 2000-line budget as `read`. Lines over 50KB are shown as markers that keep the line's anchor (use `anchor_grep` for a fragment around a match).
179
- - Toggle at runtime with `/toggle-auto-read`; the setting persists across sessions.
154
+ After `replace`, `insert`, and `undo_last_change`, the result shows the post-edit diff. The `+anchor│` and ` anchor│` rows carry the current anchors, so follow-up edits can anchor on the diff directly. The `-anchor│` rows show removed lines with their old anchors, which are stale after the edit. When the context line next to a change is blank or whitespace-only, one more context line is shown in that direction, so the change stays anchored to visible content. Call `read` when you want the full file's anchors.
155
+
156
+ Auto-read keeps the same 50KB and 2000-line budget as `read`. Toggle it at runtime with `/toggle-auto-read`; both settings persist across sessions.
180
157
 
181
158
  ## Tool result details
182
159
 
183
- All five tools return machine-readable metadata in `details` alongside the model-visible text:
160
+ All five tools return machine-readable metadata in `details` alongside the model-visible text.
184
161
 
185
- - `read`: `details.truncation` (set when the output was truncated), `details.snapshotId` (a `v2|path|ino|mtime|ctime|size` fingerprint of the file), `details.nextOffset` (use as the next `offset`), and `details.metrics` with `truncated` and `next_offset`.
186
- - `replace` and `insert`: `details.diff` (the post-edit diff, capped at 50KB with markers for oversized rows; `+HASH│` and ` HASH│` rows carry the current anchors), `details.patch` (a standard unified patch of the changes, for external tools, capped at 50KB like the diff), `details.patchTruncated` (true when the patch was cut to fit the cap and cannot be applied as-is), `details.firstChangedLine`, `details.snapshotId`, `details.classification` (`"noop"` when nothing changed), and `details.metrics`: `edits_attempted`, `edits_noop`, `warnings`, `classification` (`"applied"` or `"noop"`), `changed_lines` (`{ first, last }`), `added_lines`, `removed_lines`.
187
- - `undo_last_change`: `details.diff` (the undo diff with the restored anchors), `details.patch` (a standard unified patch of the restored changes, capped at 50KB like `replace`), `details.patchTruncated` (true when the patch was cut), and `details.metrics` (same shape as `replace`).
188
- - `anchor_grep`: `details.metrics` with `matches` (matched lines found, capped at `limit`), `files`, and `truncated` (true when the row, byte, file-scan, or `limit` cap cut the results), plus `details.truncation` (the standard pi truncation report `truncatedBy`, `totalLines`, `outputLines`, `maxLines`, `maxBytes`, — when the output was cut) and `details.linesTruncated` (true when long lines were shown as fragments).
162
+ | Tool | `details` |
163
+ | --- | --- |
164
+ | `read` | `truncation` (set when output was truncated), `snapshotId` (a `v2\|path\|ino\|mtime\|ctime\|size` fingerprint), `nextOffset` (use as the next `offset`), and `metrics` with `truncated` and `next_offset`. |
165
+ | `replace`, `insert` | `diff` (post-edit diff, capped, with current anchors on `+HASH│` and ` HASH│` rows), `patch` (a standard unified patch for external tools, capped like the diff), `patchTruncated` (true when the patch was cut and can no longer be applied as-is), `firstChangedLine`, `snapshotId`, `classification` (`"noop"` when nothing changed), and `metrics`: `edits_attempted`, `edits_noop`, `warnings`, `classification` (`"applied"` or `"noop"`), `changed_lines` (`{ first, last }`), `added_lines`, `removed_lines`. |
166
+ | `undo_last_change` | `diff` (the undo diff with restored anchors), `patch`, `patchTruncated`, and `metrics` in the same shape as `replace`. |
167
+ | `anchor_grep` | `metrics` with `matches` (capped at `limit`), `files`, and `truncated`; `truncation` (the standard pi truncation report) when output was cut; and `linesTruncated` (true when long lines were shown as fragments). |
189
168
 
190
169
  ## Settings
191
170
 
192
171
  | Command | Description |
193
172
  | --- | --- |
194
- | `/toggle-auto-read` | Toggle auto-read anchors after write and post-edit diffs after replace, insert, and undo_last_change. Persists across sessions. |
195
- | `/toggle-anchor-grep` | Enable or disable the anchor_grep tool (the built-in grep is disabled while anchor_grep is on). Persists across sessions. |
173
+ | `/toggle-auto-read` | Toggle auto-read anchors after `write` and post-edit diffs after `replace`, `insert`, and `undo_last_change`. Persists across sessions. |
174
+ | `/toggle-anchor-grep` | Enable or disable the `anchor_grep` tool. The built-in grep is disabled while `anchor_grep` is on. Persists across sessions. |
175
+ | `/clear-anchors` | Clear the session's anchor claims. Anchors are re-claimed on the next `read`. |
196
176
 
197
- Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created automatically when a setting is toggled. On non-Windows platforms, the config directory honors `XDG_CONFIG_HOME` when set (falling back to `~/.config`); on Windows it always uses `~/.config`:
177
+ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a setting is first toggled:
198
178
 
199
179
  ```json
200
180
  {
@@ -203,27 +183,29 @@ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created automatic
203
183
  }
204
184
  ```
205
185
 
206
- ## How anchors work
186
+ On non-Windows platforms the directory honors `XDG_CONFIG_HOME` when set (falling back to `~/.config`); on Windows it always uses `~/.config`.
207
187
 
208
- Each line is canonicalized (carriage returns stripped, trailing whitespace trimmed) and hashed with [xxhash-wasm](https://github.com/jungomi/xxhash-wasm) (xxHash32), then mapped to a 4-character anchor from a frozen table of 257,795 anchors. The canonicalization keeps anchors stable across editor-save cycles that add or remove trailing whitespace. A line longer than 500 bytes is hashed from its first 500 bytes; uniqueness is still guaranteed by the collision-resolution below.
188
+ ## How anchors work
209
189
 
210
- The table is curated for tokenizers, not for humans: every anchor is the concatenation of two 2-character sequences that are single tokens in the o200k, cl100k, GPT-2, Llama, and Mistral vocabularies (verified offline against each full vocabulary), and the 4-character concatenation itself is verified to encode as exactly 2 tokens in all of them. An anchor therefore costs 2 tokens on a read row and 2 tokens in an edit call, with the `│` separator as the only overhead. Anchors are letters only; the digits dropped out because SentencePiece vocabularies have no two-digit tokens. The table is generated by `scripts/generate-anchor-table.py` and shipped as `src/hashline/anchor-table.json`.
190
+ Anchors are allocated, never derived. Every line that is served to you, by `read`, `anchor_grep`, the auto-read block after `write`, or a post-edit diff, gets the next free anchor from the session's pool, claimed by walking the table with a large stride (roughly the golden ratio of the anchor space) coprime to it, so consecutively minted anchors land in unrelated regions of the table instead of sharing leading characters. Ownership is exclusive: an anchor is owned by one file's line until it is freed (the line was edited, the file was written or deleted, or you ran `/clear-anchors`). Minting prefers anchors the session has never used; when every unused anchor has been spent, freed anchors are recycled after their stale served records are purged, so an anchor is never shared by two live lines. Because ownership is exclusive, an anchor resolves to exactly one file. Two byte-identical lines never share an anchor, and that guarantee sets the file size cap: at most 1,353,139 lines per file, beyond which `read`, `replace`, and `insert` reject with `[E_FILE_TOO_LARGE]` (use `write` for very large files).
211
191
 
212
- Anchors are unique by construction. If a line's base hash collides with an already-assigned hash, the next free hash is allocated from a bitset by probing with a stride coprime to the hash space (O(1) amortized). The stride advances both anchor halves, so consecutive collisions runs of blank lines, repeated `}` never share their first two characters. Every line in a file therefore gets a unique anchor; two byte-identical lines (repeated `}`, repeated `import` statements) never share one. The same guarantee sets the file size cap: at most 257,795 lines per file, beyond which `read`, `replace`, and `insert` reject with `[E_FILE_TOO_LARGE]` (use `write` for very large files).
192
+ The table is curated for tokenizers, not for humans. Every anchor is the concatenation of two 2-character pieces that each encode as a single token, and beside the `│` separator the whole 5-character `anchor│` unit is verified to tokenize as exactly three tokens in each of eight modern open-weights tokenizers (Qwen 3.5, DeepSeek V4, Gemma 4, GLM 5.3 Flash, Tencent Hy4-preview, MiniMax M3, MiMo V2.5, Kimi K3). The shipped table is the intersection that satisfies the criterion on all of them; Nemotron 3 Ultra is the one modern tokenizer excluded. An anchor therefore costs 2 tokens on a read row and 2 in an edit call, with the `│` separator as the third. Anchors are letters only. The table is shipped as `src/hashline/anchor-table.json`.
213
193
 
214
- Hashes live in a persistent per-file store (`~/.config/pi-hashline-edit-pro/hash-store.sqlite`) that keeps the hashes of unchanged lines across edits. When a range is replaced, the runtime maps the old content onto the new content and copies hashes for lines that survived; only genuinely new lines get fresh hashes.
194
+ Each line also carries a content checksum. The line is canonicalized (carriage returns stripped, trailing whitespace trimmed) and hashed with [xxhash-wasm](https://github.com/jungomi/xxhash-wasm). The canonicalization keeps the checksum stable across editor-save cycles that add or remove trailing whitespace. A line over 500 bytes is hashed from its first 500 bytes.
215
195
 
216
- On POSIX systems, the state directory is restricted to mode `0700` and the SQLite database plus its WAL/SHM sidecars to `0600`. The undo table contains the complete pre-edit and post-edit text for the latest edit to each file, so the store should still be treated as sensitive data.
196
+ Allocated anchors live in a persistent per-file snapshot (`~/.config/pi-hashline-edit-pro/hash-store.sqlite`) keyed by content checksum, so resume-after-restart and cross-session edits reuse ownership instead of minting duplicates. Each session also appends an ownership log (`allocate`/`free`/`clear` events) to a sidecar file under `~/.config/pi-hashline-edit-pro/sessions/`; the fold of that log is the session's source of truth, and sidecars whose session file is gone are garbage-collected at startup.
217
197
 
218
- The store also keeps a per-file record of the hashes the model was last served (`read` rows, auto-read blocks, post-edit diff rows), pruned to the file's current hashes on every update so removed lines' hashes do not accumulate. `replace` verifies every line of the resolved range against that record before writing; a line whose hash is missing from the record means it either changed on disk after it was shown or was never shown, and the edit is refused with `[E_RANGE_STALE]`. A `write` clears the record, so edits after a write are verified against whatever the next `read` or auto-read block serves.
198
+ When a range is edited, the mapping between old and new content is computed per span: lines whose content is unchanged keep their allocated anchors, anchors of removed lines are freed, and every genuinely new line is minted a fresh anchor. Anchors are never assigned by matching content; only positional survival across an edit preserves one.
219
199
 
220
200
  Two guarantees make this safe even with duplicated content:
221
201
 
222
- - An edited range never borrows a hash from a line outside it. Lines outside the replaced range keep their hashes unconditionally, even when their content is byte-identical to lines inside the range.
223
- - Re-inserted identical text keeps its hash. If replacement content matches a line that was just removed, the removed line's hash is reused. "Replace X with X" doesn't rotate the anchor.
202
+ - An edited range never borrows an anchor from a line outside it. Lines outside the replaced range keep their anchors unconditionally, even when their content is byte-identical to lines inside the range.
203
+ - "Replace X with X" doesn't rotate the anchor: a line whose content is unchanged after an edit keeps its allocated anchor positionally. Every other line is minted fresh, so an anchor is never assigned by content matching.
224
204
 
225
205
  A no-op replace never changes the file, so anchors remain valid. On first run after upgrading from an older version, the previous `hash-store.json` is imported once and renamed to `hash-store.json.bak`.
226
206
 
207
+ On POSIX systems, the state directory is restricted to mode `0700` and the SQLite database plus its WAL/SHM sidecars to `0600`. The undo table contains the complete pre-edit and post-edit text for the latest edit to each file, so the store should still be treated as sensitive data.
208
+
227
209
  ## Error and warning codes
228
210
 
229
211
  Codes starting with `E_` are errors (the operation failed); codes starting with `W_` are warnings (the operation succeeded with a notice).
@@ -231,13 +213,12 @@ Codes starting with `E_` are errors (the operation failed); codes starting with
231
213
  | Code | Meaning |
232
214
  | --- | --- |
233
215
  | `[E_BAD_SHAPE]` | Request envelope or edit item has unknown, missing, or wrongly-typed fields (for example `replacement_lines` must be an array of strings, one element per line). |
234
- | `[W_BAD_SHAPE]` | Auto-corrected request slip reported as a warning (for example unwrapped JSON array syntax, embedded newlines split into lines, or a missing `path` resolved from anchors). |
216
+ | `[W_BAD_SHAPE]` | Auto-corrected request slip reported as a warning (for example unwrapped JSON array syntax or embedded newlines split into lines). |
235
217
  | `[E_BAD_REF]` | An anchor in `remove_from`/`remove_to` is not a bare 4-char anchor. |
236
218
  | `[W_BAD_REF]` | A pasted `anchor│` or diff-preview marker was stripped from an anchor field with a warning. |
237
- | `[E_STALE_ANCHOR]` | An anchor does not match any line in the current file; call `read` for fresh anchors. |
238
- | `[E_AMBIGUOUS_ANCHOR]` | An anchor matches multiple lines; call `read` for fresh anchors. |
219
+ | `[E_STALE_ANCHOR]` | An anchor is not owned in this session (it was never shown to you, or its line was edited or the file was rewritten); call `read` for fresh anchors. |
239
220
  | `[W_INVALID_PATCH]` | A `replacement_lines` element is a diff-preview row (`+anchor│`, `-anchor│`, `- │`). The marker is stripped automatically with a warning. |
240
- | `[W_BARE_HASH_PREFIX]` | A `replacement_lines` element starts with an `anchor│` prefix (the anchor plus the separator). The prefix is stripped automatically with a warning. |
221
+ | `[W_BARE_HASH_PREFIX]` | A `replacement_lines` element starts with an `anchor│` prefix. The prefix is stripped automatically with a warning. |
241
222
  | `[W_BAD_OP]` | Range start line is after range end line. The pair is swapped automatically with a warning. |
242
223
  | `[E_WOULD_EMPTY]` | An edit would empty a non-empty file; use `write` instead. |
243
224
  | `[E_NOT_FOUND]` | The path does not exist. |
@@ -247,22 +228,23 @@ Codes starting with `E_` are errors (the operation failed); codes starting with
247
228
  | `[E_UNDO_UNAVAILABLE]` | Undo history could not be persisted to the hash store; the edit was refused and the file was left unchanged. |
248
229
  | `[E_RANGE_STALE]` | A line in the replaced range no longer matches what was last shown (the file changed on disk, or the line was never shown). The edit was refused; the current range is returned with fresh anchors. |
249
230
  | `[W_BOUNDARY_BYPASS]` | The boundary anti-duplication was turned off for one replace call (an identical replacement had previously been cut to a noop); the duplicate lines were applied literally. The dedup is restored for the next call. |
250
- | `[E_FILE_TOO_LARGE]` | The file exceeds the 257,795-line hashline limit or the 100MB size limit. |
231
+ | `[E_FILE_TOO_LARGE]` | The file exceeds the 1,353,139-line hashline limit or the 100MB size limit. |
232
+ | `[E_REGISTRY]` | The anchor registry was not initialized; a serve or edit ran outside an initialized session. |
251
233
  | `[E_WRITE_HASH_ECHO]` | A `write` `content` line begins with the exact `anchor│` served for this file at the same line. The write is refused, file byte-identical; retry with bare content (remove the copied anchors). |
252
234
  | `[E_PATH_CHANGED]` | A write target changed identity after it was read; the write was refused to avoid following a swapped symlink or overwriting a replacement file. |
253
235
  | `[E_UNSAFE_REGEX]` | A grep regex can trigger excessive backtracking; simplify it or search with `literal: true`. |
254
236
 
255
237
  ## Troubleshooting
256
238
 
257
- - Stale anchors. `[E_STALE_ANCHOR]` or `[E_AMBIGUOUS_ANCHOR]` mean the file changed since the anchors were read. Call `read` for fresh anchors and retry.
239
+ - Stale anchors. `[E_STALE_ANCHOR]` means an anchor is not owned in this session: it was never shown to you, or its line was edited or the file was rewritten since. Call `read` for fresh anchors and retry.
258
240
  - Range changed on disk. `[E_RANGE_STALE]` means a line inside the replaced range changed after it was last shown to you (or was never shown). Nothing was modified; the error carries the current range with fresh anchors, so retry with those without a `read`.
259
- - Reset the hash store. Anchors live in `~/.config/pi-hashline-edit-pro/hash-store.sqlite` (with `-wal`/`-shm` sidecars). Quit pi, delete those three files, and the store is rebuilt on the next session. Anchor history is lost, but no project files are touched.
241
+ - Reset the anchor state. Anchors live in `~/.config/pi-hashline-edit-pro/hash-store.sqlite` (with `-wal`/`-shm` sidecars) and in per-session ownership logs under `~/.config/pi-hashline-edit-pro/sessions/`. Quit pi, delete those files, and everything is rebuilt on the next session. Anchor history is lost, but no project files are touched.
260
242
  - Corrupt store. If the store fails its health check it is renamed to `hash-store.sqlite.corrupt-<timestamp>` and rebuilt automatically.
261
- - Config directory moved. On non-Windows platforms, if `XDG_CONFIG_HOME` is set, the config directory (and the hash store inside it) lives at `$XDG_CONFIG_HOME/pi-hashline-edit-pro` instead of `~/.config/pi-hashline-edit-pro`. An existing store is not migrated automatically. To keep anchor and undo history, move the old `hash-store.sqlite` files (plus `-wal`/`-shm` sidecars) into the new directory before the first run.
243
+ - Config directory moved. If `XDG_CONFIG_HOME` is set on a non-Windows platform, the config directory (and the anchor state inside it) lives at `$XDG_CONFIG_HOME/pi-hashline-edit-pro` instead of `~/.config/pi-hashline-edit-pro`. An existing store is not migrated automatically. To keep anchor and undo history, move the old `hash-store.sqlite` files (plus `-wal`/`-shm` sidecars) into the new directory before the first run.
262
244
 
263
245
  ## Development
264
246
 
265
- Requires [Node.js](https://nodejs.org) 22.19 and npm.
247
+ Requires [Node.js](https://nodejs.org) 22.19 or newer and npm.
266
248
 
267
249
  ```bash
268
250
  npm install
package/index.ts CHANGED
@@ -14,8 +14,9 @@ import {
14
14
  toggleAutoRead,
15
15
  toggleAnchorGrep,
16
16
  } from "./src/config";
17
- import { loadHashStore, pruneMissing } from "./src/hash-store";
18
- import { recordServedSafe, clearServed, buildServedMap } from "./src/served";
17
+ import { loadHashStore, persistSnapshot, pruneMissing } from "./src/hash-store";
18
+ import { initRegistry, gcRegistrySidecars, clearRegistry, freeAnchors, markServed as markServedScoped } from "./src/anchor-registry";
19
+ import { buildServedMap } from "./src/served";
19
20
  import { clearBoundaryBypass } from "./src/boundary-bypass";
20
21
  import { registerWriteHook } from "./src/write-hook";
21
22
  import { readNormFile } from "./src/file-reader";
@@ -42,14 +43,17 @@ export default function (pi: ExtensionAPI): void {
42
43
  pi.setActiveTools(active.filter((t) => t !== "edit"));
43
44
  await initHasher();
44
45
  loadHashStore()
45
- .then(store =>
46
- pruneMissing(store).catch(err => {
47
- console.error("Failed to prune hash store:", err);
48
- }),
49
- )
46
+ .then(async store => {
47
+ const missing = await pruneMissing(store);
48
+ for (const path of missing) freeAnchors(path);
49
+ })
50
50
  .catch(err => {
51
51
  console.error("Failed to load hash store:", err);
52
52
  });
53
+ const sessionManager = (ctx as { sessionManager?: { getSessionFile?: () => string | undefined } }).sessionManager;
54
+ const sessionFile = sessionManager?.getSessionFile?.();
55
+ await initRegistry(sessionFile);
56
+ await gcRegistrySidecars();
53
57
  const config = await readConfig();
54
58
  autoRead = config.autoRead;
55
59
  pi.setActiveTools(
@@ -87,6 +91,14 @@ export default function (pi: ExtensionAPI): void {
87
91
  },
88
92
  });
89
93
 
94
+
95
+ pi.registerCommand("clear-anchors", {
96
+ description: "Clear the session's anchor claims (path-free resolution state); anchors are re-claimed on the next read",
97
+ handler: async (_args, ctx) => {
98
+ clearRegistry();
99
+ ctx.ui.notify(`Anchor claims cleared for this session`, "info");
100
+ },
101
+ });
90
102
  pi.on("tool_result", async (event, ctx) => {
91
103
  if (event.isError) return;
92
104
 
@@ -96,10 +108,9 @@ export default function (pi: ExtensionAPI): void {
96
108
  if (typeof writtenPath === "string") {
97
109
  try {
98
110
  resolvedPath = (await resolveInCwd(writtenPath, ctx.cwd)).resolved;
111
+ freeAnchors(resolvedPath);
99
112
  await clearUndo(resolvedPath);
100
113
  clearBoundaryBypass(resolvedPath);
101
- const store = await loadHashStore();
102
- clearServed(store, resolvedPath);
103
114
  } catch (error) {
104
115
  console.error("Failed to clear undo after write:", error);
105
116
  }
@@ -123,8 +134,8 @@ export default function (pi: ExtensionAPI): void {
123
134
  DEFAULT_MAX_LINES,
124
135
  );
125
136
  const fileLines = splitLines(normalized);
126
- const servedMap = buildServedMap(fileHashes, fileLines, preview.servedHashes);
127
- await recordServedSafe(absolutePath, servedMap, "auto-read", new Set(fileHashes));
137
+ persistSnapshot(await loadHashStore(), absolutePath, normalized, fileHashes);
138
+ markServedScoped(absolutePath, buildServedMap(fileHashes, fileLines, preview.servedHashes), new Set(fileHashes));
128
139
  return {
129
140
  content: [
130
141
  ...(event.content ?? []),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "3.0.4",
3
+ "version": "4.0.1",
4
4
  "type": "module",
5
5
  "description": "Hash-anchored read/replace/insert/grep tools for pi-coding-agent. Every line gets a unique 4-char tokenizer-friendly anchor that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
6
6
  "main": "index.ts",
package/prompts/grep.md CHANGED
@@ -1 +1 @@
1
- Search text files with ripgrep. Every hit and `context` line is returned as `lineNumber │ anchor│content` the `anchor│content` part works directly in `replace`/`insert` without a new `read`; `lineNumber` and the `=== path ===` header give file and line for navigation. Respects `.gitignore`; `.git` is always skipped, `node_modules`/`.tmp`/`coverage` are skipped only when a `.gitignore` lists them; binary/image files are skipped silently. Matches over 500 bytes are shown as a `...` fragment around the match, but the anchor still covers the whole line. If output says truncated, refine `pattern` or raise `limit` as hinted.
1
+ Search text files with ripgrep. Hits and context lines come back as `lineNumber │ anchor│content` rows, editable with replace or insert without a new read; the `=== path ===` header and line numbers locate the match. Searches respect `.gitignore`, always skip `.git`, and skip binary and image files silently. A match over 500 bytes is shown as a `...` fragment around the hit, but its anchor still covers the whole line. When the output says truncated, refine `pattern` or raise `limit`.
@@ -1,2 +1 @@
1
- - `insert`: the anchor must have been shown by `read`, a post-edit diff (`+anchor│`/` anchor│`), or any served `anchor│content` row. Empty file: `read` shows one `anchor│` row — insert `after` it.
2
- - `insert`: always include explicit `path`/`file_path`, even when the file can be inferred from anchors. Do not rely on anchor-based path inference.
1
+ - `insert`: the anchor must have been shown by `read`, a post-edit diff (`+anchor│`/` anchor│`), or any served `anchor│content` row. Empty file: `read` shows one `anchor│` row — insert `after` it.
@@ -1 +1 @@
1
- Insert `lines` after/before bare anchor `Hasu` from `Hasu│content`: anchor stays, lines are bare without `│`, one per element
1
+ Insert `lines` after or before an anchor line: the anchor line stays, `lines` are bare without `│`, one per element
package/prompts/insert.md CHANGED
@@ -1 +1 @@
1
- Insert lines after or before one existing line in a text file, by a bare anchor like `Hasu` from `Hasu│content`. The anchor line stays; `lines` are added after (`"after"`) or before (`"before"`) it. Lines are added literally, even if they duplicate neighbors.
1
+ Insert lines after or before one existing line in a text file, addressed by a bare anchor from read output or a diff row. The anchor line is preserved: `lines` go after it with `direction: "after"` or before it with `direction: "before"`, one string per line, no anchor prefixes, no embedded newlines. Lines are added literally, even when they duplicate neighbors.
package/prompts/read.md CHANGED
@@ -1 +1 @@
1
- Read a text file; each line returned as `anchor│content` a 4-character alphanumeric anchor plus the content (e.g. `Hasu│hello`: anchor `Hasu`). No line numbers: edit by anchor in replace/insert. Images visual attachments; Binary/directory rejected; UTF-16/UTF-32 (BOM) rejected; empty anchor│ (replace to insert); pageable with offset/limit; BOM stripped; non-UTF-8 shown as U+FFFD. If output says truncated, use `offset`/`limit` as hinted.
1
+ Read a text file and return it as `anchor│content` rows, one per line: a 4-character alphanumeric anchor, the `│` separator, then the line content. Target lines in replace and insert by anchor, never by content or line number. Page long files with `offset` and `limit`; when the output says truncated, continue with the hinted `offset/limit`. Images are returned as visual attachments. A binary file, a directory, or UTF-16/UTF-32 text is rejected; an empty file returns one empty row you can replace to seed content. BOMs are stripped for display and non-UTF-8 bytes are shown as U+FFFD.
@@ -1,5 +1,4 @@
1
1
  - `replace`: use the same anchor for `remove_from` and `remove_to` to change one line.
2
- - `replace`: always include explicit `path`/`file_path`, even when the file can be inferred from anchors. Do not rely on anchor-based path inference.
3
2
  - `replace`: `replacement_lines` takes bare lines without `│`; `[""]` is one blank line; pasted `anchor│` prefixes are stripped automatically.
4
3
  - `replace`: keep the range tight — only lines that actually change — and copy leading spaces exactly.
5
4
  - `replace`: post-edit diff `+anchor│`/` anchor│` rows are fresh anchors for the next edit — no new `read` needed. One edit per turn; check the diff before the next edit on that file.
@@ -1 +1 @@
1
- Replace lines via bare 4-char anchor `Hasu` from `Hasu│content`: `remove_from`/`remove_to` are `Hasu` only, `replacement_lines` is bare lines without `│`; one edit per call
1
+ Replace lines by anchor: bare anchors in `remove_from`/`remove_to`, bare lines in `replacement_lines` without `│`, one edit per call
@@ -1 +1 @@
1
- Replace a range of lines (or a single line) in a text file, targeted by 4-character anchors from read output. In `Hasu│content`, `Hasu` is the bare anchor before `│` and the full row is `Hasu│content`. `remove_from`/`remove_to` are bare anchors only (e.g. `"Hasu"`), never the row or file content. `replacement_lines` is bare content without `│`, one string per line `[]` deletes. Example: read showed `Hasu│old` and `arvm│old2`; to replace both use `{"remove_from":"Hasu","remove_to":"arvm","replacement_lines":["new line 1","new line 2"]}`. Single line same anchor for both.
1
+ Replace a range of lines (or a single line) in a text file, targeted by 4-character anchors from read output. Give `remove_from` and `remove_to` as bare anchors marking the first and last line to remove, and `replacement_lines` as one string per new line with no anchor prefixes and no embedded newlines; `[]` deletes the range. Every line in the range must match what was last shown; if the file drifted, the edit is refused and the current range is returned with fresh anchors, so retry without re-reading. Anchor follow-up edits on the `+anchor│` and ` anchor│` rows of the post-edit diff instead of re-reading.
@@ -1 +1 @@
1
- Undo the last `replace` or `insert` on a file, restoring previous content, BOM and line endings. Use after a bad edit removed wrong lines. Deleted file restored; changed elsewhere `[E_UNDO_STALE]`, nothing reverted, record kept do not modify the file to force an undo; call `read` to verify the current state, then stop. If output says truncated, use `read` to see full file.
1
+ Undo the last replace or insert on a file, restoring the previous content, BOM, and line endings. Use it when an edit removed or changed the wrong lines. If the file changed since that edit, the undo is refused with `[E_UNDO_STALE]` and nothing is modified: the record is kept, so verify the current state with read and stop instead of forcing the undo. A file deleted since the edit is restored. If the output says truncated, use read to see the full file.