pi-hashline-edit-pro 2.8.0 → 2.8.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 +9 -4
- package/index.ts +14 -8
- package/package.json +9 -9
- package/prompts/grep-guidelines.md +3 -3
- package/prompts/grep-snippet.md +1 -1
- package/prompts/grep.md +1 -1
- package/src/commit.ts +8 -3
- package/src/constants.ts +1 -1
- package/src/edit-common.ts +45 -0
- package/src/file-kind.ts +6 -2
- package/src/file-reader.ts +9 -2
- package/src/fs-write.ts +42 -11
- package/src/grep.ts +284 -98
- package/src/hash-store/cache.ts +18 -0
- package/src/hash-store/retry.ts +48 -0
- package/src/hash-store/validation.ts +93 -0
- package/src/hash-store.ts +90 -147
- package/src/hashline/apply.ts +3 -3
- package/src/hashline/resolve.ts +52 -36
- package/src/insert.ts +7 -23
- package/src/normalize.ts +27 -0
- package/src/read.ts +5 -3
- package/src/replace-diff.ts +8 -30
- package/src/replace-undo.ts +22 -7
- package/src/replace.ts +14 -30
- package/src/served.ts +56 -27
- package/src/validation.ts +2 -2
- package/src/write-hook.ts +4 -4
package/src/replace-undo.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { constants } from "fs";
|
|
2
|
+
import { open } from "fs/promises";
|
|
2
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { Type } from "typebox";
|
|
5
6
|
import { loadHashStore, persistSnapshot, upsertUndo, getUndoEntry, deleteUndo, type UndoRecord } from "./hash-store";
|
|
6
|
-
import {
|
|
7
|
-
import { resolveInCwd, writeAtomic } from "./fs-write";
|
|
8
|
-
import { toLF, stripBOM,
|
|
9
|
-
import {
|
|
7
|
+
import { recordServed, buildServedMap, servedHashesFromDiff } from "./served";
|
|
8
|
+
import { resolveInCwd, writeAtomic, type FileIdentity } from "./fs-write";
|
|
9
|
+
import { toLF, stripBOM, restoreEndings, type LineEnding } from "./normalize";
|
|
10
|
+
import { genDiff, genPatch } from "./replace-diff";
|
|
11
|
+
import { cntDiff, errCode, makePrepareArguments, splitLines } from "./utils";
|
|
10
12
|
import { loadP, loadGuide } from "./prompts";
|
|
11
13
|
import { buildMetrics } from "./replace-response";
|
|
12
14
|
import { renderEditResult } from "./replace-render";
|
|
@@ -121,8 +123,17 @@ export function regUndo(pi: ExtensionAPI): void {
|
|
|
121
123
|
|
|
122
124
|
return withFileMutationQueue(mutationTargetPath, async () => {
|
|
123
125
|
let currentRaw: string | undefined;
|
|
126
|
+
let currentIdentity: FileIdentity | undefined;
|
|
124
127
|
try {
|
|
125
|
-
|
|
128
|
+
const noFollow = process.platform === "win32" ? 0 : constants.O_NOFOLLOW;
|
|
129
|
+
const handle = await open(mutationTargetPath, constants.O_RDONLY | noFollow);
|
|
130
|
+
try {
|
|
131
|
+
const { dev, ino } = await handle.stat();
|
|
132
|
+
currentIdentity = { dev, ino };
|
|
133
|
+
currentRaw = await handle.readFile("utf-8");
|
|
134
|
+
} finally {
|
|
135
|
+
await handle.close();
|
|
136
|
+
}
|
|
126
137
|
} catch (error) {
|
|
127
138
|
if (errCode(error) !== "ENOENT") throw error;
|
|
128
139
|
}
|
|
@@ -146,6 +157,7 @@ export function regUndo(pi: ExtensionAPI): void {
|
|
|
146
157
|
await writeAtomic(
|
|
147
158
|
mutationTargetPath,
|
|
148
159
|
undo.bom + restoreEndings(undo.content, undo.originalEnding),
|
|
160
|
+
currentIdentity,
|
|
149
161
|
);
|
|
150
162
|
|
|
151
163
|
const currentNormalized = currentRaw === undefined ? "" : toLF(stripBOM(currentRaw).text);
|
|
@@ -160,7 +172,10 @@ export function regUndo(pi: ExtensionAPI): void {
|
|
|
160
172
|
try {
|
|
161
173
|
const store = await loadHashStore();
|
|
162
174
|
persistSnapshot(store, mutationTargetPath, undo.content, undo.hashes);
|
|
163
|
-
|
|
175
|
+
const diffHashes = servedHashesFromDiff(undoDiff);
|
|
176
|
+
const undoLines = splitLines(undo.content);
|
|
177
|
+
const servedMap = buildServedMap(undo.hashes, undoLines, diffHashes);
|
|
178
|
+
recordServed(store, mutationTargetPath, servedMap, new Set(undo.hashes));
|
|
164
179
|
} catch (error) {
|
|
165
180
|
console.error("Failed to restore hash store snapshot after undo:", error);
|
|
166
181
|
}
|
package/src/replace.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type {
|
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ToolDefinition,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
6
5
|
import { constants } from "fs";
|
|
7
6
|
import {
|
|
8
7
|
genDiff,
|
|
@@ -10,9 +9,9 @@ import {
|
|
|
10
9
|
} from "./replace-diff";
|
|
11
10
|
import { readNormFile, type NormFile } from "./file-reader";
|
|
12
11
|
import { editToolSchema, type ReqParams, assertReq, normReq } from "./payload-contract";
|
|
13
|
-
import { isRec
|
|
12
|
+
import { isRec } from "./utils";
|
|
14
13
|
import { loadP, loadGuide } from "./prompts";
|
|
15
|
-
import {
|
|
14
|
+
import { type FileIdentity } from "./fs-write";
|
|
16
15
|
import { applyEdit,
|
|
17
16
|
lineHashes,
|
|
18
17
|
resEdit,
|
|
@@ -26,14 +25,13 @@ import { applyEdit,
|
|
|
26
25
|
import { commitEdit } from "./commit";
|
|
27
26
|
import type { RMetrics } from "./replace-response";
|
|
28
27
|
import {
|
|
29
|
-
makeRenderCall,
|
|
30
|
-
renderEditResult,
|
|
31
28
|
type RPreview,
|
|
32
29
|
type RRState,
|
|
33
30
|
} from "./replace-render";
|
|
34
31
|
import { loadHashStore, findSnapshotPaths, findServedPaths, type HashStore } from "./hash-store";
|
|
35
32
|
import { getServed, recordServedSafe } from "./served";
|
|
36
33
|
import { noopPayloadKey, markBoundaryNoop, consumeBoundaryBypass, clearBoundaryBypass } from "./boundary-bypass";
|
|
34
|
+
import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper } from "./edit-common";
|
|
37
35
|
|
|
38
36
|
export { editToolSchema, type ReqParams, assertReq };
|
|
39
37
|
|
|
@@ -65,6 +63,7 @@ export interface PipelineResult {
|
|
|
65
63
|
totalRemovedLines: number;
|
|
66
64
|
hadBoundaryDedup: boolean;
|
|
67
65
|
boundaryRemovedLines: number;
|
|
66
|
+
identity: FileIdentity;
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
async function resolveMissingPath(
|
|
@@ -120,8 +119,8 @@ function hashSpan(hashes: string[], from: string, to: string): [number, number]
|
|
|
120
119
|
}
|
|
121
120
|
async function noteAnchorError(absolutePath: string, error: unknown, scopeHashes: string[], noPersist?: boolean): Promise<void> {
|
|
122
121
|
if (noPersist === true) return;
|
|
123
|
-
if (error instanceof RangeStaleError) await recordServedSafe(absolutePath, error.
|
|
124
|
-
else if (error instanceof AnchorMismatchError) await recordServedSafe(absolutePath, error.
|
|
122
|
+
if (error instanceof RangeStaleError) await recordServedSafe(absolutePath, error.rangeServedMap, "range-stale feedback", new Set(scopeHashes));
|
|
123
|
+
else if (error instanceof AnchorMismatchError) await recordServedSafe(absolutePath, error.feedbackMap, "anchor-mismatch feedback", new Set(scopeHashes));
|
|
125
124
|
}
|
|
126
125
|
|
|
127
126
|
function collectRemovedHashes(
|
|
@@ -170,7 +169,7 @@ export async function execPipeline(
|
|
|
170
169
|
);
|
|
171
170
|
|
|
172
171
|
const hashStore = options?.store ?? await loadHashStore();
|
|
173
|
-
const { normalized: originalNormalized, bom, originalEnding, fileHashes: originalHashes, hadUtf8DecodeErrors, absolutePath } = await readNormFile(
|
|
172
|
+
const { normalized: originalNormalized, bom, originalEnding, fileHashes: originalHashes, hadUtf8DecodeErrors, absolutePath, identity } = await readNormFile(
|
|
174
173
|
path, cwd, { signal: options?.signal, accessMode: options?.accessMode, maxLines: MAX_HASH_LINES, store: hashStore, noPersist: options?.noPersist, preloadedNorm: options?.preloadedNorm },
|
|
175
174
|
);
|
|
176
175
|
|
|
@@ -227,6 +226,7 @@ export async function execPipeline(
|
|
|
227
226
|
totalRemovedLines,
|
|
228
227
|
hadBoundaryDedup: (anchorResult.autoFixes?.length ?? 0) > 0,
|
|
229
228
|
boundaryRemovedLines: anchorResult.autoFixes?.length ?? 0,
|
|
229
|
+
identity,
|
|
230
230
|
};
|
|
231
231
|
}
|
|
232
232
|
|
|
@@ -277,22 +277,9 @@ export function buildToolDef(): ToolDef {
|
|
|
277
277
|
parameters,
|
|
278
278
|
promptSnippet: E_SNIPPET,
|
|
279
279
|
promptGuidelines: E_GUIDE,
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
renderCall: makeRenderCall(compPreview),
|
|
284
|
-
renderResult(result, { isPartial, expanded }, theme, context) {
|
|
285
|
-
return renderEditResult(
|
|
286
|
-
result as {
|
|
287
|
-
content?: Array<{ type: string; text?: string }>;
|
|
288
|
-
details?: ReplaceDetails;
|
|
289
|
-
},
|
|
290
|
-
{ isPartial, expanded },
|
|
291
|
-
theme,
|
|
292
|
-
context,
|
|
293
|
-
);
|
|
294
|
-
},
|
|
295
|
-
|
|
280
|
+
...editToolBase,
|
|
281
|
+
renderCall: editRenderCallWrapper(compPreview),
|
|
282
|
+
renderResult: editRenderResultWrapper,
|
|
296
283
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
297
284
|
const canonical = normReq(params);
|
|
298
285
|
const resolution = isRec(canonical) ? await resolveMissingPath(canonical) : undefined;
|
|
@@ -300,14 +287,11 @@ export function buildToolDef(): ToolDef {
|
|
|
300
287
|
canonical.path = resolution.path;
|
|
301
288
|
}
|
|
302
289
|
assertReq(canonical);
|
|
303
|
-
|
|
304
290
|
const normalizedParams = canonical;
|
|
305
291
|
const path = normalizedParams.path;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
return withFileMutationQueue(mutationTargetPath, async () => {
|
|
310
|
-
abortIf(signal);
|
|
292
|
+
return queuedEdit(path, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
|
|
293
|
+
const noopPayload = noopPayloadKey(mutationTargetPath, normalizedParams.remove_from, normalizedParams.remove_to, normalizedParams.replacement_lines);
|
|
294
|
+
const boundaryBypass = consumeBoundaryBypass(mutationTargetPath, noopPayload);
|
|
311
295
|
const pipe = await execPipeline(
|
|
312
296
|
normalizedParams,
|
|
313
297
|
ctx.cwd,
|
package/src/served.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { loadHashStore,
|
|
1
|
+
import { loadHashStore, parseStoredServed, STORE_NOT_OPEN_MESSAGE, withStore, type HashStore } from "./hash-store";
|
|
2
2
|
import { HASH_CLASS } from "./hashline/alphabet";
|
|
3
|
+
import { contentChecksum } from "./hashline/hasher";
|
|
3
4
|
|
|
4
5
|
const SERVED_DIFF_ROW_RE = new RegExp(`^[+ ](${HASH_CLASS})│`);
|
|
5
6
|
|
|
@@ -12,60 +13,88 @@ export function servedHashesFromDiff(diff: string): string[] {
|
|
|
12
13
|
return hashes;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export function
|
|
16
|
+
export function servedMapFromDiff(diff: string): Map<string, string> {
|
|
17
|
+
const map = new Map<string, string>();
|
|
18
|
+
for (const line of diff.split("\n")) {
|
|
19
|
+
const match = SERVED_DIFF_ROW_RE.exec(line);
|
|
20
|
+
if (!match) continue;
|
|
21
|
+
const hash = match[1]!;
|
|
22
|
+
const content = line.slice(match[0].length);
|
|
23
|
+
map.set(hash, contentChecksum(content));
|
|
24
|
+
}
|
|
25
|
+
return map;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildServedMap(fileHashes: string[], fileLines: string[], wantedHashes: string[]): Map<string, string> {
|
|
29
|
+
const index = new Map<string, number>();
|
|
30
|
+
for (let i = 0; i < fileHashes.length; i++) index.set(fileHashes[i]!, i);
|
|
31
|
+
const map = new Map<string, string>();
|
|
32
|
+
for (const h of wantedHashes) {
|
|
33
|
+
const idx = index.get(h);
|
|
34
|
+
if (idx !== undefined) map.set(h, contentChecksum(fileLines[idx]!));
|
|
35
|
+
}
|
|
36
|
+
return map;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getServed(store: HashStore, path: string): Map<string, string> | undefined {
|
|
16
40
|
const row = store.stmts.servedGet(path);
|
|
17
|
-
const parsed =
|
|
41
|
+
const parsed = parseStoredServed(row, () => store.stmts.servedDelete(path));
|
|
18
42
|
if (!parsed) return undefined;
|
|
19
|
-
return
|
|
43
|
+
return parsed;
|
|
20
44
|
}
|
|
21
45
|
|
|
22
46
|
function computeUpdate(
|
|
23
47
|
store: HashStore,
|
|
24
48
|
path: string,
|
|
25
|
-
|
|
49
|
+
entries: Map<string, string>,
|
|
26
50
|
scope?: ReadonlySet<string>,
|
|
27
|
-
):
|
|
51
|
+
): Map<string, string> | undefined {
|
|
28
52
|
const existing = getServed(store, path);
|
|
29
|
-
if (!existing &&
|
|
30
|
-
const
|
|
53
|
+
if (!existing && entries.size === 0 && !scope) return undefined;
|
|
54
|
+
const map = existing ? new Map(existing) : new Map<string, string>();
|
|
31
55
|
let changed = false;
|
|
32
56
|
if (scope) {
|
|
33
|
-
for (const hash of
|
|
57
|
+
for (const hash of [...map.keys()]) {
|
|
34
58
|
if (!scope.has(hash)) {
|
|
35
|
-
|
|
59
|
+
map.delete(hash);
|
|
36
60
|
changed = true;
|
|
37
61
|
}
|
|
38
62
|
}
|
|
39
63
|
}
|
|
40
|
-
for (const hash of
|
|
41
|
-
|
|
42
|
-
|
|
64
|
+
for (const [hash, content] of entries) {
|
|
65
|
+
const prev = map.get(hash);
|
|
66
|
+
if (prev !== content) {
|
|
67
|
+
map.set(hash, content);
|
|
43
68
|
changed = true;
|
|
44
69
|
}
|
|
45
70
|
}
|
|
46
|
-
if (!changed) return undefined;
|
|
47
|
-
return
|
|
71
|
+
if (!changed && existing) return undefined;
|
|
72
|
+
if (map.size === 0 && (!existing || existing.size === 0)) return undefined;
|
|
73
|
+
if (!changed) return existing;
|
|
74
|
+
return map;
|
|
48
75
|
}
|
|
49
76
|
|
|
50
77
|
export function recordServed(
|
|
51
78
|
store: HashStore,
|
|
52
79
|
path: string,
|
|
53
|
-
|
|
80
|
+
entries: Map<string, string>,
|
|
54
81
|
scope?: ReadonlySet<string>,
|
|
55
82
|
): void {
|
|
56
83
|
try {
|
|
57
84
|
withStore(() => {
|
|
58
|
-
const
|
|
59
|
-
if (!
|
|
60
|
-
|
|
85
|
+
const map = computeUpdate(store, path, entries, scope);
|
|
86
|
+
if (!map) return;
|
|
87
|
+
const obj = Object.fromEntries(map);
|
|
88
|
+
store.stmts.servedUpsert(path, JSON.stringify(obj), Date.now());
|
|
61
89
|
});
|
|
62
90
|
return;
|
|
63
91
|
} catch (error) {
|
|
64
92
|
if (!(error instanceof Error && error.message === STORE_NOT_OPEN_MESSAGE)) throw error;
|
|
65
93
|
}
|
|
66
|
-
const
|
|
67
|
-
if (!
|
|
68
|
-
|
|
94
|
+
const map = computeUpdate(store, path, entries, scope);
|
|
95
|
+
if (!map) return;
|
|
96
|
+
const obj = Object.fromEntries(map);
|
|
97
|
+
store.stmts.servedUpsert(path, JSON.stringify(obj), Date.now());
|
|
69
98
|
}
|
|
70
99
|
|
|
71
100
|
export function recordServedDiff(
|
|
@@ -74,7 +103,7 @@ export function recordServedDiff(
|
|
|
74
103
|
diff: string,
|
|
75
104
|
scope?: ReadonlySet<string>,
|
|
76
105
|
): void {
|
|
77
|
-
recordServed(store, path,
|
|
106
|
+
recordServed(store, path, servedMapFromDiff(diff), scope);
|
|
78
107
|
}
|
|
79
108
|
|
|
80
109
|
export function clearServed(store: HashStore, path: string): void {
|
|
@@ -83,14 +112,14 @@ export function clearServed(store: HashStore, path: string): void {
|
|
|
83
112
|
|
|
84
113
|
export async function recordServedSafe(
|
|
85
114
|
path: string,
|
|
86
|
-
|
|
115
|
+
entries: Map<string, string>,
|
|
87
116
|
context: string,
|
|
88
117
|
scope?: ReadonlySet<string>,
|
|
89
118
|
): Promise<void> {
|
|
90
|
-
if (
|
|
119
|
+
if (entries.size === 0 && !scope) return;
|
|
91
120
|
try {
|
|
92
121
|
const store = await loadHashStore();
|
|
93
|
-
recordServed(store, path,
|
|
122
|
+
recordServed(store, path, entries, scope);
|
|
94
123
|
} catch (error) {
|
|
95
124
|
console.error(`Failed to record served state (${context}):`, error);
|
|
96
125
|
}
|
|
@@ -103,5 +132,5 @@ export async function recordServedDiffSafe(
|
|
|
103
132
|
scope?: ReadonlySet<string>,
|
|
104
133
|
): Promise<void> {
|
|
105
134
|
if (!diff) return;
|
|
106
|
-
await recordServedSafe(path,
|
|
135
|
+
await recordServedSafe(path, servedMapFromDiff(diff), context, scope);
|
|
107
136
|
}
|
package/src/validation.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { constants } from "fs";
|
|
2
2
|
import { access as fsAccess } from "fs/promises";
|
|
3
3
|
import type { LFile } from "./file-kind";
|
|
4
|
+
import type { FileIdentity } from "./fs-write";
|
|
4
5
|
import { errCode } from "./utils";
|
|
5
6
|
|
|
6
7
|
export async function valAccess(
|
|
@@ -26,7 +27,7 @@ export async function valAccess(
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
export function valKind(file: LFile, path: string): asserts file is { kind: "text"; text: string; hadUtf8DecodeErrors?: true } {
|
|
30
|
+
export function valKind(file: LFile, path: string): asserts file is { kind: "text"; text: string; identity?: FileIdentity; hadUtf8DecodeErrors?: true } {
|
|
30
31
|
if (file.kind === "directory") {
|
|
31
32
|
throw new Error(`[E_NOT_TEXT] Path is a directory: ${path}. Use ls to inspect directories.`);
|
|
32
33
|
}
|
|
@@ -42,4 +43,3 @@ export function valKind(file: LFile, path: string): asserts file is { kind: "tex
|
|
|
42
43
|
);
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
|
-
|
package/src/write-hook.ts
CHANGED
|
@@ -8,19 +8,19 @@ import { abortIf, splitLines, isRec, normalizeFilePath } from "./utils";
|
|
|
8
8
|
|
|
9
9
|
const HASH_ECHO_RE = new RegExp(`^(${HASH_CLASS})${HASH_SEP}`);
|
|
10
10
|
|
|
11
|
-
function searchEcho(lines: string[], served: ReadonlySet<string>): { line: number; hash: string } | undefined {
|
|
11
|
+
function searchEcho(lines: string[], served: ReadonlyMap<string, string> | ReadonlySet<string>): { line: number; hash: string } | undefined {
|
|
12
12
|
for (let i = 0; i < lines.length; i++) {
|
|
13
13
|
const match = HASH_ECHO_RE.exec(lines[i]!);
|
|
14
|
-
if (match && served.has(match[1]!)) return { line: i + 1, hash: match[1]! };
|
|
14
|
+
if (match && served.has(match[1]! as never)) return { line: i + 1, hash: match[1]! };
|
|
15
15
|
}
|
|
16
16
|
return undefined;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function findServedHashEcho(content: string, served: ReadonlySet<string>): { line: number; hash: string } | undefined {
|
|
19
|
+
export function findServedHashEcho(content: string, served: ReadonlyMap<string, string> | ReadonlySet<string>): { line: number; hash: string } | undefined {
|
|
20
20
|
return searchEcho(splitLines(content), served);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export function findEditHashEcho(lines: string[], served: ReadonlySet<string>): { line: number; hash: string } | undefined {
|
|
23
|
+
export function findEditHashEcho(lines: string[], served: ReadonlyMap<string, string> | ReadonlySet<string>): { line: number; hash: string } | undefined {
|
|
24
24
|
return searchEcho(lines, served);
|
|
25
25
|
}
|
|
26
26
|
|