pi-hashline-edit-pro 0.4.2 → 0.4.3

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
@@ -33,7 +33,7 @@ pi install /path/to/pi-hashline-edit-pro
33
33
 
34
34
  ### `read` -- tagged line output
35
35
 
36
- Text files are returned with a `HASH|content` prefix on every line. The line number is not part of the wire format, only the 4-character hash followed by the `|` separator and the line content. Example output for the source below:
36
+ Text files are returned with a `HASHcontent` prefix on every line. The line number is not part of the wire format, only the 4-character hash followed by the `│` separator and the line content. Example output for the source below:
37
37
 
38
38
  ```js
39
39
  function hello() {
@@ -44,9 +44,9 @@ function hello() {
44
44
  would be returned as:
45
45
 
46
46
  ```text
47
- 0qH3|function hello() {
48
- szJr| console.log("world");
49
- _zlP|}
47
+ 0qH3function hello() {
48
+ szJr console.log("world");
49
+ _zlP}
50
50
  ```
51
51
 
52
52
  - `HASH` is a 4-character content hash from the URL-safe base64 alphabet `A-Za-z0-9-_` (e.g. `aB3x`).
@@ -60,7 +60,7 @@ Images (JPEG, PNG, GIF, WebP) are passed through as attachments and do not parti
60
60
 
61
61
  ### `edit` -- hash-anchored modifications
62
62
 
63
- Edits use the `HASH|content` anchors from `read` output to target lines precisely:
63
+ Edits use the `HASHcontent` anchors from `read` output to target lines precisely:
64
64
 
65
65
  ```json
66
66
  {
@@ -84,11 +84,11 @@ All edits in a single call validate against the same pre-edit snapshot and apply
84
84
 
85
85
  ### Chained edits
86
86
 
87
- After a successful edit, the result text contains an `--- Anchors ---` block with fresh `HASH|content` references for the changed region. These can be used directly in the next `edit` call on the same file without a full re-read, provided the next edit targets the same or nearby lines. For distant changes, use `read` first.
87
+ After a successful edit, the result text contains an `--- Anchors ---` block with fresh `HASHcontent` references for the changed region. These can be used directly in the next `edit` call on the same file without a full re-read, provided the next edit targets the same or nearby lines. For distant changes, use `read` first.
88
88
 
89
89
  ### Auto-read after write
90
90
 
91
- After a successful `write`, the extension automatically reads the file and appends a `--- Auto-read (hashline anchors) ---` block to the result. This gives the model immediate `HASH|content` anchors for the newly written file without requiring a separate `read` call. The workflow becomes:
91
+ After a successful `write`, the extension automatically reads the file and appends a `--- Auto-read (hashline anchors) ---` block to the result. This gives the model immediate `HASHcontent` anchors for the newly written file without requiring a separate `read` call. The workflow becomes:
92
92
 
93
93
  1. `write` a file, result includes hashline anchors
94
94
  2. `edit` using those anchors directly
@@ -97,20 +97,20 @@ For large files (>2000 lines), the auto-read output is truncated with a paginati
97
97
 
98
98
  ### Diff for the host
99
99
 
100
- The post-edit diff (with `+`/`-` markers and new `HASH|content` anchors) is exposed to the host UI via `details.diff`. It is intentionally not in the LLM-visible text. The model only needs the fresh anchors in `text` to chain follow-up edits, and re-emitting the diff would cost extra tokens.
100
+ The post-edit diff (with `+`/`-` markers and new `HASHcontent` anchors) is exposed to the host UI via `details.diff`. It is intentionally not in the LLM-visible text. The model only needs the fresh anchors in `text` to chain follow-up edits, and re-emitting the diff would cost extra tokens.
101
101
 
102
102
  ## Design Decisions
103
103
 
104
- - **Stale anchors fail.** A hash mismatch means the file has changed since the last `read`. The error includes fresh `>>> HASH|content` lines for the affected region. The model copies the HASH portion and retries.
104
+ - **Stale anchors fail.** A hash mismatch means the file has changed since the last `read`. The error includes fresh `>>> HASHcontent` lines for the affected region. The model copies the HASH portion and retries.
105
105
  - **No fallback relocation.** Mismatched anchors are never silently relocated to a "close enough" line. This trades convenience for correctness.
106
- - **Strict patch content.** If `lines` contains `+HASH|` display prefixes (or `-N ` diff rows), the edit is rejected with `[E_INVALID_PATCH]`. Bare `HASH|` content (the first 5 chars of a `lines` entry looking like 4 base64 chars + `|`) is also rejected with `[E_BARE_HASH_PREFIX]`. When the suspect's prefix happens to match a real file-line anchor, the error message flags that as strong evidence the model copied an anchor from the read output.
106
+ - **Strict patch content.** If `lines` contains `+HASH│` display prefixes (or `-N ` diff rows), the edit is rejected with `[E_INVALID_PATCH]`. Bare `HASH│` content (the first 5 chars of a `lines` entry looking like 4 base64 chars + `│`) is also rejected with `[E_BARE_HASH_PREFIX]`. When the suspect's prefix happens to match a real file-line anchor, the error message flags that as strong evidence the model copied an anchor from the read output.
107
107
  - **Legacy dialect rejected.** The native top-level `oldText`/`newText` (and `old_text`/`new_text`) dialect and `op: "replace_text"` are rejected with `[E_LEGACY_SHAPE]`. The error message tells the model to call `read` first and send `{op:"replace", start:"<HASH>", end:"<HASH>", lines:[...]}` (or `append`/`prepend` with `pos`).
108
108
  - **Atomic writes.** Files are written via temp-file-then-rename to avoid corruption from interrupted writes. Symlink chains are resolved so the target file is updated without replacing the symlink. Hard-linked files are updated in place to preserve the shared inode. File permissions are preserved across atomic renames.
109
109
  - **Per-file mutation queue.** Edits queue by the canonical write target, so concurrent edits through different symlink paths still serialize onto the same underlying file.
110
110
 
111
111
  ## Hashing
112
112
 
113
- Hashes are computed with [xxhashjs](https://github.com/pierrec/js-xxhash) (xxHash32), then mapped to a 4-character string from the URL-safe base64 alphabet `A-Za-z0-9-_`. That's 64 distinct characters, 6 bits per position, 24 bits of entropy per anchor.
113
+ Hashes are computed with [xxhash-wasm](https://github.com/jungomi/xxhash-wasm) (xxHash32 via WebAssembly), then mapped to a 4-character string from the URL-safe base64 alphabet `A-Za-z0-9-_`. That's 64 distinct characters, 6 bits per position, 24 bits of entropy per anchor.
114
114
 
115
115
  The alphabet is sized for an LLM consumer. The model tokenizes, it doesn't squint at pixel glyphs, so the human-readability heuristics used by smaller hand-curated alphabets (no G/L/I/O because they look like digits, no vowels so the hash doesn't accidentally spell a word, no hex digits so it can't be confused with `0xFF`) don't apply. The full 64 chars give maximum entropy per character, with case and digits included.
116
116
 
@@ -125,7 +125,7 @@ The runtime always precomputes the full per-line hash array for a file via `comp
125
125
 
126
126
  ### Bare-prefix detector
127
127
 
128
- With the `|` delimiter format, the bare-prefix detector regex `^\s*[A-Za-z0-9_-]{4}|` is highly specific. It only matches lines starting with exactly 4 base64 chars and `|`. This eliminates false positives from common code patterns like `init:`, `data:`, `else:`, etc. The detector rejects edit lines matching this pattern with `[E_BARE_HASH_PREFIX]` to prevent the model from accidentally pasting hash anchors into file content.
128
+ With the `│` delimiter format, the bare-prefix detector regex `^\s*[A-Za-z0-9_\-]{4}│` is highly specific. It only matches lines starting with exactly 4 base64 chars and `│`. This eliminates false positives from common code patterns like `init:`, `data:`, `else:`, etc. The detector rejects edit lines matching this pattern with `[E_BARE_HASH_PREFIX]` to prevent the model from accidentally pasting hash anchors into file content.
129
129
 
130
130
  ## Development
131
131
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Strict hashline read/edit tool override for pi-coding-agent with hash-anchored edits (4-char, 24-bit)",
5
5
  "main": "index.ts",
6
6
  "repository": {
@@ -0,0 +1,3 @@
1
+ - Use edit with HASH anchors for all file changes; batch every change to one file into a single edit call.
2
+ - After a successful edit, the returned `--- Anchors ---` block replaces a re-read for nearby follow-up edits.
3
+ - On `[E_STALE_ANCHOR]`, retry with the `>>>` lines quoted in the error instead of re-reading the whole file.
@@ -1 +1 @@
1
- Edit a text file via HASH anchors from read
1
+ Edit a text file via HASH anchors from read, batching all edits to a file in one call
package/prompts/read.md CHANGED
@@ -22,6 +22,9 @@ File kinds:
22
22
  - Images (JPEG, PNG, GIF, WebP) are returned as visual attachments; the HASH-line protocol does not apply.
23
23
  - Binary files and directories are rejected with a descriptive error.
24
24
 
25
+ Non-UTF-8 bytes:
26
+ - Non-UTF-8 bytes are decoded as U+FFFD. The output is flagged when this happens; editing such a file rewrites it as UTF-8. Recover the original encoding with `iconv` afterwards if it must survive.
27
+
25
28
  Auto-read after write:
26
29
  - After a successful `write`, the result includes a `--- Auto-read (hashline anchors) ---` block with `HASH│content` for the written file.
27
30
  - Use those anchors directly for `edit` calls without a separate `read`.
package/src/edit.ts CHANGED
@@ -198,6 +198,15 @@ const EDIT_PROMPT_SNIPPET = readFileSync(
198
198
  "utf-8",
199
199
  ).trim();
200
200
 
201
+
202
+ const EDIT_PROMPT_GUIDELINES = readFileSync(
203
+ new URL("../prompts/edit-guidelines.md", import.meta.url),
204
+ "utf-8",
205
+ )
206
+ .split("\n")
207
+ .map((line) => line.trim())
208
+ .filter((line) => line.startsWith("- "))
209
+ .map((line) => line.slice(2));
201
210
  const ROOT_KEYS = new Set(["path", "returnMode", "returnRanges", "edits"]);
202
211
 
203
212
  export function assertEditRequest(
@@ -426,6 +435,7 @@ const editToolDefinition: EditToolDefinition = {
426
435
  description: EDIT_DESC,
427
436
  parameters: hashlineEditToolSchema,
428
437
  promptSnippet: EDIT_PROMPT_SNIPPET,
438
+ promptGuidelines: EDIT_PROMPT_GUIDELINES,
429
439
  prepareArguments: (args: unknown) =>
430
440
  normalizeEditRequest(args) as EditRequestParams,
431
441
  renderShell: "default",