pi-hashline-edit-pro 2.5.1 → 2.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
@@ -1,5 +1,7 @@
1
1
  # pi-hashline-edit-pro
2
2
 
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
+
3
5
  Hash-anchored `read` and `replace` tools for [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent). Every line of a file gets a unique 3-character hash, and you edit by hash. No line numbers, no fuzzy matching, no edits landing on the wrong line.
4
6
 
5
7
  Fork of [pi-hashline-edit](https://github.com/RimuruW/pi-hashline-edit) by RimuruW, extended with 3-character hashes and collision resolution.
@@ -68,7 +70,7 @@ Edge cases:
68
70
  - UTF-16 and UTF-32 text (detected via BOM) is rejected, since editing it would corrupt the file.
69
71
  - Empty files come back as a single empty-line hash (`HASH│`); use `replace` on that hash to insert content.
70
72
  - 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.
71
- - Files over 238,328 lines are rejected with `[E_FILE_TOO_LARGE]`.
73
+ - Files over 238,328 lines or 100MB are rejected with `[E_FILE_TOO_LARGE]`.
72
74
 
73
75
  ## The replace tool
74
76
 
@@ -169,7 +171,7 @@ A no-op replace never changes the file, so anchors remain valid. On first run af
169
171
  | `[E_UNDO_STALE]` | `undo_last_replace` refused: the file was modified or deleted after the last replace. |
170
172
  | `[E_UNDO_UNAVAILABLE]` | Undo history could not be persisted to the hash store; the `replace` was refused and the file was left unchanged. |
171
173
  | `[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. |
172
- | `[E_FILE_TOO_LARGE]` | The file exceeds the 238,328-line hashline limit. |
174
+ | `[E_FILE_TOO_LARGE]` | The file exceeds the 238,328-line hashline limit or the 100MB size limit. |
173
175
 
174
176
  ## Troubleshooting
175
177
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "2.5.1",
3
+ "version": "2.5.2",
4
4
  "type": "module",
5
5
  "description": "Hash-anchored read/replace/undo tools for pi-coding-agent. Every line gets a unique 3-char hash (A-Za-z0-9) that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
6
6
  "main": "index.ts",
package/src/file-kind.ts CHANGED
@@ -54,7 +54,8 @@ export type LFile =
54
54
  | { kind: "directory" }
55
55
  | { kind: "image"; mimeType: string }
56
56
  | { kind: "text"; text: string; hadUtf8DecodeErrors?: true }
57
- | { kind: "binary"; description: string };
57
+ | { kind: "binary"; description: string }
58
+ | { kind: "too_large"; description: string };
58
59
 
59
60
 
60
61
  export interface LoadFileOptions {
@@ -78,8 +79,8 @@ export async function loadFileKindAndText(
78
79
  }
79
80
  if (pathStat.size > MAX_BYTES) {
80
81
  return {
81
- kind: "binary",
82
- description: `file exceeds ${MAX_BYTES} byte limit`
82
+ kind: "too_large",
83
+ description: `exceeds the ${MAX_BYTES / (1024 * 1024)}MB size limit`,
83
84
  };
84
85
  }
85
86
 
package/src/hash-store.ts CHANGED
@@ -49,6 +49,21 @@ export function isValidHashList(value: unknown): value is string[] {
49
49
  return true;
50
50
  }
51
51
 
52
+ export function parseHashList(raw: string, onInvalid: () => void): string[] | undefined {
53
+ let parsed: unknown;
54
+ try {
55
+ parsed = JSON.parse(raw);
56
+ } catch {
57
+ onInvalid();
58
+ return undefined;
59
+ }
60
+ if (!isValidHashList(parsed)) {
61
+ onInvalid();
62
+ return undefined;
63
+ }
64
+ return parsed;
65
+ }
66
+
52
67
  function isValidSnapshot(value: unknown): value is LegacySnapshot {
53
68
  if (typeof value !== "object" || value === null) return false;
54
69
  const v = value as Record<string, unknown>;
@@ -418,21 +433,13 @@ export function getSnapshot(
418
433
  }
419
434
  const row = store.stmts.get(path, checksum, lineCount);
420
435
  if (!row) return undefined;
421
- let parsed: unknown;
422
- try {
423
- parsed = JSON.parse(row.hashes as string);
424
- } catch {
436
+ const parsed = parseHashList(row.hashes as string, () => {
425
437
  if (deleteCorrupt) store.stmts.deleteOne(path);
426
438
  snapshotCache.delete(path);
427
- return undefined;
428
- }
429
- if (isValidHashList(parsed)) {
430
- cacheSnapshot(path, checksum, lineCount, parsed);
431
- return parsed;
432
- }
433
- if (deleteCorrupt) store.stmts.deleteOne(path);
434
- snapshotCache.delete(path);
435
- return undefined;
439
+ });
440
+ if (!parsed) return undefined;
441
+ cacheSnapshot(path, checksum, lineCount, parsed);
442
+ return parsed;
436
443
  }
437
444
 
438
445
  export function upsertSnapshot(
@@ -461,23 +468,15 @@ export function upsertUndo(store: HashStore, path: string, entry: UndoRecord): v
461
468
  export function getUndoEntry(store: HashStore, path: string): UndoRecord | undefined {
462
469
  const row = store.stmts.undoGet(path);
463
470
  if (!row) return undefined;
464
- try {
465
- const parsed = JSON.parse(row.hashes as string);
466
- if (!isValidHashList(parsed)) {
467
- store.stmts.undoDelete(path);
468
- return undefined;
469
- }
470
- return {
471
- content: row.content as string,
472
- bom: row.bom as string,
473
- ending: row.ending as string,
474
- hashes: parsed as string[],
475
- resultContent: row.result_content as string,
476
- };
477
- } catch {
478
- store.stmts.undoDelete(path);
479
- return undefined;
480
- }
471
+ const parsed = parseHashList(row.hashes as string, () => store.stmts.undoDelete(path));
472
+ if (!parsed) return undefined;
473
+ return {
474
+ content: row.content as string,
475
+ bom: row.bom as string,
476
+ ending: row.ending as string,
477
+ hashes: parsed,
478
+ resultContent: row.result_content as string,
479
+ };
481
480
  }
482
481
 
483
482
  export function deleteUndo(store: HashStore, path: string): void {
package/src/served.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { HashStore } from "./hash-store";
2
- import { isValidHashList } from "./hash-store";
2
+ import { parseHashList } from "./hash-store";
3
3
  import { HASH_CLASS } from "./hashline/alphabet";
4
4
 
5
5
  const SERVED_DIFF_ROW_RE = new RegExp(`^[+ ](${HASH_CLASS})│`);
@@ -16,17 +16,9 @@ export function servedHashesFromDiff(diff: string): string[] {
16
16
  export function getServed(store: HashStore, path: string): Set<string> | undefined {
17
17
  const row = store.stmts.servedGet(path);
18
18
  if (!row) return undefined;
19
- try {
20
- const parsed = JSON.parse(row.hashes as string);
21
- if (!isValidHashList(parsed)) {
22
- store.stmts.servedDelete(path);
23
- return undefined;
24
- }
25
- return new Set(parsed);
26
- } catch {
27
- store.stmts.servedDelete(path);
28
- return undefined;
29
- }
19
+ const parsed = parseHashList(row.hashes as string, () => store.stmts.servedDelete(path));
20
+ if (!parsed) return undefined;
21
+ return new Set(parsed);
30
22
  }
31
23
 
32
24
  export function recordServed(store: HashStore, path: string, hashes: string[]): void {
package/src/utils.ts CHANGED
@@ -16,9 +16,7 @@ export function splitLines(text: string): string[] {
16
16
  }
17
17
 
18
18
  export function visLines(text: string): string[] {
19
- if (text.length === 0) return [];
20
- const lines = text.split("\n");
21
- return text.endsWith("\n") ? lines.slice(0, -1) : lines;
19
+ return text.length === 0 ? [] : splitLines(text);
22
20
  }
23
21
 
24
22
 
package/src/validation.ts CHANGED
@@ -36,5 +36,10 @@ export function valKind(file: LFile, path: string): asserts file is { kind: "tex
36
36
  if (file.kind === "image") {
37
37
  throw new Error(`[E_NOT_TEXT] Path is an image file: ${path}. Hashline edit only supports text files.`);
38
38
  }
39
+ if (file.kind === "too_large") {
40
+ throw new Error(
41
+ `[E_FILE_TOO_LARGE] File is too large: ${path} (${file.description}). Hashline editing targets source-sized files; for very large files use write or a non-line-based approach.`,
42
+ );
43
+ }
39
44
  }
40
45