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/README.md +8 -5
- package/index.ts +8 -3
- package/package.json +1 -1
- package/prompts/grep-guidelines.md +0 -1
- package/prompts/grep.md +1 -1
- package/prompts/insert-guidelines.md +1 -2
- package/prompts/insert.md +1 -1
- package/prompts/read-guidelines.md +2 -3
- package/prompts/replace-guidelines.md +4 -4
- package/prompts/replace.md +1 -1
- package/src/auto-read-all-state.ts +13 -0
- package/src/auto-read-all.ts +79 -27
- package/src/batch.ts +23 -32
- package/src/config-ui.ts +79 -7
- package/src/config.ts +35 -0
- package/src/edit-common.ts +25 -7
- package/src/file-reader.ts +2 -3
- package/src/glob.ts +186 -0
- package/src/grep.ts +11 -41
- package/src/hash-store.ts +21 -7
- package/src/hashline/apply.ts +1 -1
- package/src/hashline/hash.ts +3 -15
- package/src/hashline/hasher.ts +15 -1
- package/src/insert.ts +1 -1
- package/src/paths.ts +5 -1
- package/src/payload-contract.ts +2 -2
- package/src/read.ts +20 -1
- package/src/replace-undo.ts +10 -4
- package/src/replace.ts +2 -3
- package/src/utils.ts +9 -1
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
|
-
|
|
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
|
-
|
|
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?.
|
|
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
|
}
|
package/src/hashline/apply.ts
CHANGED
|
@@ -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 ??
|
|
182
|
+
const fileLines = options?.baseFileLines ?? splitLines(content);
|
|
183
183
|
const lineIndex = { fileLines };
|
|
184
184
|
const fileHashes = precomputedHashes;
|
|
185
185
|
const warnings: string[] = [];
|
package/src/hashline/hash.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { splitLines,
|
|
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,
|
|
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 {
|
package/src/hashline/hasher.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/src/payload-contract.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
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 });
|
package/src/replace-undo.ts
CHANGED
|
@@ -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:
|
|
110
|
-
promptSnippet:
|
|
111
|
-
promptGuidelines:
|
|
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 =
|
|
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
|
|