pi-hashline-edit-pro 4.3.2 → 4.3.4

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/src/hash-store.ts CHANGED
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { chmod, readFile, rename, mkdir, stat } from "node:fs/promises";
4
4
  import { hashStorePath, hashStoreDir, legacyHashStorePath } from "./paths";
5
5
  import { errCode, isRec, splitLines } from "./utils";
6
- import { initHasher, contentChecksum } from "./hashline/hasher";
6
+ import { initHasher, contentChecksum, lineChecksum } from "./hashline/hasher";
7
7
  import { HASH_STORE_VERSION, HASH_STORE_BUSY_TIMEOUT } from "./constants";
8
8
  import {
9
9
  isValidHashList,
@@ -22,6 +22,7 @@ import { snapshotCache, cacheSnapshot, SNAPSHOT_CACHE_LIMIT } from "./hash-store
22
22
  export { isValidHashList, parseHashList, parseStoredHashes, isCorruptionError };
23
23
  export { SNAPSHOT_CACHE_LIMIT };
24
24
  export const STORE_NOT_OPEN_MESSAGE = "Hash store is not open; transactional update aborted";
25
+ export const STORE_SHUT_DOWN_MESSAGE = "Hash store was shut down while it was opening; call loadHashStore again.";
25
26
 
26
27
  type SqlParams = (string | number | null)[];
27
28
 
@@ -143,15 +144,16 @@ export interface UndoRecord {
143
144
  let cachedDb: { path: string; db: RawDb; stmts: Prepared } | null = null;
144
145
  let opening: { path: string; promise: Promise<HashStore> } | null = null;
145
146
  let exitHandlerRegistered = false;
147
+ let storeEpoch = 0;
148
+ const liveDbs = new Set<RawDb>();
146
149
 
147
150
  function openDb(storePath: string): { db: RawDb; stmts: Prepared } {
148
151
  const db = openDbFn(storePath);
152
+ liveDbs.add(db);
149
153
  try {
150
154
  return buildStore(db);
151
155
  } catch (error) {
152
- try {
153
- db.close();
154
- } catch {}
156
+ shutdownDb(db);
155
157
  throw error;
156
158
  }
157
159
  }
@@ -259,11 +261,15 @@ async function quarantineStore(storePath: string): Promise<void> {
259
261
  }
260
262
 
261
263
  function shutdownDb(db: RawDb): void {
264
+ if (!liveDbs.delete(db)) return;
262
265
  try {
263
266
  db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
264
267
  } catch {
265
268
  }
266
- db.close();
269
+ try {
270
+ db.close();
271
+ } catch {
272
+ }
267
273
  }
268
274
 
269
275
  async function openStore(storePath: string): Promise<HashStore> {
@@ -271,6 +277,7 @@ async function openStore(storePath: string): Promise<HashStore> {
271
277
  return { stmts: cachedDb.stmts, engine: sqliteEngine };
272
278
  }
273
279
  if (cachedDb) shutdownHashStore();
280
+ const epoch = storeEpoch;
274
281
  await initHasher();
275
282
  await mkdir(hashStoreDir(), { recursive: true, mode: 0o700 });
276
283
  if (process.platform !== "win32") {
@@ -327,6 +334,10 @@ async function openStore(storePath: string): Promise<HashStore> {
327
334
  console.error("Hash store migration failed; continuing without legacy import:", error);
328
335
  }
329
336
  }
337
+ if (storeEpoch !== epoch) {
338
+ shutdownDb(db);
339
+ throw new Error(STORE_SHUT_DOWN_MESSAGE);
340
+ }
330
341
  cachedDb = { path: storePath, db, stmts };
331
342
 
332
343
  if (!exitHandlerRegistered) {
@@ -352,17 +363,20 @@ export function loadHashStore(): Promise<HashStore> {
352
363
  return opening.promise;
353
364
  }
354
365
  const promise = openStore(storePath).finally(() => {
355
- if (opening?.path === storePath) opening = null;
366
+ if (opening?.promise === promise) opening = null;
356
367
  });
357
368
  opening = { path: storePath, promise };
358
369
  return promise;
359
370
  }
360
371
 
361
372
  export function shutdownHashStore(): void {
373
+ storeEpoch += 1;
362
374
  if (cachedDb) {
363
375
  shutdownDb(cachedDb.db);
364
376
  cachedDb = null;
365
377
  }
378
+ for (const db of [...liveDbs]) shutdownDb(db);
379
+ opening = null;
366
380
  snapshotCache.clear();
367
381
  }
368
382
 
@@ -426,7 +440,7 @@ async function migrateLegacy(db: RawDb): Promise<void> {
426
440
  contentChecksum(value.content),
427
441
  splitLines(value.content).length,
428
442
  JSON.stringify(value.hashes),
429
- "",
443
+ JSON.stringify(splitLines(value.content).map(lineChecksum)),
430
444
  Date.now(),
431
445
  ]);
432
446
  }
@@ -179,7 +179,7 @@ export function planEdit(
179
179
  ): PlannedEdit {
180
180
  const signal = options?.signal;
181
181
  abortIf(signal);
182
- const fileLines = options?.baseFileLines ?? buildIdx(content).fileLines;
182
+ const fileLines = options?.baseFileLines ?? splitLines(content);
183
183
  const lineIndex = { fileLines };
184
184
  const fileHashes = precomputedHashes;
185
185
  const warnings: string[] = [];
@@ -1,8 +1,8 @@
1
- import { splitLines, truncateToBytes, getCached } from "../utils";
2
- import { MAX_HASH_SOURCE_BYTES } from "../constants";
1
+ import { splitLines, getCached } from "../utils";
3
2
  import { loadHashStore, type HashStore } from "../hash-store";
4
3
  import { allocateFileAnchors } from "../anchor-registry";
5
- import { xxh32, initHasher, contentChecksum } from "./hasher";
4
+ import { xxh32, initHasher, canon, hashSource, lineChecksum } from "./hasher";
5
+ export { canon, hashSource, lineChecksum };
6
6
  import { HASH_LEN, ANCHOR_COUNT, anchorAt, HASH_CLASS, HASH_RUN } from "./alphabet";
7
7
  export { initHasher, HASH_LEN, HASH_CLASS, HASH_RUN };
8
8
 
@@ -48,18 +48,6 @@ export function stripRowPrefix(line: string): StrippedRow {
48
48
  return { text: line, kind: null, hash: undefined };
49
49
  }
50
50
 
51
- export function canon(line: string): string {
52
- return line.replace(/\r/g, "").trimEnd();
53
- }
54
-
55
- export function hashSource(line: string): string {
56
- return truncateToBytes(canon(line), MAX_HASH_SOURCE_BYTES);
57
- }
58
-
59
- export function lineChecksum(line: string): string {
60
- return contentChecksum(hashSource(line));
61
- }
62
-
63
51
  const BITSET_WORDS = Math.ceil(HASH_SPACE / 32);
64
52
 
65
53
  function getBit(bits: Uint32Array, idx: number): boolean {
@@ -1,4 +1,6 @@
1
1
  import xxhash from "xxhash-wasm";
2
+ import { truncateToBytes } from "../utils";
3
+ import { MAX_HASH_SOURCE_BYTES } from "../constants";
2
4
 
3
5
  export type Hasher = {
4
6
  h32(input: string, seed?: number): number;
@@ -30,4 +32,16 @@ export function xxh32(input: string, seed = 0): number {
30
32
 
31
33
  export function contentChecksum(content: string): string {
32
34
  return getH().h64ToString(content);
33
- }
35
+ }
36
+
37
+ export function canon(line: string): string {
38
+ return line.replace(/\r/g, "").trimEnd();
39
+ }
40
+
41
+ export function hashSource(line: string): string {
42
+ return truncateToBytes(canon(line), MAX_HASH_SOURCE_BYTES);
43
+ }
44
+
45
+ export function lineChecksum(line: string): string {
46
+ return contentChecksum(hashSource(line));
47
+ }
package/src/insert.ts CHANGED
@@ -17,7 +17,7 @@ export { assertInsertReq, type InsertReq };
17
17
 
18
18
  const insertAnchorSchema = Type.String({
19
19
  description:
20
- 'Bare 4-char anchor from a read row (the text before the `│` separator), never the row content. A pasted diff row or `anchor│` prefix is stripped with a warning. The anchor line is preserved; lines go after or before it.',
20
+ 'Bare 4-char anchor from a served anchor│content row (the text before the `│` separator), never the row content. A pasted diff row or `anchor│` prefix is stripped with a warning. The anchor line is preserved; lines go after or before it.',
21
21
  });
22
22
 
23
23
  const insertDirectionSchema = Type.Union(
package/src/paths.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { homedir } from "node:os";
2
- import { isAbsolute, resolve as resolvePath, join, dirname } from "node:path";
2
+ import { isAbsolute, relative, resolve as resolvePath, join, dirname } from "node:path";
3
3
 
4
4
 
5
5
  function homeBase(): string {
@@ -50,3 +50,7 @@ export function toCwd(filePath: string, cwd: string): string {
50
50
  const expanded = expand(filePath);
51
51
  return isAbsolute(expanded) ? expanded : resolvePath(cwd, expanded);
52
52
  }
53
+
54
+ export function toDisplayPath(cwd: string, absolutePath: string, fallback?: string): string {
55
+ return relative(cwd, absolutePath).replace(/\\/g, "/") || (fallback ?? absolutePath);
56
+ }
@@ -14,12 +14,12 @@ const replacementLinesSchema = Type.Array(
14
14
 
15
15
  const removeFromSchema = Type.String({
16
16
  description:
17
- "Bare 4-char anchor from a read row (the text before the `│` separator), never the row content. Marks the FIRST line to remove (inclusive)",
17
+ "Bare 4-char anchor from a served anchor│content row (the text before the `│` separator), never the row content. Marks the FIRST line to remove (inclusive)",
18
18
  });
19
19
 
20
20
  const removeToSchema = Type.String({
21
21
  description:
22
- "Bare 4-char anchor from a read row (the text before the `│` separator), never the row content. Marks the LAST line to remove (inclusive)",
22
+ "Bare 4-char anchor from a served anchor│content row (the text before the `│` separator), never the row content. Marks the LAST line to remove (inclusive)",
23
23
  });
24
24
  const pathRequiredSchema = Type.String({
25
25
  description:
package/src/read.ts CHANGED
@@ -17,8 +17,11 @@ import { abortIf, makePrepareArguments, numberedRead, visLines, splitLines } fro
17
17
  import { loadP, loadGuide } from "./prompts";
18
18
  import { withReadPrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
19
19
  import { valAccess } from "./validation";
20
- import { withAnchorSession } from "./anchor-registry";
20
+ import { readConfig } from "./config";
21
+ import { resolveTarget } from "./fs-write";
22
+ import { withAnchorSession, servedForPath } from "./anchor-registry";
21
23
  import { serveRows } from "./served";
24
+ import { getAutoReadAllSnapshot } from "./auto-read-all-state";
22
25
  import { Text } from "@earendil-works/pi-tui";
23
26
  const R_DESC = loadP("../prompts/read.md");
24
27
  const R_SNIPPET = loadP("../prompts/read-snippet.md");
@@ -210,6 +213,22 @@ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
210
213
 
211
214
  abortIf(signal);
212
215
  await valAccess(absolutePath, rawPath);
216
+ const autoReadAllMode = (await readConfig()).autoReadAll ?? "off";
217
+ if (autoReadAllMode !== "off") {
218
+ const canonical = await resolveTarget(absolutePath).catch(() => undefined);
219
+ if (canonical !== undefined) {
220
+ const stored = getAutoReadAllSnapshot(canonical);
221
+ if (stored !== undefined) {
222
+ const current = await safeSnapId(canonical, "auto-read-all guard");
223
+ if (current !== undefined && current === stored) {
224
+ const served = servedForPath(canonical);
225
+ if (served !== undefined && served.size > 0) {
226
+ throw new Error(`[E_AUTO_READ_ALL] ${rawPath} is unchanged since this session's start-of-session auto-read, so the attached content is still exact. Read succeeds on files that have changed since the full auto read.`);
227
+ }
228
+ }
229
+ }
230
+ }
231
+ }
213
232
 
214
233
  abortIf(signal);
215
234
  const file = await loadFileKindAndText(absolutePath, { maxLines: MAX_HASH_LINES, displayPath: rawPath });
@@ -13,6 +13,7 @@ import { genDiff, genPatch, spansFromHashes } from "./replace-diff";
13
13
  import { getDiffContextLines } from "./config";
14
14
  import { cntDiff, errCode, makePrepareArguments, splitLines } from "./utils";
15
15
  import { loadP, loadGuide } from "./prompts";
16
+ import { withUndoPrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
16
17
  import { buildMetrics } from "./replace-response";
17
18
  import { renderEditResult, fmtCall } from "./replace-render";
18
19
  import { Text } from "@earendil-works/pi-tui";
@@ -102,13 +103,18 @@ function fallbackFileMode(): number {
102
103
  return 0o666 & ~process.umask();
103
104
  }
104
105
 
105
- export function regUndo(pi: ExtensionAPI): void {
106
+ export function regUndo(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FLAGS): void {
107
+ const prompted = withUndoPrompts({
108
+ description: loadP("../prompts/undo-last-change.md"),
109
+ snippet: loadP("../prompts/undo-last-change-snippet.md"),
110
+ guidelines: loadGuide("../prompts/undo-last-change-guidelines.md"),
111
+ }, flags);
106
112
  pi.registerTool({
107
113
  name: "undo_last_change",
108
114
  label: "Undo Last Change",
109
- description: loadP("../prompts/undo-last-change.md"),
110
- promptSnippet: loadP("../prompts/undo-last-change-snippet.md"),
111
- promptGuidelines: loadGuide("../prompts/undo-last-change-guidelines.md"),
115
+ description: prompted.description,
116
+ promptSnippet: prompted.snippet,
117
+ promptGuidelines: prompted.guidelines,
112
118
  prepareArguments: makePrepareArguments(),
113
119
  parameters: Type.Object({
114
120
  path: Type.String({
package/src/replace.ts CHANGED
@@ -3,7 +3,6 @@ import type {
3
3
  ToolDefinition,
4
4
  } from "@earendil-works/pi-coding-agent";
5
5
  import { constants } from "node:fs";
6
- import { relative } from "node:path";
7
6
  import {
8
7
  genDiff,
9
8
  type DiffSpan,
@@ -31,7 +30,7 @@ import {
31
30
  import { loadHashStore, type HashStore } from "./hash-store";
32
31
  import { adoptAnchors, servedForPath, withAnchorSession } from "./anchor-registry";
33
32
  import { resolveTarget } from "./fs-write";
34
- import { toCwd } from "./paths";
33
+ import { toCwd, toDisplayPath } from "./paths";
35
34
  import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper, resolveEditTargetWithRequirement, throwIfStrictInput, getBoundaryDedupMode, withReplacePrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
36
35
  import { commitEdit, dedupRowsFromFixes } from "./commit";
37
36
  import { batchMemberFor, executeBatchMember, noteBatchFailure } from "./batch";
@@ -148,7 +147,7 @@ export async function execPipeline(
148
147
  const { normalized: originalNormalized, bom, originalEnding, fileHashes: originalHashes, hadUtf8DecodeErrors, absolutePath, identity } = await readNormFile(
149
148
  targetPath, cwd, { signal: options?.signal, accessMode: options?.accessMode, maxLines: MAX_HASH_LINES, store: hashStore, noPersist: options?.noPersist, allocation: options?.noPersist ? "shadow" : "real", preloadedNorm: options?.preloadedNorm },
150
149
  );
151
- const displayPath = relative(cwd, absolutePath).replace(/\\/g, "/") || targetPath;
150
+ const displayPath = toDisplayPath(cwd, absolutePath, targetPath);
152
151
 
153
152
  const dedupMode = await getBoundaryDedupMode();
154
153
  const effectiveSkipBoundaryDedup = options?.skipBoundaryDedup === true || dedupMode === "off";
package/src/utils.ts CHANGED
@@ -20,6 +20,14 @@ export function normalizeAnchors(record: Record<string, unknown>): void {
20
20
  record.remove_to = record.replace_to;
21
21
  delete record.replace_to;
22
22
  }
23
+ if (typeof record.remove_from !== "string" && typeof record.from === "string") {
24
+ record.remove_from = record.from;
25
+ delete record.from;
26
+ }
27
+ if (typeof record.remove_to !== "string" && typeof record.to === "string") {
28
+ record.remove_to = record.to;
29
+ delete record.to;
30
+ }
23
31
  }
24
32
 
25
33
  export function normalizeRequest(input: unknown): unknown {
@@ -137,7 +145,7 @@ export function isHashRow(line: string): boolean {
137
145
  return /^[A-Za-z0-9]{4}│/.test(line);
138
146
  }
139
147
 
140
- function gutterWidth(max: number, fallback: number): number {
148
+ export function gutterWidth(max: number, fallback: number): number {
141
149
  return String(max || fallback).length;
142
150
  }
143
151