pi-hashline-edit-pro 4.5.3 → 5.0.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 CHANGED
@@ -17,7 +17,7 @@ HDtm│}
17
17
 
18
18
  replace one line by its anchor:
19
19
 
20
- { "remove_from": "Emno", "remove_to": "Emno", "replacement_lines": [" console.log('hi');"] }
20
+ { "remove_from": "Emno", "remove_to": "Emno", "replacement_lines": " console.log('hi');" }
21
21
 
22
22
  the result is the post-edit diff with fresh anchors, so the next edit needs no re-read.
23
23
  ```
@@ -30,6 +30,7 @@ the result is the post-edit diff with fresh anchors, so the next edit needs no r
30
30
  - [Tools](#tools)
31
31
  - [read](#read)
32
32
  - [replace](#replace)
33
+ - [replace_within](#replace_within)
33
34
  - [insert](#insert)
34
35
  - [copy](#copy)
35
36
  - [move](#move)
@@ -89,6 +90,7 @@ pi install /path/to/pi-hashline-edit-pro
89
90
  | `edit` | disabled |
90
91
  | `grep` | disabled while `anchor_grep` is enabled |
91
92
  | `copy`, `move` | disabled while Copy/move is off |
93
+ | `replace_within` | disabled while Replace within is off |
92
94
  | `write` | kept; an auto-read block with fresh anchors is appended to its result |
93
95
  | `bash` | untouched |
94
96
 
@@ -128,7 +130,7 @@ pi uninstall npm:pi-hashline-edit-pro
128
130
  {
129
131
  "remove_from": "Emno",
130
132
  "remove_to": "Emno",
131
- "replacement_lines": [" console.log('hi');"]
133
+ "replacement_lines": " console.log('hi');"
132
134
  }
133
135
  ```
134
136
 
@@ -153,7 +155,7 @@ Nothing commits until an edit call returns: the extension validates the request
153
155
 
154
156
  ## Tools
155
157
 
156
- The extension registers seven tools: `read`, `replace`, `insert`, `copy`, `move`, `anchor_grep`, and `undo_last_change`. The built-in `edit` tool is disabled. `copy` and `move` are enabled by default; turn Copy/move off in `/hashline-config` to remove both. `replace`, `insert`, `copy`, and `move` take no `path` parameter by default: the file is resolved from the anchors' session ownership alone, so an edit can only land on the file the anchors were served for. Opt in with `/hashline-config` to require `path` in `replace`, `insert`, `copy`, and `move` for RPC visibility (for example pimacs.el); anchors still resolve the target and `path` must match.
158
+ The extension registers eight tools: `read`, `replace`, `replace_within`, `insert`, `copy`, `move`, `anchor_grep`, and `undo_last_change`. The built-in `edit` tool is disabled. `copy` and `move` are enabled by default; turn Copy/move off in `/hashline-config` to remove both. `replace_within` is enabled by default; turn Replace within off in `/hashline-config` to remove it. `replace`, `replace_within`, `insert`, `copy`, and `move` take no `path` parameter by default: the file is resolved from the anchors' session ownership alone, so an edit can only land on the file the anchors were served for. Opt in with `/hashline-config` to require `path` in `replace`, `replace_within`, `insert`, `copy`, and `move` for RPC visibility (for example pimacs.el); anchors still resolve the target and `path` must match.
157
159
 
158
160
  ### read
159
161
 
@@ -186,7 +188,7 @@ Edge cases:
186
188
  | --- | --- |
187
189
  | `remove_from` | 4-char anchor marking the FIRST line to remove (inclusive). |
188
190
  | `remove_to` | 4-char anchor marking the LAST line to remove (inclusive). |
189
- | `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. One element is one line: a real line-break character (`\n`, `\r\n`, or `\r`) splits it and sets the line's ending; escapes decode once — `\uXXXX` is the character, `\\uXXXX` the literal text. A trailing line-break sets the ending of the element's last line instead of adding a blank line, so `["b\n"]` sets that line's ending to LF, `["b\r\n"]` to CRLF, and on a file without a final newline it adds it. A lone string is accepted too: it is split on newlines, and stringified array text is unwrapped. |
191
+ | `replacement_lines` | The exact text to write in place of the removed range, as one string: `""` deletes the range, `"\n"` is one blank line, and a trailing line break sets the last line's ending instead of adding a blank line. Embedded `\r\n`/`\r`/`\n` are preserved; escapes decode once — `\uXXXX` is the character, `\\uXXXX` the literal text. Legacy arrays are converted to text (elements joined with LF); prefer the string form. |
190
192
 
191
193
  Example: read showed `Hasu│old` and `arvm│old2`; to replace both:
192
194
 
@@ -194,7 +196,7 @@ Example: read showed `Hasu│old` and `arvm│old2`; to replace both:
194
196
  {
195
197
  "remove_from": "Hasu",
196
198
  "remove_to": "arvm",
197
- "replacement_lines": ["new line 1", "new line 2"]
199
+ "replacement_lines": "new line 1\nnew line 2"
198
200
  }
199
201
  ```
200
202
 
@@ -202,7 +204,7 @@ Single line: use the same anchor for `remove_from` and `remove_to`. `replace_fro
202
204
 
203
205
  The extension checks the request before any file I/O, so a bad request never touches the file.
204
206
 
205
- Auto-fixable slips fall into two groups. Fixed silently: a reversed range, stringified array text (even with a trailing JS method call, for example `[…].map(s => s)`), and embedded newlines. Fixed with a warning: a leftover `anchor│` prefix in `replacement_lines` or the anchor fields (a prefix of 4 to 5 letters before `│`, for example `abde│`), and diff-preview rows pasted into the replacement.
207
+ Auto-fixable slips fall into two groups. Fixed silently: a reversed range, embedded newlines, and a legacy array payload (a single-element array that holds stringified array text, even with a trailing JS method call, for example `[…].map(s => s)`, is unwrapped). Fixed with a warning: a leftover `anchor│` prefix in `replacement_lines` or the anchor fields (a prefix of 4 to 5 letters before `│`, for example `abde│`), and diff-preview rows pasted into the replacement.
206
208
 
207
209
  Content containing a NUL byte (`U+0000`) is rejected with `[E_BAD_SHAPE]` before any file I/O: writing it would make the file binary, so use an empty replacement to delete. This applies to `replace`'s `replacement_lines` and `insert`'s `lines`.
208
210
 
@@ -212,6 +214,14 @@ An edit that changes neither content nor line endings reports `No changes made`
212
214
 
213
215
  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`.
214
216
 
217
+ ### replace_within
218
+
219
+ `replace_within` changes part of a line (or a range of lines) without retyping the rest. `replace_from` and `replace_to` are bare anchors marking the first and last line of the range; use the same anchor for a single line. `replace_old` is the exact text to find inside that range, and `replace_new` replaces just that match; every other character stays untouched. That makes it the tool for a change the request quotes as a substring: a whole-line `replace` has to reproduce the rest of the line, so a slipped character becomes a wrong byte, while `replace_within` leaves everything the request did not name untouched. It is enabled by default; turn Replace within off in `/hashline-config` to remove the tool.
220
+
221
+ `replace_old` is matched against the range's text (LF line breaks, no final terminator) and must occur exactly once. A missing match is refused with `[E_SUBSTRING_NOT_FOUND]` and the current `anchor│content` rows; a repeated match is refused with `[E_SUBSTRING_AMBIGUOUS]` and the matching line numbers. Both refusals carry enough to retry without a `read`.
222
+
223
+ A `replace_within` call is never grouped into a batch; it commits on its own like `copy` and `move`. The post-edit diff carries fresh anchors, and the edit is undoable with `undo_last_change`.
224
+
215
225
  ### insert
216
226
 
217
227
  `insert` adds lines after or before an existing line without removing anything. Like `replace`, there is no `path` parameter.
@@ -220,14 +230,14 @@ After a successful edit, the diff is capped at 50KB. A row over 50KB is shown as
220
230
  | --- | --- |
221
231
  | `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. |
222
232
  | `direction` | `"after"` inserts below the anchor line, `"before"` above it. |
223
- | `lines` | Lines to insert, one element per line. `[""]` is a blank line. Never include the anchor line. One element is one line: a real line-break character (`\n`, `\r\n`, or `\r`) splits it and sets the line's ending; escapes decode once — `\uXXXX` is the character, `\\uXXXX` the literal text. A trailing line-break sets the ending of the element's last line instead of adding a blank line, so `["b\n"]` inserts `b` with an LF ending, and on a file without a final newline it adds it. A lone string is split on newlines, and stringified array text is unwrapped. |
233
+ | `lines` | The exact text to insert, as one string: `""` inserts nothing, `"\n"` is one blank line, and a trailing line break sets the last line's ending instead of adding a blank line. Never include the anchor line. Embedded `\r\n`/`\r`/`\n` are preserved; escapes decode once — `\uXXXX` is the character, `\\uXXXX` the literal text. Legacy arrays are converted to text (elements joined with LF); prefer the string form. |
224
234
 
225
- Nothing is removed and the inserted lines are written exactly as given; the anchor line and every other line stay in place. Inserting nothing (`lines: []`) reports a noop. To seed an empty file, read it and insert after the `anchor│` empty-line row.
235
+ Nothing is removed and the inserted lines are written exactly as given; the anchor line and every other line stay in place. Inserting nothing (`lines: ""`) reports a noop. To seed an empty file, read it and insert after the `anchor│` empty-line row.
226
236
 
227
237
  Example: add a line after `Emno│`:
228
238
 
229
239
  ```json
230
- { "anchor": "Emno", "direction": "after", "lines": [" // log the greeting"] }
240
+ { "anchor": "Emno", "direction": "after", "lines": " // log the greeting" }
231
241
  ```
232
242
 
233
243
  The same safety machinery as `replace` applies: undo is saved before the write (a failed write restores the previous undo record), and line endings and BOMs survive.
@@ -280,11 +290,11 @@ Output is capped at `limit` matched lines, 2000 rows, and 50KB of text, whicheve
280
290
 
281
291
  ### undo_last_change
282
292
 
283
- `undo_last_change` reverts the most recent successful `replace`, `insert`, `copy`, or `move` on a file, restoring the exact previous content, BOM and line endings included, plus the previous anchors.
293
+ `undo_last_change` reverts the most recent successful `replace`, `replace_within`, `insert`, `copy`, or `move` on a file, restoring the exact previous content, BOM and line endings included, plus the previous anchors.
284
294
 
285
- - History is per-file and single-level: only the most recent `replace`, `insert`, `copy`, or `move` can be reverted. A same-message batch of `replace`/`insert` calls on one file counts as one entry: one undo reverts the whole batch.
295
+ - History is per-file and single-level: only the most recent `replace`, `replace_within`, `insert`, `copy`, or `move` can be reverted. A same-message batch of `replace`/`insert` calls on one file counts as one entry: one undo reverts the whole batch.
286
296
  - History is persisted and survives session restarts. A failed `write` does not clear it.
287
- - Every applied `replace`, `insert`, `copy`, or `move` is undoable; the undo record is saved before the edit is written.
297
+ - Every applied `replace`, `replace_within`, `insert`, `copy`, or `move` is undoable; the undo record is saved before the edit is written.
288
298
  - A cross-file `move` stores one undo entry per file; `undo_last_change` reverts the file you name, so revert both sides to undo the whole move.
289
299
  - A successful `write` clears the history for that file.
290
300
  - 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.
@@ -297,7 +307,7 @@ Output is capped at `limit` matched lines, 2000 rows, and 50KB of text, whicheve
297
307
  Multiple `replace` and `insert` calls on the same file in one assistant message are grouped per file into one batch. The batch unit is the message, not the turn: calls from separate messages in the same turn run on their own, one after another.
298
308
 
299
309
  - A call outside a batch commits before its result returns.
300
- - A `copy` or `move` call is never grouped into a batch: it commits on its own, and a pending same-file batch aborts safely with `[E_OP_ABORTED]` if the file changed under it.
310
+ - A `copy`, `move`, or `replace_within` call is never grouped into a batch: it commits on its own, and a pending same-file batch aborts safely with `[E_OP_ABORTED]` if the file changed under it.
301
311
  - A batch validates every call against the pre-batch state and commits once, during the batch's last call: earlier calls reply `In batch N`, and the batch's last call shows the combined diff, with one undo reverting the whole batch.
302
312
  - If a batch aborts, an earlier member's row renders the abort message instead of the placeholder. Nothing commits until the last call succeeds.
303
313
  - A batch member accepts the same request shapes and auto-fixes as a standalone call.
@@ -312,7 +322,7 @@ The hashline tools are sequential in pi, so a message that contains one runs all
312
322
 
313
323
  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.
314
324
 
315
- After `replace`, `insert`, `copy`, `move`, and `undo_last_change`, the result shows the post-edit diff. Inside a same-message batch, only the batch's last call shows the combined diff, headed by a `batch N:` line; earlier calls reply `In batch N`. 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.
325
+ After `replace`, `replace_within`, `insert`, `copy`, `move`, and `undo_last_change`, the result shows the post-edit diff. Inside a same-message batch, only the batch's last call shows the combined diff, headed by a `batch N:` line; earlier calls reply `In batch N`. 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.
316
326
 
317
327
  An edit that changes only line endings has no content diff; the result still reports `applied`, and one `undo_last_change` reverts it.
318
328
 
@@ -334,7 +344,7 @@ The setting lives in `/hashline-config` as Auto-read all and in `config.json` as
334
344
 
335
345
  | Command | Description |
336
346
  | --- | --- |
337
- | `/hashline-config` | Open the settings window: auto-read anchors, auto-read all mode, ignore folders/files, diff context lines, `anchor_grep` tool, copy/move tools, required `path`, and strict input. Persists across sessions. |
347
+ | `/hashline-config` | Open the settings window: auto-read anchors, auto-read all mode, ignore folders/files, diff context lines, `anchor_grep` tool, copy/move tools, replace_within tool, required `path`, and strict input. Persists across sessions. |
338
348
  | `/clear-anchors` | Clear the session's anchor claims. Anchors are re-claimed on the next `read`. |
339
349
 
340
350
  Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a setting is first changed in `/hashline-config`:
@@ -346,6 +356,7 @@ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a se
346
356
  "autoReadAllIgnore": [],
347
357
  "anchorGrepEnabled": true,
348
358
  "copyMoveEnabled": true,
359
+ "replaceWithinEnabled": true,
349
360
  "requirePath": false,
350
361
  "strictInput": false,
351
362
  "diffContextLines": 1
@@ -359,7 +370,8 @@ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a se
359
370
  | `autoReadAllIgnore` | Ignore folders/files | `[]` | Extra folder names, file names, or globs skipped by auto-read all. |
360
371
  | `anchorGrepEnabled` | Anchor grep | `true` | Register `anchor_grep` and disable the built-in grep while it is on. |
361
372
  | `copyMoveEnabled` | Copy/move | `true` | Offer the `copy` and `move` tools; when off, both are removed from the active tools. |
362
- | `requirePath` | Require path | `false` | `replace`, `insert`, `copy`, and `move` require a `path` argument that must match anchor ownership. |
373
+ | `replaceWithinEnabled` | Replace within | `true` | Offer the `replace_within` tool; when off, it is removed from the active tools. |
374
+ | `requirePath` | Require path | `false` | `replace`, `replace_within`, `insert`, `copy`, and `move` require a `path` argument that must match anchor ownership. |
363
375
  | `strictInput` | Strict input | `false` | Reject auto-fixable slips (`[W_BAD_SHAPE]`, `[W_BAD_REF]`, `[W_INVALID_PATCH]`, `[W_BARE_HASH_PREFIX]`) with `[E_BAD_SHAPE]` instead of applying them with a warning. |
364
376
  | `diffContextLines` | Diff context | `1` | Surrounding lines in post-edit diffs, 0-10 (needs Auto-read). |
365
377
 
@@ -371,7 +383,7 @@ When `PI_HASHLINE_DIR` is unset or empty, non-Windows platforms honor `XDG_CONFI
371
383
  | --- | --- | --- |
372
384
  | Output cap | 2000 lines and 50KB | `read`, auto-read after `write`, post-edit diffs, patches, previews, `details.patch` |
373
385
  | Oversized row | 50KB per `anchor│content` row | replaced by an anchor-keeping marker you can still edit through |
374
- | Line cap | 1,353,139 lines per file | `read`, `replace`, `insert`, `copy`, `move` (`[E_FILE_TOO_LARGE]`) |
386
+ | Line cap | 1,353,139 lines per file | `read`, `replace`, `replace_within`, `insert`, `copy`, `move` (`[E_FILE_TOO_LARGE]`) |
375
387
  | File size | 100MB | all tools (`[E_FILE_TOO_LARGE]`) |
376
388
  | Hash window | first 500 bytes of a line | anchor identity for long lines |
377
389
  | Patch guard | 1MB of pre-edit + post-edit text | patch generation is skipped and `patchTruncated` is set |
@@ -387,12 +399,13 @@ When `PI_HASHLINE_DIR` is unset or empty, non-Windows platforms honor `XDG_CONFI
387
399
 
388
400
  ## Tool result details
389
401
 
390
- All seven tools return machine-readable metadata in `details` alongside the model-visible text.
402
+ All eight tools return machine-readable metadata in `details` alongside the model-visible text.
391
403
 
392
404
  | Tool | `details` |
393
405
  | --- | --- |
394
406
  | `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`. |
395
407
  | `replace`, `insert` | `diff` (post-edit diff, capped, with current anchors on `+anchor│` and ` anchor│` rows; a same-message batch reports the combined diff on its last call and an empty diff on earlier calls), `patch` (a standard unified patch for external tools, capped like the diff), `patchTruncated` (true when the patch was cut or skipped for a pair over 1MB and can no longer be applied as-is), `firstChangedLine`, `snapshotId`, `classification` (`"noop"` when nothing changed), `batch` (`{ id, size, last, total }` marking same-message batch membership; earlier members also carry `aborted: true` and `abortMessage` after a batch abort), `hints` (informative `[H_*]` notices, for example literal escaped text written as sent), and `metrics`: `edits_attempted`, `edits_noop`, `warnings`, `classification` (`"applied"` or `"noop"`), `changed_lines` (`{ first, last }`), `added_lines`, `removed_lines`. |
408
+ | `replace_within` | Same shape as `replace`: `diff` (post-edit diff with current anchors), `patch`, `patchTruncated`, `firstChangedLine`, `snapshotId`, `classification` (`"noop"` when nothing changed), and `metrics` with the same counters. |
396
409
  | `copy`, `move` | Same shape as `replace`: `diff` (post-edit diff with current anchors), `patch`, `patchTruncated`, `firstChangedLine`, `snapshotId`, `classification` (`"noop"` when a move changes nothing), and `metrics` with the same counters. |
397
410
  | `undo_last_change` | `diff` (the undo diff with restored anchors), `patch`, `patchTruncated`, and `metrics` in the same shape as `replace`. |
398
411
  | `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). |
@@ -417,15 +430,18 @@ Full reference:
417
430
  | Code | Meaning |
418
431
  | --- | --- |
419
432
  | `[E_CONFIG]` | `PI_HASHLINE_DIR` is nonempty but not an absolute path. |
420
- | `[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), content contains a NUL byte (`U+0000`), which would make the file binary, or a grep `glob` has invalid bracket or brace syntax. |
421
- | `[W_BAD_SHAPE]` | Auto-corrected request slip reported as a warning (for example stringified array text that could not be parsed and was kept as one literal line). |
433
+ | `[E_BAD_SHAPE]` | Request envelope or edit item has unknown, missing, or wrongly-typed fields (for example `replacement_lines` must be a string holding the exact text), content contains a NUL byte (`U+0000`), which would make the file binary, or a grep `glob` has invalid bracket or brace syntax. |
434
+ | `[W_BAD_SHAPE]` | Auto-corrected request slip reported as a warning (for example legacy array text that could not be parsed and was kept as one literal line). |
422
435
  | `[E_BAD_REF]` | An anchor in `remove_from`/`remove_to` is not a bare 4-character anchor (the anchor table is letters only). |
436
+ | `[E_SUBSTRING_NOT_FOUND]` | `replace_within` did not find `replace_old` in the selected range. The current `anchor│content` rows are returned; copy `replace_old` exactly from the served row and retry. |
437
+ | `[E_SUBSTRING_AMBIGUOUS]` | `replace_within` found `replace_old` more than once in the selected range. Narrow `replace_from`/`replace_to` or extend `replace_old` so it matches exactly once. |
423
438
  | `[W_BAD_REF]` | A pasted `anchor│` or diff-preview marker was stripped from an anchor field with a warning. |
424
439
  | `[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. |
425
- | `[W_INVALID_PATCH]` | A `replacement_lines` element is a diff-preview row (`+anchor│`, `-anchor│`, `- │`). The marker is stripped automatically with a warning. |
426
- | `[W_BARE_HASH_PREFIX]` | A `replacement_lines` element starts with an `anchor│` prefix. The prefix is stripped automatically with a warning. |
440
+ | `[W_INVALID_PATCH]` | A `replacement_lines` line is a diff-preview row (`+anchor│`, `-anchor│`, `- │`). The marker is stripped automatically with a warning. |
441
+ | `[W_BARE_HASH_PREFIX]` | A `replacement_lines` line starts with an `anchor│` prefix. The prefix is stripped automatically with a warning. |
427
442
  | `[W_ANCHOR_RECLAIMED]` | The session's anchor quota was exhausted, so all anchors of the listed files (the least recently read or edited) were freed to make room. Read those files again before editing them. |
428
443
  | `[H_LITERAL_ESCAPE]` | `lines` or `replacement_lines` contains literal escaped text such as `\uXXXX` or `\n`; the file receives those backslash characters as written. Escapes decode once in the tool call (`\uXXXX` → the character), so a doubled escape (`\\uXXXX`) lands literally — resend with the real character if that was not intended. |
444
+ | `[H_UNICODE_LOST]` | The removed line contained an invisible or look-alike character (for example `U+200B` zero-width space, `U+00A0` no-break space, or a smart quote) that the replacement does not. The edit applied as sent; copy the character from the served row if the request did not ask to remove it. |
429
445
  | `[E_WOULD_EMPTY]` | An edit would empty a non-empty file; use `write` instead. A cross-file `move` may empty its source file. |
430
446
  | `[E_NOT_FOUND]` | The path does not exist. |
431
447
  | `[E_ACCESS]` | The file is not readable or writable. |
@@ -454,7 +470,7 @@ Full reference:
454
470
  - 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.
455
471
  - Corrupt store. If the store fails its health check it is renamed to `hash-store.sqlite.corrupt-<timestamp>` and rebuilt automatically.
456
472
  - 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.
457
- - Windows drives in WSL. Editing a file under a Windows mount (`/mnt/c`, drvfs/9p) can fail with `EPERM` from `fchmod` because those filesystems do not store POSIX modes. Mode preservation is best-effort there, so `replace`, `insert`, and `undo_last_change` still write the edit.
473
+ - Windows drives in WSL. Editing a file under a Windows mount (`/mnt/c`, drvfs/9p) can fail with `EPERM` from `fchmod` because those filesystems do not store POSIX modes. Mode preservation is best-effort there, so `replace`, `replace_within`, `insert`, and `undo_last_change` still write the edit.
458
474
  - Not sure what the extension changed. `read` returns anchored rows and the built-in `edit` is gone; that is expected. See [What changes in your session](#what-changes-in-your-session).
459
475
 
460
476
  ## Privacy and on-disk state
@@ -481,7 +497,7 @@ Background snapshot pruning and registry sidecar GC skip `EPERM`/`EACCES` withou
481
497
 
482
498
  ### Allocation
483
499
 
484
- 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 stride of 836,286 entries (coprime to the 1,353,139-entry table), so consecutively minted anchors land in unrelated regions of the table instead of sharing leading characters. Each session seeds its walk from its own offset (derived from the session key and the process id), so concurrent sessions mint different sequences instead of identical ones: an anchor minted in one session is unknown in another and is rejected with `[E_STALE_ANCHOR]` rather than resolving to a different file. 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, you ran `/clear-anchors`, or the session's quota ran out and the file was the least recently read or edited, which frees all of its anchors and reports it in `[W_ANCHOR_RECLAIMED]`). Minting prefers anchors the session has never used; when a bounded fresh-anchor probe finds nothing, 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: the pool is the shipped table's 1,353,139 entries (not all 52⁴ letter combinations), so a file can hold at most 1,353,139 lines, beyond which `read`, `replace`, `insert`, `copy`, and `move` reject with `[E_FILE_TOO_LARGE]` (use `write` for very large files).
500
+ 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 stride of 836,286 entries (coprime to the 1,353,139-entry table), so consecutively minted anchors land in unrelated regions of the table instead of sharing leading characters. Each session seeds its walk from its own offset (derived from the session key and the process id), so concurrent sessions mint different sequences instead of identical ones: an anchor minted in one session is unknown in another and is rejected with `[E_STALE_ANCHOR]` rather than resolving to a different file. 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, you ran `/clear-anchors`, or the session's quota ran out and the file was the least recently read or edited, which frees all of its anchors and reports it in `[W_ANCHOR_RECLAIMED]`). Minting prefers anchors the session has never used; when a bounded fresh-anchor probe finds nothing, 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: the pool is the shipped table's 1,353,139 entries (not all 52⁴ letter combinations), so a file can hold at most 1,353,139 lines, beyond which `read`, `replace`, `replace_within`, `insert`, `copy`, and `move` reject with `[E_FILE_TOO_LARGE]` (use `write` for very large files).
485
501
 
486
502
  ### Ownership and mapping across edits
487
503
 
package/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
3
3
  import { initHasher } from "./src/hashline";
4
+ import { regReplaceWithin } from "./src/replace-within";
4
5
  import { regReplace } from "./src/replace";
5
6
  import { regInsert } from "./src/insert";
6
7
  import { regCopy, regMove } from "./src/copy-move";
@@ -20,6 +21,7 @@ import {
20
21
  cycleAutoReadAllMode,
21
22
  toggleAnchorGrep,
22
23
  toggleCopyMove,
24
+ toggleReplaceWithin,
23
25
  toggleRequirePath,
24
26
  toggleStrictInput,
25
27
  adjustDiffContextLines,
@@ -43,6 +45,7 @@ export default function (pi: ExtensionAPI): void {
43
45
  regRead(pi);
44
46
 
45
47
  regReplace(pi);
48
+ regReplaceWithin(pi);
46
49
  regInsert(pi);
47
50
  regCopy(pi);
48
51
  regMove(pi);
@@ -61,9 +64,11 @@ export default function (pi: ExtensionAPI): void {
61
64
  const flags = await currentEditFlags();
62
65
  regRead(pi, flags);
63
66
  regReplace(pi, flags);
67
+ regReplaceWithin(pi, flags);
64
68
  regInsert(pi, flags);
65
69
  regCopy(pi, flags);
66
70
  regMove(pi, flags);
71
+ regGrep(pi, flags);
67
72
  regUndo(pi, flags);
68
73
  } catch (error) {
69
74
  console.error("Failed to refresh edit tools:", error);
@@ -99,6 +104,7 @@ export default function (pi: ExtensionAPI): void {
99
104
  pi.getActiveTools().filter((t) => {
100
105
  if (config.anchorGrepEnabled ? t === "grep" : t === "anchor_grep") return false;
101
106
  if (config.copyMoveEnabled === false && (t === "copy" || t === "move")) return false;
107
+ if (config.replaceWithinEnabled === false && t === "replace_within") return false;
102
108
  return true;
103
109
  }),
104
110
  );
@@ -133,7 +139,7 @@ export default function (pi: ExtensionAPI): void {
133
139
  }));
134
140
 
135
141
  pi.registerCommand("hashline-config", {
136
- description: "Open the hashline settings window (auto-read, auto-read all, ignore folders/files, diff context, grep, copy/move, path, strict input)",
142
+ description: "Open the hashline settings window (auto-read, auto-read all, ignore folders/files, diff context, grep, copy/move, replace_within, path, strict input)",
137
143
  handler: async (_args, ctx) => {
138
144
  if (!ctx.hasUI) {
139
145
  ctx.ui.notify("/hashline-config requires interactive mode", "error");
@@ -159,6 +165,11 @@ export default function (pi: ExtensionAPI): void {
159
165
  const active = pi.getActiveTools();
160
166
  pi.setActiveTools(enabled ? [...new Set([...active, "copy", "move"])] : active.filter((t) => t !== "copy" && t !== "move"));
161
167
  }
168
+ else if (key === "replaceWithinEnabled") {
169
+ const enabled = await toggleReplaceWithin();
170
+ const active = pi.getActiveTools();
171
+ pi.setActiveTools(enabled ? [...new Set([...active, "replace_within"])] : active.filter((t) => t !== "replace_within"));
172
+ }
162
173
  else if (key === "requirePath") await toggleRequirePath();
163
174
  else if (key === "strictInput") await toggleStrictInput();
164
175
  await refreshEditTools();
@@ -251,6 +262,7 @@ export default function (pi: ExtensionAPI): void {
251
262
 
252
263
  if (
253
264
  event.toolName !== "replace" &&
265
+ event.toolName !== "replace_within" &&
254
266
  event.toolName !== "insert" &&
255
267
  event.toolName !== "copy" &&
256
268
  event.toolName !== "move" &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "4.5.3",
3
+ "version": "5.0.0",
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",
@@ -1 +1 @@
1
-
1
+ - `insert`: after inserting a quoted payload, check the post-edit diff for an extra `+anchor│` blank row before the next line.
@@ -1 +1 @@
1
- Insert `lines` after or before an anchor line: the anchor line stays, `lines` are bare without `│`, one per element
1
+ Insert a text block after or before an anchor line: the anchor line stays; `lines` is the exact text without anchor prefixes
package/prompts/insert.md CHANGED
@@ -1,3 +1,3 @@
1
- Insert lines after or before one existing line in a text file, addressed by a bare anchor from any served anchor│content 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. An element is one line: a real line-break character (`\n`, `\r\n`, or `\r`) splits it and sets that line's ending; escapes decode once — `\uXXXX` is the character, `\\uXXXX` the literal text. Inserted lines are written exactly as given; nothing else in the file changes.
1
+ Insert text after or before one existing line in a text file, addressed by a bare anchor from any served anchor│content row. The anchor line is preserved: `lines` go after it with `direction: "after"` or before it with `direction: "before"`. `lines` is one string holding the exact text to insert; escapes decode once — `\uXXXX` is the character, `\\uXXXX` the literal text.
2
2
 
3
3
  Same-file calls in one message batch: earlier calls reply `In batch N` and the last call shows the combined diff, with one undo for the whole batch.
@@ -1 +1 @@
1
- - `move`: a cross-file move records one undo entry per file — undo both sides or the moved lines stay duplicated or missing. Lines between source and target may be re-anchored.
1
+ - `move`: a cross-file move records one undo entry per file — undo both sides. Lines between source and target may be re-anchored.
@@ -1,4 +1,2 @@
1
- - `replace`: `-anchor│` rows in a post-edit diff are dead anchors; only `+anchor│` and ` anchor│` rows are live. Check the post-edit diff before the next turn's edits on that file.
2
- - `replace`: same-file same-message calls batch: disjoint ranges, one undo. A call whose anchors resolve nowhere fails alone; its file's batch still commits.
3
- - `replace`: use `replace`/`insert` for structural or multi-line edits — anchored, verified, undoable.
4
- - `replace`: `replacement_lines`: one string per line, `[""]` is one blank line, `[]` deletes. Pasted `anchor│` prefixes are stripped. Single line: same anchor for `remove_from` and `remove_to`.
1
+ - `replace`: same-file same-message calls batch: disjoint ranges, one undo. A call whose anchors resolve nowhere fails alone.
2
+ - `replace`: `replacement_lines` is one string; a pasted `anchor│` prefix is stripped; single line: same anchor for `remove_from` and `remove_to`.
@@ -1 +1 @@
1
- Replace lines by anchor: bare anchors in `remove_from`/`remove_to`, bare lines in `replacement_lines` without `│`, one edit per call
1
+ Replace lines by anchor: bare anchors in `remove_from`/`remove_to`, the exact replacement text in `replacement_lines` (one edit per call)
@@ -0,0 +1,3 @@
1
+ - `replace_within`: use it instead of `replace` to change part of a line, so every character the request does not name is preserved as served.
2
+ - `replace_within`: `replace_old` must be copied exactly from the served row and occur once in the range; a missing or repeated match is refused and returns the current rows.
3
+ - `replace_within`: a batch never groups it; it commits on its own, like `copy` and `move`.
@@ -0,0 +1 @@
1
+ Replace part of a line or range by anchor: `replace_old` is matched exactly once inside `replace_from`/`replace_to` and replaced by `replace_new`, leaving the rest of the text untouched
@@ -0,0 +1,5 @@
1
+ Replace part of a line (or a range of lines) without retyping the rest. `replace_from` and `replace_to` are bare anchors from served `anchor│content` rows marking the first and last line of the range; use the same anchor for one line. `replace_old` is the exact text to find inside that range, copied from the served row; it must occur exactly once. `replace_new` replaces just that match, and every other character stays untouched.
2
+
3
+ Example: read served `Hasu│ {"id": "checkout-5", "feature": "legacyCheckout", "retries": 3},`. Call { "replace_from": "Hasu", "replace_to": "Hasu", "replace_old": "legacyCheckout", "replace_new": "stableCheckout" }. The line becomes ` {"id": "checkout-5", "feature": "stableCheckout", "retries": 3},` and the post-edit diff carries fresh anchors.
4
+
5
+ Both strings are exact text; escapes decode once — `\uXXXX` is the character, `\\uXXXX` the literal text. Matching uses LF line breaks and excludes the last line's terminator. A missing match is refused with the current rows, a repeated match with the matching line numbers, so the retry needs no read. Nothing but the matched text changes.
@@ -1,5 +1,6 @@
1
- Replace a range of lines (or a single line) in a text file, targeted by 4-character anchors from any served anchor│content row. 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. An element is one line: a real line-break character (`\n`, `\r\n`, or `\r`) splits it and sets that line's ending; escapes decode once — `\uXXXX` is the character, `\\uXXXX` the literal text. `[]` deletes the range.
1
+ Replace a range of lines (or a single line) in a text file by anchor. `remove_from` and `remove_to` are the 4-character anchors of the first and last line to remove, and `replacement_lines` is one string with the exact replacement text: `""` deletes the range, and escapes decode once — `\uXXXX` is the character, `\\uXXXX` the literal text. The text is written exactly as given, and nothing else in the file changes.
2
+ To change only part of a line without retyping the rest, use `replace_within` instead; it preserves every character the request does not name.
2
3
 
3
4
  Same-file calls in one message batch: earlier calls reply `In batch N` and the last call shows the combined diff, with one undo for the whole batch.
4
5
 
5
- Example: read served `Hasu│old` and `arvm│old2`. Call { "remove_from": "Hasu", "remove_to": "arvm", "replacement_lines": ["new"] }. The post-edit diff shows `-Hasu│old`, `-arvm│old2`, `+Qwer│new`: the `-` rows are dead anchors now; the `+` and ` ` rows are live anchors for the next edit.
6
+ Example: read served `Hasu│old` and `arvm│old2`. Call { "remove_from": "Hasu", "remove_to": "arvm", "replacement_lines": "new line 1\nnew line 2" }. The post-edit diff shows `-Hasu│old`, `-arvm│old2`, `+Qwer│new line 1`: the `-` rows are dead anchors now; the `+` and ` ` rows are live anchors for the next edit.
@@ -1,2 +1,2 @@
1
- - `undo_last_change`: only the last `replace`/`insert`/`copy`/`move` per file is undoable; a `write` clears it, so undo right after a bad diff — review the diff's `-anchor│` rows first to confirm what you're restoring.
1
+ - `undo_last_change`: only the last `replace`/`replace_within`/`insert`/`copy`/`move` per file is undoable; a `write` clears it, so undo right after a bad diff — review the diff's `-anchor│` rows first to confirm what you're restoring.
2
2
  - `undo_last_change`: a cross-file `move` records one undo entry per file; undo both the source and the destination to revert the whole move, because undoing one side alone leaves the moved lines duplicated or missing.
@@ -1 +1 @@
1
- Single-level undo: reverts a file's last `replace`, `insert`, `copy`, or `move`
1
+ Single-level undo: reverts a file's last `replace`, `replace_within`, `insert`, `copy`, or `move`
@@ -1 +1 @@
1
- Undo the last replace, insert, copy, or move 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.
1
+ Undo the last replace, replace_within, insert, copy, or move 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.
package/src/config-ui.ts CHANGED
@@ -2,7 +2,7 @@ import { Key, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
2
2
  import type { Theme } from "@earendil-works/pi-coding-agent";
3
3
  import { readConfig, type Config } from "./config";
4
4
 
5
- export type ConfigToggleKey = "autoRead" | "autoReadAll" | "autoReadAllIgnore" | "anchorGrepEnabled" | "copyMoveEnabled" | "requirePath" | "strictInput" | "diffContextLines";
5
+ export type ConfigToggleKey = "autoRead" | "autoReadAll" | "autoReadAllIgnore" | "anchorGrepEnabled" | "copyMoveEnabled" | "replaceWithinEnabled" | "requirePath" | "strictInput" | "diffContextLines";
6
6
 
7
7
  export interface ConfigRow {
8
8
  key: ConfigToggleKey;
@@ -26,6 +26,7 @@ export function configRows(config: Config): ConfigRow[] {
26
26
  { key: "copyMoveEnabled", label: "Copy/move", hint: "copy and move tools (both off while disabled)", enabled: config.copyMoveEnabled !== false },
27
27
  { key: "requirePath", label: "Require path", hint: "replace, insert, copy, move need path (RPC visibility)", enabled: config.requirePath === true },
28
28
  { key: "strictInput", label: "Strict input", hint: "Reject auto-fixable slips instead of warnings", enabled: config.strictInput === true },
29
+ { key: "replaceWithinEnabled", label: "Replace within", hint: "replace_within tool (off while disabled)", enabled: config.replaceWithinEnabled !== false },
29
30
  ];
30
31
  }
31
32
 
package/src/config.ts CHANGED
@@ -14,6 +14,7 @@ export interface Config {
14
14
  autoRead: boolean;
15
15
  anchorGrepEnabled: boolean;
16
16
  copyMoveEnabled?: boolean;
17
+ replaceWithinEnabled?: boolean;
17
18
  autoReadAll?: AutoReadAllMode;
18
19
  autoReadAllIgnore?: string[];
19
20
  requirePath?: boolean;
@@ -25,6 +26,7 @@ const DEFAULT_CONFIG: Config = {
25
26
  autoRead: true,
26
27
  anchorGrepEnabled: true,
27
28
  copyMoveEnabled: true,
29
+ replaceWithinEnabled: true,
28
30
  autoReadAll: "off",
29
31
  autoReadAllIgnore: [],
30
32
  requirePath: false,
@@ -75,6 +77,7 @@ function parseConfig(content: string): Config {
75
77
  const autoRead = parsed.autoRead;
76
78
  const anchorGrepEnabled = parsed.anchorGrepEnabled;
77
79
  const copyMoveEnabled = parsed.copyMoveEnabled;
80
+ const replaceWithinEnabled = parsed.replaceWithinEnabled;
78
81
  const autoReadAll = parsed.autoReadAll;
79
82
  const requirePath = parsed.requirePath;
80
83
  const strictInput = parsed.strictInput;
@@ -84,6 +87,7 @@ function parseConfig(content: string): Config {
84
87
  autoRead: typeof autoRead === "boolean" ? autoRead : DEFAULT_CONFIG.autoRead,
85
88
  anchorGrepEnabled: typeof anchorGrepEnabled === "boolean" ? anchorGrepEnabled : DEFAULT_CONFIG.anchorGrepEnabled,
86
89
  copyMoveEnabled: typeof copyMoveEnabled === "boolean" ? copyMoveEnabled : DEFAULT_CONFIG.copyMoveEnabled,
90
+ replaceWithinEnabled: typeof replaceWithinEnabled === "boolean" ? replaceWithinEnabled : DEFAULT_CONFIG.replaceWithinEnabled,
87
91
  autoReadAll: parseAutoReadAllMode(autoReadAll),
88
92
  requirePath: typeof requirePath === "boolean" ? requirePath : DEFAULT_CONFIG.requirePath,
89
93
  strictInput: typeof strictInput === "boolean" ? strictInput : DEFAULT_CONFIG.strictInput,
@@ -196,7 +200,7 @@ export async function writeConfig(config: Config): Promise<void> {
196
200
  }
197
201
 
198
202
 
199
- type ToggleKey = "autoRead" | "anchorGrepEnabled" | "copyMoveEnabled" | "requirePath" | "strictInput";
203
+ type ToggleKey = "autoRead" | "anchorGrepEnabled" | "copyMoveEnabled" | "replaceWithinEnabled" | "requirePath" | "strictInput";
200
204
 
201
205
  async function toggleFlag(key: ToggleKey): Promise<boolean> {
202
206
  const config = await updateConfig((c) => { c[key] = !(c[key] === true); });
@@ -205,6 +209,7 @@ async function toggleFlag(key: ToggleKey): Promise<boolean> {
205
209
  export const toggleAutoRead = (): Promise<boolean> => toggleFlag("autoRead");
206
210
  export const toggleAnchorGrep = (): Promise<boolean> => toggleFlag("anchorGrepEnabled");
207
211
  export const toggleCopyMove = (): Promise<boolean> => toggleFlag("copyMoveEnabled");
212
+ export const toggleReplaceWithin = (): Promise<boolean> => toggleFlag("replaceWithinEnabled");
208
213
  export async function cycleAutoReadAllMode(): Promise<AutoReadAllMode> {
209
214
  let next: AutoReadAllMode = "off";
210
215
  await updateConfig((c) => {
package/src/constants.ts CHANGED
@@ -13,6 +13,12 @@ export const HASH_STORE_VERSION = 9;
13
13
  export const NEW_CONTENT_NOT_ARRAY_MSG =
14
14
  `[E_BAD_SHAPE] "replacement_lines" must be an array of strings, one per line (use [] to delete).`;
15
15
 
16
+ export const NEW_CONTENT_NOT_STRING_MSG =
17
+ `[E_BAD_SHAPE] "replacement_lines" must be a string holding the exact text to write. Use "" to delete the range and "\\n" for one blank line; line breaks inside the string separate lines.`;
18
+
19
+ export const LINES_NOT_STRING_MSG =
20
+ `[E_BAD_SHAPE] "lines" must be a string holding the exact text to insert. Use "" to insert nothing and "\\n" for one blank line; line breaks inside the string separate lines.`;
21
+
16
22
  export const NUL_CONTENT_MSG =
17
23
  `[E_BAD_SHAPE] Content contains a NUL byte (U+0000); a text file cannot contain NUL, and writing it would break further reads and edits. Remove the NUL byte and retry. An empty replacement ([]) deletes a range or inserts nothing.`;
18
24
 
package/src/copy-move.ts CHANGED
@@ -2,7 +2,7 @@ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-age
2
2
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
3
3
  import { Type } from "typebox";
4
4
  import { constants } from "node:fs";
5
- import { execPipeline, noteAnchorError, previewFromPipe, previewError, type PipelineResult, type ReqParams, type ReplaceDetails } from "./replace";
5
+ import { execPipeline, noteAnchorError, previewFromPipe, previewError, type PipelineResult, type ReplaceDetails } from "./replace";
6
6
  import { commitEdit } from "./commit";
7
7
  import { readNormFile, safeSnapId, type NormFile } from "./file-reader";
8
8
  import {
@@ -14,6 +14,7 @@ import {
14
14
  resolveAnchorLine,
15
15
  stripAnchorRow,
16
16
  type Anchor,
17
+ type HTEdit,
17
18
  } from "./hashline";
18
19
  import { formatAnchorReclaimNotice, servedForPath, takeReclaimedPaths, withAnchorSession } from "./anchor-registry";
19
20
  import { loadP, loadGuide } from "./prompts";
@@ -49,7 +50,7 @@ interface TransferRefs {
49
50
  }
50
51
 
51
52
  export interface TransferPlan {
52
- editParams: ReqParams;
53
+ editParams: HTEdit;
53
54
  foldedAnchorLines: number;
54
55
  anchorCarry?: number;
55
56
  servedOverride?: ReadonlyMap<string, string>;
@@ -224,10 +225,10 @@ interface CrossTransferPreparation {
224
225
  destinationPreload: NormFile;
225
226
  sourceDisplay: string;
226
227
  destinationDisplay: string;
227
- destinationEdit: ReqParams;
228
+ destinationEdit: HTEdit;
228
229
  destinationFolded: number;
229
230
  endingOverrides: (LineEnding | undefined)[];
230
- sourceEdit?: ReqParams;
231
+ sourceEdit?: HTEdit;
231
232
  }
232
233
 
233
234
  async function prepareCrossTransfer(input: {
@@ -292,7 +293,7 @@ async function prepareCrossTransfer(input: {
292
293
  }
293
294
 
294
295
  const moved = sourceLines.slice(sourceStart - 1, sourceEnd);
295
- const destinationEdit: ReqParams = destinationPreload.normalized.length === 0
296
+ const destinationEdit: HTEdit = destinationPreload.normalized.length === 0
296
297
  ? {
297
298
  remove_from: destinationPreload.fileHashes[0]!,
298
299
  remove_to: destinationPreload.fileHashes[0]!,
@@ -309,7 +310,7 @@ async function prepareCrossTransfer(input: {
309
310
  if (kind === "copy") {
310
311
  return { sourcePreload, destinationPreload, sourceDisplay, destinationDisplay, destinationEdit, destinationFolded, endingOverrides };
311
312
  }
312
- const sourceEdit: ReqParams = {
313
+ const sourceEdit: HTEdit = {
313
314
  remove_from: sourcePreload.fileHashes[sourceStart - 1]!,
314
315
  remove_to: sourcePreload.fileHashes[sourceEnd - 1]!,
315
316
  replacement_lines: [],
@@ -671,19 +672,19 @@ function getTransferInput(args: unknown): { path?: string; source_from?: string;
671
672
 
672
673
  const transferSourceFromSchema = Type.String({
673
674
  description:
674
- "Bare 4-char anchor from a served anchor│content row (the text before the `│` separator), never the row content. Marks the FIRST source line (inclusive); this anchor and `source_to` resolve the source file.",
675
+ "4-char anchor of the FIRST source line to copy (never the row content).",
675
676
  });
676
677
  const transferSourceToSchema = Type.String({
677
678
  description:
678
- "Bare 4-char anchor from a served anchor│content row (the text before the `│` separator), never the row content. Marks the LAST source line (inclusive); this anchor and `source_from` resolve the source file.",
679
+ "4-char anchor of the LAST source line to copy.",
679
680
  });
680
681
  const transferInsertAfterSchema = Type.String({
681
682
  description:
682
- "Bare 4-char anchor of the destination line after which the block goes; it may live in another file than the source, and the destination file is the one this anchor belongs to. The anchor line is preserved.",
683
+ "4-char anchor of the destination line; the block goes after it and may live in another file.",
683
684
  });
684
685
  const transferPathRequiredSchema = Type.String({
685
686
  description:
686
- "Path to the source or destination file the anchors were served for; required and must match anchor ownership. The anchors still resolve both files.",
687
+ "Path to the source or destination file the anchors were served for; required and must match anchor ownership.",
687
688
  });
688
689
 
689
690
  const transferToolSchema = Type.Object(
@@ -718,7 +719,6 @@ export function buildTransferToolDef(kind: TransferKind, flags: EditToolFlags =
718
719
  guidelines: loadGuide(`../prompts/${kind}-guidelines.md`),
719
720
  },
720
721
  flags,
721
- kind,
722
722
  );
723
723
  return {
724
724
  name: kind,