pi-hashline-edit-pro 2.5.2 → 2.5.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/index.ts CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  toggleAutoRead,
14
14
  } from "./src/config";
15
15
  import { loadHashStore, pruneMissing } from "./src/hash-store";
16
- import { recordServed, clearServed } from "./src/served";
16
+ import { recordServedSafe, clearServed } from "./src/served";
17
17
  import { readNormFile } from "./src/file-reader";
18
18
  import { loadFileKindAndText } from "./src/file-kind";
19
19
  import { toCwd } from "./src/paths";
@@ -88,12 +88,7 @@ export default function (pi: ExtensionAPI): void {
88
88
  DEFAULT_MAX_BYTES,
89
89
  AUTO_READ_MAX,
90
90
  );
91
- try {
92
- const store = await loadHashStore();
93
- recordServed(store, absolutePath, preview.servedHashes);
94
- } catch (error) {
95
- console.error("Failed to record served state from auto-read:", error);
96
- }
91
+ await recordServedSafe(absolutePath, preview.servedHashes, "auto-read");
97
92
  return {
98
93
  content: [
99
94
  ...(event.content ?? []),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "2.5.2",
3
+ "version": "2.5.3",
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/config.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFile } from "fs/promises";
2
2
  import { configPath } from "./paths";
3
- import { errCode } from "./utils";
3
+ import { errCode, isRec } from "./utils";
4
4
  import { writeAtomic } from "./fs-write";
5
5
 
6
6
  export interface Config {
@@ -12,10 +12,12 @@ const DEFAULT_CONFIG: Config = {
12
12
  };
13
13
 
14
14
  function parseConfig(content: string): Config {
15
- const parsed = JSON.parse(content) as Partial<Config>;
16
- return {
17
- autoRead: parsed.autoRead === true,
18
- };
15
+ const parsed = JSON.parse(content) as unknown;
16
+ const autoRead = isRec(parsed) ? parsed.autoRead : undefined;
17
+ if (typeof autoRead !== "boolean") {
18
+ throw new Error("config.json must be an object with a boolean autoRead field");
19
+ }
20
+ return { autoRead };
19
21
  }
20
22
 
21
23
 
@@ -45,6 +45,18 @@ export async function fileSnap(absolutePath: string): Promise<SnapInfo> {
45
45
  };
46
46
  }
47
47
 
48
+ export async function safeSnapId(
49
+ absolutePath: string,
50
+ context: string,
51
+ ): Promise<string | undefined> {
52
+ try {
53
+ return (await fileSnap(absolutePath)).snapshotId;
54
+ } catch (error) {
55
+ console.error(`Failed to compute snapshot (${context}):`, error);
56
+ return undefined;
57
+ }
58
+ }
59
+
48
60
  export interface ReadNormOptions {
49
61
  signal?: AbortSignal;
50
62
  accessMode?: number;
package/src/hash-store.ts CHANGED
@@ -286,7 +286,11 @@ async function openStore(storePath: string): Promise<HashStore> {
286
286
  const { db, stmts } = opened;
287
287
 
288
288
  if (!existed) {
289
- await migrateLegacy(db);
289
+ try {
290
+ await migrateLegacy(db);
291
+ } catch (error) {
292
+ console.error("Hash store migration failed; continuing without legacy import:", error);
293
+ }
290
294
  }
291
295
  cachedDb = { path: storePath, db, stmts };
292
296
 
@@ -328,20 +332,19 @@ export function shutdownHashStore(): void {
328
332
  }
329
333
 
330
334
  function withStore(fn: () => void): void {
331
- if (cachedDb) {
332
- withBusyRetry(() => {
333
- cachedDb!.db.exec("BEGIN IMMEDIATE");
334
- try {
335
- fn();
336
- cachedDb!.db.exec("COMMIT");
337
- } catch (e) {
338
- try { cachedDb!.db.exec("ROLLBACK"); } catch {}
339
- throw e;
340
- }
341
- });
342
- } else {
343
- fn();
335
+ if (!cachedDb) {
336
+ throw new Error("Hash store is not open; transactional update aborted");
344
337
  }
338
+ withBusyRetry(() => {
339
+ cachedDb!.db.exec("BEGIN IMMEDIATE");
340
+ try {
341
+ fn();
342
+ cachedDb!.db.exec("COMMIT");
343
+ } catch (e) {
344
+ try { cachedDb!.db.exec("ROLLBACK"); } catch {}
345
+ throw e;
346
+ }
347
+ });
345
348
  }
346
349
 
347
350
  async function migrateLegacy(db: DatabaseSync): Promise<void> {
@@ -388,17 +391,19 @@ async function migrateLegacy(db: DatabaseSync): Promise<void> {
388
391
  ]);
389
392
  }
390
393
  if (rows.length > 0) {
391
- db.exec("BEGIN IMMEDIATE");
392
- try {
393
- const stmt = db.prepare(
394
- "INSERT OR REPLACE INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?)"
395
- );
396
- for (const row of rows) stmt.run(...row);
397
- db.exec("COMMIT");
398
- } catch (e) {
399
- db.exec("ROLLBACK");
400
- throw e;
401
- }
394
+ withBusyRetry(() => {
395
+ db.exec("BEGIN IMMEDIATE");
396
+ try {
397
+ const stmt = db.prepare(
398
+ "INSERT OR REPLACE INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?)"
399
+ );
400
+ for (const row of rows) stmt.run(...row);
401
+ db.exec("COMMIT");
402
+ } catch (e) {
403
+ try { db.exec("ROLLBACK"); } catch {}
404
+ throw e;
405
+ }
406
+ });
402
407
  }
403
408
 
404
409
  try {
@@ -6,9 +6,6 @@ export {
6
6
  HASH_SPACE,
7
7
  HASH_PROBE_STRIDE,
8
8
  MAX_HASH_LINES,
9
- HL_PREFIX_PLUS_RE,
10
- HL_PREFIX_MINUS_RE,
11
- HL_BARE_PREFIX_RE,
12
9
  lineHashes,
13
10
  _lineHashesPure,
14
11
  initHasher,
@@ -22,7 +19,6 @@ export {
22
19
  } from "./parse";
23
20
 
24
21
  export {
25
- type RAnchor,
26
22
  type HEdit,
27
23
  type RHEdit,
28
24
  type HTEdit,
@@ -34,7 +30,6 @@ export {
34
30
  stripBarePrefixes,
35
31
  stripDiffPrefixes,
36
32
  swapReversedRanges,
37
- fmtMismatch,
38
33
  findNewEdge,
39
34
  assertRangeServed,
40
35
  RangeStaleError,
@@ -133,14 +133,6 @@ export function fmtMismatchWithHashes(
133
133
  return { text: out.join("\n"), hashes };
134
134
  }
135
135
 
136
- export function fmtMismatch(
137
- mismatches: HMismatch[],
138
- fileLines: string[],
139
- fileHashes: string[],
140
- filePath?: string,
141
- ): string {
142
- return fmtMismatchWithHashes(mismatches, fileLines, fileHashes, filePath).text;
143
- }
144
136
 
145
137
  const ITEM_KS = new Set(["replacement_text", "remove_from", "remove_to"]);
146
138
 
package/src/read.ts CHANGED
@@ -9,13 +9,11 @@ import {
9
9
  import { Type } from "typebox";
10
10
  import { MAX_READ_LINE_BYTES } from "./constants";
11
11
  import { loadFileKindAndText } from "./file-kind";
12
- import { readNormFile } from "./file-reader";
12
+ import { readNormFile, safeSnapId } from "./file-reader";
13
13
  import { lineHashes, fmtRegion, HASH_SEP, MAX_HASH_LINES } from "./hashline";
14
14
  import { toCwd } from "./paths";
15
- import { abortIf, isRec, normalizeFilePath, visLines } from "./utils";
16
- import { fileSnap } from "./file-reader";
17
- import { loadHashStore } from "./hash-store";
18
- import { recordServed } from "./served";
15
+ import { abortIf, makePrepareArguments, visLines } from "./utils";
16
+ import { recordServedSafe } from "./served";
19
17
  import { loadP, loadGuide } from "./prompts";
20
18
  import { valAccess } from "./validation";
21
19
 
@@ -167,12 +165,7 @@ export function regRead(pi: ExtensionAPI): void {
167
165
  description: R_DESC,
168
166
  promptSnippet: R_SNIPPET,
169
167
  promptGuidelines: readGuide(),
170
- prepareArguments: (args: unknown) => {
171
- if (!isRec(args)) return args as any;
172
- const record = { ...args };
173
- normalizeFilePath(record);
174
- return record;
175
- },
168
+ prepareArguments: makePrepareArguments(),
176
169
  parameters: Type.Object({
177
170
  path: Type.String({
178
171
  description: "Path to the file to read (relative or absolute)",
@@ -223,18 +216,8 @@ export function regRead(pi: ExtensionAPI): void {
223
216
  fileHashes,
224
217
  resolvedPath,
225
218
  );
226
- try {
227
- const store = await loadHashStore();
228
- recordServed(store, resolvedPath, preview.servedHashes);
229
- } catch (error) {
230
- console.error("Failed to record served state from read:", error);
231
- }
232
- let snapshotId: string | undefined;
233
- try {
234
- snapshotId = (await fileSnap(absolutePath)).snapshotId;
235
- } catch (error) {
236
- console.error("Failed to compute snapshot for read:", error);
237
- }
219
+ await recordServedSafe(resolvedPath, preview.servedHashes, "read");
220
+ const snapshotId = await safeSnapId(absolutePath, "read");
238
221
  const previewText =
239
222
  hadUtf8DecodeErrors
240
223
  ? `${preview.text}\n\n[Non-UTF-8 bytes shown as U+FFFD; editing rewrites the file as UTF-8.]`
@@ -101,7 +101,7 @@ export function buildNoop(input: NoopInput): TResult {
101
101
  ? `Replacement for ${noopEdit.loc} is identical to current content:\n ${noopEdit.loc}: ${clipLine(noopEdit.currentContent)}`
102
102
  : "The edit produced identical content.";
103
103
 
104
- const text = `No changes made to ${path}\nClassification: noop\n${noopDetailsText}`;
104
+ const text = `No changes made to ${path}\nClassification: noop\n${noopDetailsText}${warnBlock(warnings)}`;
105
105
 
106
106
  const metrics = buildMetrics({
107
107
  classification: "noop",
@@ -8,7 +8,7 @@ import { contentChecksum } from "./hashline/hasher";
8
8
  import { resolveTarget, writeAtomic } from "./fs-write";
9
9
  import { toCwd } from "./paths";
10
10
  import { toLF, stripBOM, genDiff, restoreEndings, type LineEnding } from "./replace-diff";
11
- import { cntDiff, splitLines, errCode, isRec, normalizeFilePath } from "./utils";
11
+ import { cntDiff, splitLines, errCode, makePrepareArguments } from "./utils";
12
12
  import { loadP, loadGuide } from "./prompts";
13
13
  import { buildMetrics } from "./replace-response";
14
14
  import { changedRange, lineHashes } from "./hashline";
@@ -92,12 +92,7 @@ export function regReplaceUndo(pi: ExtensionAPI): void {
92
92
  description: loadP("../prompts/undo-last-replace.md"),
93
93
  promptSnippet: loadP("../prompts/undo-last-replace-snippet.md"),
94
94
  promptGuidelines: loadGuide("../prompts/undo-last-replace-guidelines.md"),
95
- prepareArguments: (args: unknown) => {
96
- if (!isRec(args)) return args as any;
97
- const record = { ...args };
98
- normalizeFilePath(record);
99
- return record;
100
- },
95
+ prepareArguments: makePrepareArguments(),
101
96
  parameters: Type.Object({
102
97
  path: Type.String({
103
98
  description: "Path to the file to undo",
package/src/replace.ts CHANGED
@@ -11,9 +11,9 @@ import {
11
11
  restoreEndings,
12
12
  type LineEnding,
13
13
  } from "./replace-diff";
14
- import { readNormFile } from "./file-reader";
14
+ import { readNormFile, safeSnapId } from "./file-reader";
15
15
  import { normReq } from "./replace-normalize";
16
- import { isRec, rejectUnknownFields, abortIf, normalizeFilePath } from "./utils";
16
+ import { isRec, rejectUnknownFields, abortIf, makePrepareArguments } from "./utils";
17
17
  import { resolveTarget, writeAtomic } from "./fs-write";
18
18
  import { applyEdit,
19
19
  lineHashes,
@@ -26,7 +26,6 @@ import { applyEdit,
26
26
  type NEdit,
27
27
  } from "./hashline";
28
28
  import { toCwd } from "./paths";
29
- import { fileSnap } from "./file-reader";
30
29
  import {
31
30
  buildChanged,
32
31
  buildNoop,
@@ -47,7 +46,7 @@ import {
47
46
  import { loadP, loadGuide } from "./prompts";
48
47
  import { saveUndo } from "./replace-undo";
49
48
  import { loadHashStore, findSnapshotPaths, type HashStore } from "./hash-store";
50
- import { getServed, recordServed, recordServedDiff } from "./served";
49
+ import { getServed, recordServedSafe, recordServedDiffSafe } from "./served";
51
50
 
52
51
  const replacementTextSchema = Type.String({
53
52
  description:
@@ -249,17 +248,9 @@ export async function execPipeline(
249
248
  } catch (error) {
250
249
  if (options?.noPersist !== true) {
251
250
  if (error instanceof RangeStaleError) {
252
- try {
253
- recordServed(hashStore, absolutePath, error.rangeHashes);
254
- } catch (recordError) {
255
- console.error("Failed to record served state from range-stale feedback:", recordError);
256
- }
251
+ await recordServedSafe(absolutePath, error.rangeHashes, "range-stale feedback");
257
252
  } else if (error instanceof AnchorMismatchError) {
258
- try {
259
- recordServed(hashStore, absolutePath, error.feedbackHashes);
260
- } catch (recordError) {
261
- console.error("Failed to record served state from anchor-mismatch feedback:", recordError);
262
- }
253
+ await recordServedSafe(absolutePath, error.feedbackHashes, "anchor-mismatch feedback");
263
254
  }
264
255
  }
265
256
  throw error;
@@ -361,12 +352,7 @@ export function buildToolDef(): ToolDef {
361
352
  parameters,
362
353
  promptSnippet: E_SNIPPET,
363
354
  promptGuidelines: E_GUIDE,
364
- prepareArguments: (args: unknown) => {
365
- if (!isRec(args)) return args as any;
366
- const record = { ...args };
367
- normalizeFilePath(record);
368
- return record;
369
- },
355
+ prepareArguments: makePrepareArguments(),
370
356
  renderShell: "default",
371
357
  renderCall(args, theme, context) {
372
358
  const previewInput = getPreviewInput(args);
@@ -512,12 +498,7 @@ export function buildToolDef(): ToolDef {
512
498
 
513
499
  const editsAttempted = 1;
514
500
  if (originalNormalized === result) {
515
- let noopSnapshotId: string | undefined;
516
- try {
517
- noopSnapshotId = (await fileSnap(absolutePath)).snapshotId;
518
- } catch (error) {
519
- console.error("Failed to compute snapshot for noop edit:", error);
520
- }
501
+ const noopSnapshotId = await safeSnapId(absolutePath, "noop edit");
521
502
  return buildNoop({
522
503
  path,
523
504
  noopEdit,
@@ -561,12 +542,7 @@ export function buildToolDef(): ToolDef {
561
542
  await undo.restore();
562
543
  throw error;
563
544
  }
564
- let updatedSnapshotId: string | undefined;
565
- try {
566
- updatedSnapshotId = (await fileSnap(absolutePath)).snapshotId;
567
- } catch (error) {
568
- console.error("Failed to compute post-edit snapshot:", error);
569
- }
545
+ const updatedSnapshotId = await safeSnapId(absolutePath, "post-edit");
570
546
 
571
547
  const editMeta: RMeta = {
572
548
  editsAttempted,
@@ -589,12 +565,7 @@ export function buildToolDef(): ToolDef {
589
565
  };
590
566
  const changed = buildChanged(successInput);
591
567
  if (changed.details.diff) {
592
- try {
593
- const store = await loadHashStore();
594
- recordServedDiff(store, mutationTargetPath, changed.details.diff);
595
- } catch (error) {
596
- console.error("Failed to record served state from post-edit diff:", error);
597
- }
568
+ await recordServedDiffSafe(mutationTargetPath, changed.details.diff, "post-edit diff");
598
569
  }
599
570
  return changed;
600
571
  });
package/src/served.ts CHANGED
@@ -1,5 +1,4 @@
1
- import type { HashStore } from "./hash-store";
2
- import { parseHashList } from "./hash-store";
1
+ import { loadHashStore, parseHashList, type HashStore } from "./hash-store";
3
2
  import { HASH_CLASS } from "./hashline/alphabet";
4
3
 
5
4
  const SERVED_DIFF_ROW_RE = new RegExp(`^[+ ](${HASH_CLASS})│`);
@@ -42,3 +41,26 @@ export function recordServedDiff(store: HashStore, path: string, diff: string):
42
41
  export function clearServed(store: HashStore, path: string): void {
43
42
  store.stmts.servedDelete(path);
44
43
  }
44
+
45
+ export async function recordServedSafe(
46
+ path: string,
47
+ hashes: string[],
48
+ context: string,
49
+ ): Promise<void> {
50
+ if (hashes.length === 0) return;
51
+ try {
52
+ const store = await loadHashStore();
53
+ recordServed(store, path, hashes);
54
+ } catch (error) {
55
+ console.error(`Failed to record served state (${context}):`, error);
56
+ }
57
+ }
58
+
59
+ export async function recordServedDiffSafe(
60
+ path: string,
61
+ diff: string,
62
+ context: string,
63
+ ): Promise<void> {
64
+ if (!diff) return;
65
+ await recordServedSafe(path, servedHashesFromDiff(diff), context);
66
+ }
package/src/utils.ts CHANGED
@@ -9,6 +9,15 @@ export function normalizeFilePath(record: Record<string, unknown>): void {
9
9
  }
10
10
  }
11
11
 
12
+ export function makePrepareArguments(): (args: unknown) => any {
13
+ return (args) => {
14
+ if (!isRec(args)) return args;
15
+ const record = { ...args };
16
+ normalizeFilePath(record);
17
+ return record;
18
+ };
19
+ }
20
+
12
21
  export function splitLines(text: string): string[] {
13
22
  if (text.length === 0) return [""];
14
23
  const lines = text.split("\n");