@d3ara1n/pi-hashline-edit 0.1.0 → 0.1.1
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 +12 -15
- package/package.json +1 -1
- package/src/core/apply.test.ts +14 -14
- package/src/core/apply.ts +21 -18
- package/src/core/diff.test.ts +4 -4
- package/src/core/diff.ts +11 -9
- package/src/core/hash.test.ts +8 -8
- package/src/core/hash.ts +25 -20
- package/src/core/index.ts +3 -3
- package/src/core/parse.test.ts +19 -19
- package/src/core/parse.ts +10 -9
- package/src/core/snapshot.test.ts +8 -8
- package/src/core/snapshot.ts +25 -19
- package/src/core/types.ts +19 -18
- package/src/index.ts +6 -5
- package/src/pi/config.ts +8 -7
- package/src/pi/edit-tool.ts +72 -27
- package/src/pi/execute.test.ts +15 -14
- package/src/pi/pi.test.ts +4 -4
- package/src/pi/read-tool.ts +10 -10
- package/src/pi/state.ts +13 -12
package/src/pi/pi.test.ts
CHANGED
|
@@ -3,25 +3,25 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import { canonicalPath } from "./read-tool.ts";
|
|
4
4
|
import { missingInputError } from "./edit-tool.ts";
|
|
5
5
|
|
|
6
|
-
test("canonicalPath
|
|
6
|
+
test("canonicalPath resolves relative/absolute", () => {
|
|
7
7
|
assert.equal(canonicalPath("/cwd", "foo.ts"), "/cwd/foo.ts");
|
|
8
8
|
assert.equal(canonicalPath("/cwd", "./foo.ts"), "/cwd/foo.ts");
|
|
9
9
|
assert.equal(canonicalPath("/cwd", "/abs/x.ts"), "/abs/x.ts");
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
test("missingInputError: edits
|
|
12
|
+
test("missingInputError: edits array → explicit no-degradation message", () => {
|
|
13
13
|
const msg = missingInputError("f.ts", { edits: [{ oldText: "a", newText: "b" }] });
|
|
14
14
|
assert.ok(msg.includes("legacy"), msg);
|
|
15
15
|
assert.ok(msg.includes("ONLY"), msg);
|
|
16
16
|
assert.ok(msg.includes("f.ts"), msg);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
test("missingInputError:
|
|
19
|
+
test("missingInputError: top-level oldText/newText also recognized as legacy", () => {
|
|
20
20
|
const msg = missingInputError("f.ts", { oldText: "a", newText: "b" });
|
|
21
21
|
assert.ok(msg.includes("legacy"));
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
test("missingInputError:
|
|
24
|
+
test("missingInputError: only input missing (not legacy)", () => {
|
|
25
25
|
const msg = missingInputError("f.ts", {});
|
|
26
26
|
assert.ok(msg.includes("missing"));
|
|
27
27
|
assert.ok(!msg.includes("legacy"));
|
package/src/pi/read-tool.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Override read
|
|
3
|
-
*
|
|
2
|
+
* Override read: text files output "lineNo#hash│content" and record a snapshot;
|
|
3
|
+
* non-text (images/binary) and read errors delegate to the built-in read, whose
|
|
4
|
+
* renderer is inherited automatically.
|
|
4
5
|
*
|
|
5
6
|
* @module pi-hashline-edit/pi
|
|
6
7
|
*/
|
|
@@ -14,12 +15,12 @@ import { getState, getSnapshot, recordSnapshot } from "./state.ts";
|
|
|
14
15
|
const MAX_LINES = 2000;
|
|
15
16
|
const MAX_BYTES = 256 * 1024;
|
|
16
17
|
|
|
17
|
-
/** canonical path
|
|
18
|
+
/** canonical path: shared by read/edit to keep the snapshot key consistent. */
|
|
18
19
|
export function canonicalPath(cwd: string, p: string): string {
|
|
19
20
|
return resolve(cwd, p);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
/**
|
|
23
|
+
/** Build the read override (a ToolDefinition fragment for registerTool). */
|
|
23
24
|
export function makeReadOverride(cwd: string) {
|
|
24
25
|
const builtin = createReadTool(cwd);
|
|
25
26
|
|
|
@@ -36,8 +37,7 @@ export function makeReadOverride(cwd: string) {
|
|
|
36
37
|
parameters: builtin.parameters,
|
|
37
38
|
|
|
38
39
|
async execute(toolCallId: string, params: any, signal: AbortSignal | undefined, onUpdate: any) {
|
|
39
|
-
//
|
|
40
|
-
// 未启用 或 用户已取消 → 透传内置(builtin 自行处理 abort)
|
|
40
|
+
// Not enabled OR user cancelled → delegate to the built-in (builtin handles abort itself)
|
|
41
41
|
if (!getState().config.enabled || signal?.aborted) return builtin.execute(toolCallId, params, signal, onUpdate);
|
|
42
42
|
|
|
43
43
|
const absPath = canonicalPath(cwd, params.path as string);
|
|
@@ -45,18 +45,18 @@ export function makeReadOverride(cwd: string) {
|
|
|
45
45
|
try {
|
|
46
46
|
buf = await readFile(absPath);
|
|
47
47
|
} catch {
|
|
48
|
-
//
|
|
48
|
+
// read error → delegate to the built-in (it has polished error messages)
|
|
49
49
|
return builtin.execute(toolCallId, params, signal, onUpdate);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
//
|
|
52
|
+
// binary/image detection (null byte) → delegate to the built-in (it uses file-type for images)
|
|
53
53
|
if (buf.includes(0)) return builtin.execute(toolCallId, params, signal, onUpdate);
|
|
54
54
|
|
|
55
55
|
const text = buf.toString("utf-8");
|
|
56
56
|
const allLines = splitLines(text);
|
|
57
57
|
const totalLines = allLines.length;
|
|
58
58
|
|
|
59
|
-
//
|
|
59
|
+
// record a full-file snapshot (edit anchors are based on full-file line numbers)
|
|
60
60
|
const snap = recordSnapshot(absPath, text);
|
|
61
61
|
|
|
62
62
|
// offset/limit
|
|
@@ -92,7 +92,7 @@ export function makeReadOverride(cwd: string) {
|
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
/**
|
|
95
|
+
/** Reused by the edit override: look up a recorded snapshot for a path (by canonical path). */
|
|
96
96
|
export function lookupSnapshot(cwd: string, p: string) {
|
|
97
97
|
return getSnapshot(canonicalPath(cwd, p));
|
|
98
98
|
}
|
package/src/pi/state.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Session
|
|
2
|
+
* Session-level state: file snapshot store (LRU) + config.
|
|
3
3
|
*
|
|
4
|
-
* globalThis
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* globalThis singleton (avoids Bun module-identity issues; see the repo AGENTS).
|
|
5
|
+
* Snapshot LRU eviction (default 64 files) prevents unbounded memory growth in
|
|
6
|
+
* long sessions — cold files are pushed out by new ones. read records; edit
|
|
7
|
+
* verifies; key is the canonical absolute path.
|
|
8
|
+
* config lives in state and is reloaded on session_start.
|
|
8
9
|
*
|
|
9
10
|
* @module pi-hashline-edit/pi
|
|
10
11
|
*/
|
|
@@ -15,11 +16,11 @@ import type { HashlineEditConfig } from "./config.ts";
|
|
|
15
16
|
|
|
16
17
|
const GLOBAL_KEY = "__piHashlineEdit";
|
|
17
18
|
const DEFAULT_CONFIG: HashlineEditConfig = { enabled: true, hashLen: 4 };
|
|
18
|
-
/**
|
|
19
|
+
/** Maximum number of cached file snapshots; beyond this the least-recently-accessed is evicted. */
|
|
19
20
|
const MAX_SNAPSHOTS = 64;
|
|
20
21
|
|
|
21
22
|
export interface HashlineEditState {
|
|
22
|
-
/** canonical path →
|
|
23
|
+
/** canonical path → snapshot. LRU order: Map insertion order, most recently accessed at the end. */
|
|
23
24
|
readonly snapshots: Map<string, FileSnapshot>;
|
|
24
25
|
hashLen: number;
|
|
25
26
|
config: HashlineEditConfig;
|
|
@@ -38,7 +39,7 @@ export function getState(): HashlineEditState {
|
|
|
38
39
|
return state;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
/**
|
|
42
|
+
/** Write a snapshot and maintain LRU: move to the end (most recently used); evict the oldest (Map's first entry) when over the limit. */
|
|
42
43
|
function touchAndEvict(map: Map<string, FileSnapshot>, path: string, snap: FileSnapshot): void {
|
|
43
44
|
if (map.has(path)) map.delete(path);
|
|
44
45
|
map.set(path, snap);
|
|
@@ -49,7 +50,7 @@ function touchAndEvict(map: Map<string, FileSnapshot>, path: string, snap: FileS
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
/**
|
|
53
|
+
/** Record a file snapshot (called on read): compute line hashes + LRU bookkeeping. */
|
|
53
54
|
export function recordSnapshot(canonicalPath: string, text: string): FileSnapshot {
|
|
54
55
|
const state = getState();
|
|
55
56
|
const snap = createSnapshot(canonicalPath, text, state.hashLen);
|
|
@@ -57,12 +58,12 @@ export function recordSnapshot(canonicalPath: string, text: string): FileSnapsho
|
|
|
57
58
|
return snap;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
/**
|
|
61
|
+
/** Store an already-computed snapshot (updated after a successful edit), via LRU. */
|
|
61
62
|
export function putSnapshot(canonicalPath: string, snap: FileSnapshot): void {
|
|
62
63
|
touchAndEvict(getState().snapshots, canonicalPath, snap);
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
/**
|
|
66
|
+
/** Get a file snapshot (for edit verification); on a hit move it to the end (LRU touch). */
|
|
66
67
|
export function getSnapshot(canonicalPath: string): FileSnapshot | undefined {
|
|
67
68
|
const map = getState().snapshots;
|
|
68
69
|
const snap = map.get(canonicalPath);
|
|
@@ -73,7 +74,7 @@ export function getSnapshot(canonicalPath: string): FileSnapshot | undefined {
|
|
|
73
74
|
return snap;
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
/**
|
|
77
|
+
/** Clear all snapshots (on session restart). */
|
|
77
78
|
export function clearSnapshots(): void {
|
|
78
79
|
getState().snapshots.clear();
|
|
79
80
|
}
|