pi-hashline-edit-pro 4.5.1 → 4.5.2

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
@@ -186,7 +186,7 @@ Edge cases:
186
186
  | --- | --- |
187
187
  | `remove_from` | 4-char anchor marking the FIRST line to remove (inclusive). |
188
188
  | `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 that line's ending; escape sequences such as `\n` or `\u200b` are not decoded. A lone string is accepted too: it is split on newlines, and stringified array text is unwrapped. |
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 that line's ending; escapes decode once — `\uXXXX` is the character, `\\uXXXX` the literal text. A lone string is accepted too: it is split on newlines, and stringified array text is unwrapped. |
190
190
 
191
191
  Example: read showed `Hasu│old` and `arvm│old2`; to replace both:
192
192
 
@@ -220,7 +220,7 @@ After a successful edit, the diff is capped at 50KB. A row over 50KB is shown as
220
220
  | --- | --- |
221
221
  | `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
222
  | `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 that line's ending; escape sequences such as `\n` or `\u200b` are not decoded. A lone string is split on newlines, and stringified array text is unwrapped. |
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 that line's ending; escapes decode once — `\uXXXX` is the character, `\\uXXXX` the literal text. A lone string is split on newlines, and stringified array text is unwrapped. |
224
224
 
225
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.
226
226
 
@@ -357,7 +357,7 @@ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a se
357
357
  | `autoReadAllIgnore` | Ignore folders/files | `[]` | Extra folder names, file names, or globs skipped by auto-read all. |
358
358
  | `anchorGrepEnabled` | Anchor grep | `true` | Register `anchor_grep` and disable the built-in grep while it is on. |
359
359
  | `copyMoveEnabled` | Copy/move | `true` | Offer the `copy` and `move` tools; when off, both are removed from the active tools. |
360
- | `requirePath` | Require path | `false` | `replace` and `insert` require a `path` argument that must match anchor ownership. |
360
+ | `requirePath` | Require path | `false` | `replace`, `insert`, `copy`, and `move` require a `path` argument that must match anchor ownership. |
361
361
  | `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. |
362
362
  | `diffContextLines` | Diff context | `1` | Surrounding lines in post-edit diffs, 0-10 (needs Auto-read). |
363
363
 
@@ -369,7 +369,7 @@ When `PI_HASHLINE_DIR` is unset or empty, non-Windows platforms honor `XDG_CONFI
369
369
  | --- | --- | --- |
370
370
  | Output cap | 2000 lines and 50KB | `read`, auto-read after `write`, post-edit diffs, patches, previews, `details.patch` |
371
371
  | Oversized row | 50KB per `anchor│content` row | replaced by an anchor-keeping marker you can still edit through |
372
- | Line cap | 1,353,139 lines per file | `read`, `replace`, `insert` (`[E_FILE_TOO_LARGE]`) |
372
+ | Line cap | 1,353,139 lines per file | `read`, `replace`, `insert`, `copy`, `move` (`[E_FILE_TOO_LARGE]`) |
373
373
  | File size | 100MB | all tools (`[E_FILE_TOO_LARGE]`) |
374
374
  | Hash window | first 500 bytes of a line | anchor identity for long lines |
375
375
  | Patch guard | 1MB of pre-edit + post-edit text | patch generation is skipped and `patchTruncated` is set |
@@ -423,7 +423,7 @@ Full reference:
423
423
  | `[W_INVALID_PATCH]` | A `replacement_lines` element is a diff-preview row (`+anchor│`, `-anchor│`, `- │`). The marker is stripped automatically with a warning. |
424
424
  | `[W_BARE_HASH_PREFIX]` | A `replacement_lines` element starts with an `anchor│` prefix. The prefix is stripped automatically with a warning. |
425
425
  | `[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. |
426
- | `[H_LITERAL_ESCAPE]` | `lines` or `replacement_lines` contains the literal escaped text such as `\u200b` or `\n`; the file receives those characters as written. Decode the escapes first if the text came from a quoted prompt. |
426
+ | `[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. |
427
427
  | `[E_WOULD_EMPTY]` | An edit would empty a non-empty file; use `write` instead. A cross-file `move` may empty its source file. |
428
428
  | `[E_NOT_FOUND]` | The path does not exist. |
429
429
  | `[E_ACCESS]` | The file is not readable or writable. |
@@ -479,7 +479,7 @@ Background snapshot pruning and registry sidecar GC skip `EPERM`/`EACCES` withou
479
479
 
480
480
  ### Allocation
481
481
 
482
- 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 26⁴ letter combinations), so a file can hold at most 1,353,139 lines, beyond which `read`, `replace`, and `insert` reject with `[E_FILE_TOO_LARGE]` (use `write` for very large files).
482
+ 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).
483
483
 
484
484
  ### Ownership and mapping across edits
485
485
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "4.5.1",
3
+ "version": "4.5.2",
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/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. Inserted lines are written exactly as given; nothing else in the file changes.
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.
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,4 +1,4 @@
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. `[]` deletes the range.
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.
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.
4
4
 
package/src/config-ui.ts CHANGED
@@ -24,7 +24,7 @@ export function configRows(config: Config): ConfigRow[] {
24
24
  { key: "diffContextLines", label: "Diff context", hint: "Surrounding lines in post-edit diffs (needs Auto-read)", enabled: config.autoRead !== false, value: config.diffContextLines ?? 1, disabled: config.autoRead === false },
25
25
  { key: "anchorGrepEnabled", label: "Anchor grep", hint: "anchor_grep tool (builtin grep off while on)", enabled: config.anchorGrepEnabled === true },
26
26
  { key: "copyMoveEnabled", label: "Copy/move", hint: "copy and move tools (both off while disabled)", enabled: config.copyMoveEnabled !== false },
27
- { key: "requirePath", label: "Require path", hint: "replace + insert need path (RPC visibility)", enabled: config.requirePath === true },
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
29
  ];
30
30
  }
package/src/grep.ts CHANGED
@@ -298,14 +298,30 @@ export async function resolveRgPath(): Promise<string> {
298
298
  throw new Error("[E_ACCESS] ripgrep (rg) is required for grep but was not found. Install ripgrep or ensure pi can download it to ~/.pi/agent/bin.");
299
299
  }
300
300
 
301
+ async function insideGitRepo(start: string): Promise<boolean> {
302
+ let current = start;
303
+ for (;;) {
304
+ try {
305
+ await stat(join(current, ".git"));
306
+ return true;
307
+ } catch {
308
+ const parent = dirname(current);
309
+ if (parent === current) return false;
310
+ current = parent;
311
+ }
312
+ }
313
+ }
314
+
301
315
  async function collectRgMatches(
302
316
  rgPath: string,
303
317
  pattern: string,
304
318
  searchPath: string,
305
319
  req: GrepReq,
306
- signal?: AbortSignal,
320
+ signal: AbortSignal | undefined,
321
+ repoRooted: boolean,
307
322
  ): Promise<Map<string, number[]>> {
308
323
  const args = ["--json", "--line-number", "--color=never", "--hidden", "--glob", "!.git"];
324
+ if (!repoRooted) args.push("--no-require-git");
309
325
  const wanted = req.limit ?? 100;
310
326
  args.push("--max-count", String(wanted + 1));
311
327
  if (typeof req.glob === "string" && req.glob.length > 0) {
@@ -583,7 +599,8 @@ export function regGrep(pi: ExtensionAPI): void {
583
599
  };
584
600
  const readGrepFile = makeGrepReader("real");
585
601
  const readGrepFileShadow = makeGrepReader("shadow");
586
- const rgMatches = await collectRgMatches(rgPath, req.pattern, base, req, signal);
602
+ const repoRooted = await insideGitRepo(globRoot);
603
+ const rgMatches = await collectRgMatches(rgPath, req.pattern, base, req, signal, repoRooted);
587
604
  const sortedFiles = [...rgMatches.keys()].sort(cmp);
588
605
  for (let f = 0; f < sortedFiles.length; f++) {
589
606
  abortIf(signal);