pi-hashline-edit-pro 2.5.1 → 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/README.md +4 -2
- package/index.ts +2 -7
- package/package.json +1 -1
- package/src/config.ts +7 -5
- package/src/file-kind.ts +4 -3
- package/src/file-reader.ts +12 -0
- package/src/hash-store.ts +59 -55
- package/src/hashline/index.ts +0 -5
- package/src/hashline/resolve.ts +0 -8
- package/src/read.ts +6 -23
- package/src/replace-response.ts +1 -1
- package/src/replace-undo.ts +2 -7
- package/src/replace.ts +9 -38
- package/src/served.ts +27 -13
- package/src/utils.ts +10 -3
- package/src/validation.ts +5 -0
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# pi-hashline-edit-pro
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/pi-hashline-edit-pro) [](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/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 {
|
|
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
|
-
|
|
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.
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
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: "
|
|
82
|
-
description: `
|
|
82
|
+
kind: "too_large",
|
|
83
|
+
description: `exceeds the ${MAX_BYTES / (1024 * 1024)}MB size limit`,
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
|
package/src/file-reader.ts
CHANGED
|
@@ -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
|
@@ -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>;
|
|
@@ -271,7 +286,11 @@ async function openStore(storePath: string): Promise<HashStore> {
|
|
|
271
286
|
const { db, stmts } = opened;
|
|
272
287
|
|
|
273
288
|
if (!existed) {
|
|
274
|
-
|
|
289
|
+
try {
|
|
290
|
+
await migrateLegacy(db);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error("Hash store migration failed; continuing without legacy import:", error);
|
|
293
|
+
}
|
|
275
294
|
}
|
|
276
295
|
cachedDb = { path: storePath, db, stmts };
|
|
277
296
|
|
|
@@ -313,20 +332,19 @@ export function shutdownHashStore(): void {
|
|
|
313
332
|
}
|
|
314
333
|
|
|
315
334
|
function withStore(fn: () => void): void {
|
|
316
|
-
if (cachedDb) {
|
|
317
|
-
|
|
318
|
-
cachedDb!.db.exec("BEGIN IMMEDIATE");
|
|
319
|
-
try {
|
|
320
|
-
fn();
|
|
321
|
-
cachedDb!.db.exec("COMMIT");
|
|
322
|
-
} catch (e) {
|
|
323
|
-
try { cachedDb!.db.exec("ROLLBACK"); } catch {}
|
|
324
|
-
throw e;
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
} else {
|
|
328
|
-
fn();
|
|
335
|
+
if (!cachedDb) {
|
|
336
|
+
throw new Error("Hash store is not open; transactional update aborted");
|
|
329
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
|
+
});
|
|
330
348
|
}
|
|
331
349
|
|
|
332
350
|
async function migrateLegacy(db: DatabaseSync): Promise<void> {
|
|
@@ -373,17 +391,19 @@ async function migrateLegacy(db: DatabaseSync): Promise<void> {
|
|
|
373
391
|
]);
|
|
374
392
|
}
|
|
375
393
|
if (rows.length > 0) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
+
});
|
|
387
407
|
}
|
|
388
408
|
|
|
389
409
|
try {
|
|
@@ -418,21 +438,13 @@ export function getSnapshot(
|
|
|
418
438
|
}
|
|
419
439
|
const row = store.stmts.get(path, checksum, lineCount);
|
|
420
440
|
if (!row) return undefined;
|
|
421
|
-
|
|
422
|
-
try {
|
|
423
|
-
parsed = JSON.parse(row.hashes as string);
|
|
424
|
-
} catch {
|
|
441
|
+
const parsed = parseHashList(row.hashes as string, () => {
|
|
425
442
|
if (deleteCorrupt) store.stmts.deleteOne(path);
|
|
426
443
|
snapshotCache.delete(path);
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
return parsed;
|
|
432
|
-
}
|
|
433
|
-
if (deleteCorrupt) store.stmts.deleteOne(path);
|
|
434
|
-
snapshotCache.delete(path);
|
|
435
|
-
return undefined;
|
|
444
|
+
});
|
|
445
|
+
if (!parsed) return undefined;
|
|
446
|
+
cacheSnapshot(path, checksum, lineCount, parsed);
|
|
447
|
+
return parsed;
|
|
436
448
|
}
|
|
437
449
|
|
|
438
450
|
export function upsertSnapshot(
|
|
@@ -461,23 +473,15 @@ export function upsertUndo(store: HashStore, path: string, entry: UndoRecord): v
|
|
|
461
473
|
export function getUndoEntry(store: HashStore, path: string): UndoRecord | undefined {
|
|
462
474
|
const row = store.stmts.undoGet(path);
|
|
463
475
|
if (!row) return undefined;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
}
|
|
476
|
+
const parsed = parseHashList(row.hashes as string, () => store.stmts.undoDelete(path));
|
|
477
|
+
if (!parsed) return undefined;
|
|
478
|
+
return {
|
|
479
|
+
content: row.content as string,
|
|
480
|
+
bom: row.bom as string,
|
|
481
|
+
ending: row.ending as string,
|
|
482
|
+
hashes: parsed,
|
|
483
|
+
resultContent: row.result_content as string,
|
|
484
|
+
};
|
|
481
485
|
}
|
|
482
486
|
|
|
483
487
|
export function deleteUndo(store: HashStore, path: string): void {
|
package/src/hashline/index.ts
CHANGED
|
@@ -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,
|
package/src/hashline/resolve.ts
CHANGED
|
@@ -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,
|
|
16
|
-
import {
|
|
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: (
|
|
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
|
-
|
|
227
|
-
|
|
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.]`
|
package/src/replace-response.ts
CHANGED
|
@@ -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",
|
package/src/replace-undo.ts
CHANGED
|
@@ -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,
|
|
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: (
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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: (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2
|
-
import { isValidHashList } 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})│`);
|
|
@@ -16,17 +15,9 @@ export function servedHashesFromDiff(diff: string): string[] {
|
|
|
16
15
|
export function getServed(store: HashStore, path: string): Set<string> | undefined {
|
|
17
16
|
const row = store.stmts.servedGet(path);
|
|
18
17
|
if (!row) return undefined;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
}
|
|
18
|
+
const parsed = parseHashList(row.hashes as string, () => store.stmts.servedDelete(path));
|
|
19
|
+
if (!parsed) return undefined;
|
|
20
|
+
return new Set(parsed);
|
|
30
21
|
}
|
|
31
22
|
|
|
32
23
|
export function recordServed(store: HashStore, path: string, hashes: string[]): void {
|
|
@@ -50,3 +41,26 @@ export function recordServedDiff(store: HashStore, path: string, diff: string):
|
|
|
50
41
|
export function clearServed(store: HashStore, path: string): void {
|
|
51
42
|
store.stmts.servedDelete(path);
|
|
52
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");
|
|
@@ -16,9 +25,7 @@ export function splitLines(text: string): string[] {
|
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
export function visLines(text: string): string[] {
|
|
19
|
-
|
|
20
|
-
const lines = text.split("\n");
|
|
21
|
-
return text.endsWith("\n") ? lines.slice(0, -1) : lines;
|
|
28
|
+
return text.length === 0 ? [] : splitLines(text);
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
|
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
|
|