pi-hashline-edit-pro 0.17.11 → 0.17.13
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/index.ts +8 -7
- package/package.json +1 -1
- package/scripts/cleanup-better-sqlite3.cjs +15 -11
- package/src/file-kind.ts +12 -19
- package/src/file-reader.ts +16 -10
- package/src/read.ts +3 -3
- package/src/replace-normalize.ts +1 -1
- package/src/replace-response.ts +3 -3
- package/src/replace-undo.ts +9 -5
- package/src/replace.ts +16 -18
package/index.ts
CHANGED
|
@@ -13,12 +13,6 @@ import {
|
|
|
13
13
|
import { loadHashStore, pruneMissing } from "./src/hash-store";
|
|
14
14
|
import { readNormFile } from "./src/file-reader";
|
|
15
15
|
|
|
16
|
-
export default function (pi: ExtensionAPI): void {
|
|
17
|
-
regRead(pi);
|
|
18
|
-
|
|
19
|
-
regReplace(pi);
|
|
20
|
-
regReplaceUndo(pi);
|
|
21
|
-
|
|
22
16
|
function registerReplaceTool(pi: ExtensionAPI, mode: string, autoRead?: boolean): void {
|
|
23
17
|
if (mode === "flat") {
|
|
24
18
|
regReplaceFlat(pi, autoRead);
|
|
@@ -26,6 +20,13 @@ function registerReplaceTool(pi: ExtensionAPI, mode: string, autoRead?: boolean)
|
|
|
26
20
|
regReplace(pi, autoRead);
|
|
27
21
|
}
|
|
28
22
|
}
|
|
23
|
+
|
|
24
|
+
export default function (pi: ExtensionAPI): void {
|
|
25
|
+
regRead(pi);
|
|
26
|
+
|
|
27
|
+
regReplace(pi);
|
|
28
|
+
regReplaceUndo(pi);
|
|
29
|
+
|
|
29
30
|
const debugValue = process.env.PI_HASHLINE_DEBUG;
|
|
30
31
|
const autoReadValue = process.env.PI_HASHLINE_AUTO_READ;
|
|
31
32
|
let autoRead = autoReadValue === "1" || autoReadValue === "true";
|
|
@@ -80,7 +81,7 @@ function registerReplaceTool(pi: ExtensionAPI, mode: string, autoRead?: boolean)
|
|
|
80
81
|
if (typeof filePath !== "string") return;
|
|
81
82
|
|
|
82
83
|
try {
|
|
83
|
-
const { normalized, fileHashes, absolutePath } = await readNormFile(filePath, ctx.cwd
|
|
84
|
+
const { normalized, fileHashes, absolutePath } = await readNormFile(filePath, ctx.cwd);
|
|
84
85
|
|
|
85
86
|
if (visLines(normalized).length === 0) return;
|
|
86
87
|
|
package/package.json
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
if (!fs.existsSync(
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
function cleanNodeModules(nm) {
|
|
5
|
+
if (!fs.existsSync(nm)) return;
|
|
6
|
+
const entries = fs.readdirSync(nm, { withFileTypes: true });
|
|
7
|
+
for (const entry of entries) {
|
|
8
|
+
if (entry.name.startsWith(".better-sqlite3-")) {
|
|
9
|
+
const full = path.join(nm, entry.name);
|
|
10
|
+
try {
|
|
11
|
+
fs.rmSync(full, { recursive: true, force: true });
|
|
12
|
+
console.error("Cleaned up stale better-sqlite3 build artifact:", entry.name);
|
|
13
|
+
} catch {
|
|
14
|
+
}
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
const pkgDir = path.resolve(__dirname, "..");
|
|
20
|
+
cleanNodeModules(path.resolve(pkgDir, "node_modules"));
|
|
21
|
+
cleanNodeModules(path.resolve(pkgDir, "..", "node_modules"));
|
package/src/file-kind.ts
CHANGED
|
@@ -81,24 +81,19 @@ export async function loadFileKindAndText(
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
const decoder = new TextDecoder("utf-8");
|
|
85
|
-
const fatalDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
84
|
+
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
86
85
|
let hadUtf8DecodeErrors = false;
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
hadUtf8DecodeErrors = true;
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
throw error;
|
|
86
|
+
const parts: string[] = [];
|
|
87
|
+
|
|
88
|
+
function decodeChunk(chunk: Uint8Array, stream: boolean): string {
|
|
89
|
+
const decoded = decoder.decode(chunk, { stream });
|
|
90
|
+
if (!hadUtf8DecodeErrors && decoded.includes("\uFFFD")) {
|
|
91
|
+
hadUtf8DecodeErrors = true;
|
|
97
92
|
}
|
|
98
|
-
|
|
93
|
+
return decoded;
|
|
94
|
+
}
|
|
99
95
|
|
|
100
|
-
|
|
101
|
-
const parts: string[] = [decoder.decode(sample, { stream: true })];
|
|
96
|
+
parts.push(decodeChunk(sample, true));
|
|
102
97
|
|
|
103
98
|
let position = bytesRead;
|
|
104
99
|
while (true) {
|
|
@@ -113,12 +108,10 @@ export async function loadFileKindAndText(
|
|
|
113
108
|
}
|
|
114
109
|
|
|
115
110
|
const chunk = buffer.subarray(0, chunkBytesRead);
|
|
116
|
-
|
|
117
|
-
parts.push(decoder.decode(chunk, { stream: true }));
|
|
111
|
+
parts.push(decodeChunk(chunk, true));
|
|
118
112
|
position += chunkBytesRead;
|
|
119
113
|
}
|
|
120
|
-
|
|
121
|
-
parts.push(decoder.decode());
|
|
114
|
+
parts.push(decodeChunk(new Uint8Array(0), false));
|
|
122
115
|
|
|
123
116
|
return {
|
|
124
117
|
kind: "text",
|
package/src/file-reader.ts
CHANGED
|
@@ -38,23 +38,29 @@ export async function fileSnap(absolutePath: string): Promise<SnapInfo> {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export interface ReadNormOptions {
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
accessMode?: number;
|
|
44
|
+
preloadedFile?: LFile;
|
|
45
|
+
maxLines?: number;
|
|
46
|
+
store?: HashStore;
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
export async function readNormFile(
|
|
42
50
|
path: string,
|
|
43
51
|
cwd: string,
|
|
44
|
-
|
|
45
|
-
accessMode: number = constants.R_OK,
|
|
46
|
-
preloadedFile?: LFile,
|
|
47
|
-
maxLines?: number,
|
|
48
|
-
store?: HashStore,
|
|
52
|
+
options?: ReadNormOptions,
|
|
49
53
|
): Promise<NormFile> {
|
|
50
54
|
const absolutePath = toCwd(path, cwd);
|
|
51
55
|
const resolvedPath = await resolveTarget(absolutePath);
|
|
56
|
+
const signal = options?.signal;
|
|
57
|
+
const accessMode = options?.accessMode ?? constants.R_OK;
|
|
52
58
|
|
|
53
59
|
abortIf(signal);
|
|
54
60
|
await valAccess(resolvedPath, path, accessMode);
|
|
55
61
|
|
|
56
62
|
abortIf(signal);
|
|
57
|
-
const file = preloadedFile ?? (await loadFileKindAndText(resolvedPath));
|
|
63
|
+
const file = options?.preloadedFile ?? (await loadFileKindAndText(resolvedPath));
|
|
58
64
|
valKind(file, path);
|
|
59
65
|
|
|
60
66
|
abortIf(signal);
|
|
@@ -62,16 +68,16 @@ export async function readNormFile(
|
|
|
62
68
|
const originalEnding = detectEnding(rawContent);
|
|
63
69
|
const normalized = toLF(rawContent);
|
|
64
70
|
|
|
65
|
-
if (maxLines !== undefined) {
|
|
71
|
+
if (options?.maxLines !== undefined) {
|
|
66
72
|
const lineCount = visLines(normalized).length;
|
|
67
|
-
if (lineCount > maxLines) {
|
|
73
|
+
if (lineCount > options.maxLines) {
|
|
68
74
|
throw new Error(
|
|
69
|
-
`[E_FILE_TOO_LARGE] ${path} has ${lineCount} lines, exceeding the ${maxLines}-line edit limit. Hashline editing targets source-sized files; for very large files use write or a non-line-based approach.`,
|
|
75
|
+
`[E_FILE_TOO_LARGE] ${path} has ${lineCount} lines, exceeding the ${options.maxLines}-line edit limit. Hashline editing targets source-sized files; for very large files use write or a non-line-based approach.`,
|
|
70
76
|
);
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
const fileHashes = await lineHashes(normalized, resolvedPath, undefined, store);
|
|
80
|
+
const fileHashes = await lineHashes(normalized, resolvedPath, undefined, options?.store);
|
|
75
81
|
return {
|
|
76
82
|
absolutePath: resolvedPath,
|
|
77
83
|
normalized,
|
package/src/read.ts
CHANGED
|
@@ -163,9 +163,9 @@ export function regRead(pi: ExtensionAPI): void {
|
|
|
163
163
|
) => ReturnType<typeof builtinRead.execute>;
|
|
164
164
|
return executeBuiltinRead(_toolCallId, params, signal, _onUpdate, ctx);
|
|
165
165
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
const { normalized, fileHashes, hadUtf8DecodeErrors } = await readNormFile(
|
|
167
|
+
rawPath, ctx.cwd, { signal, preloadedFile: file },
|
|
168
|
+
);
|
|
169
169
|
const preview = await fmtReadPreview(
|
|
170
170
|
normalized,
|
|
171
171
|
{
|
package/src/replace-normalize.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isRec, has } from "./utils";
|
|
2
2
|
import { CONTENT_LINES_NOT_STRING_MSG } from "./constants";
|
|
3
3
|
|
|
4
|
-
function tryParseContentLines(record: Record<string, unknown>, key: string): void {
|
|
4
|
+
export function tryParseContentLines(record: Record<string, unknown>, key: string): void {
|
|
5
5
|
const val = record[key];
|
|
6
6
|
if (typeof val !== "string") return;
|
|
7
7
|
try {
|
package/src/replace-response.ts
CHANGED
|
@@ -53,7 +53,7 @@ export interface SuccessInput {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
function
|
|
56
|
+
export function buildMetrics(args: {
|
|
57
57
|
classification: "applied" | "noop";
|
|
58
58
|
editsAttempted: number;
|
|
59
59
|
noopEditsCount: number;
|
|
@@ -109,7 +109,7 @@ export function buildNoop(input: NoopInput): TResult {
|
|
|
109
109
|
|
|
110
110
|
const text = `No changes made to ${path}\nClassification: noop\n${noopDetailsText}`;
|
|
111
111
|
|
|
112
|
-
const metrics =
|
|
112
|
+
const metrics = buildMetrics({
|
|
113
113
|
classification: "noop",
|
|
114
114
|
editsAttempted: editMeta.editsAttempted,
|
|
115
115
|
noopEditsCount: editMeta.noopEditsCount,
|
|
@@ -146,7 +146,7 @@ export function buildChanged(input: SuccessInput): TResult {
|
|
|
146
146
|
? `${successPrefix}${lineSummary}${warningsBlock}`
|
|
147
147
|
: `${successPrefix}${lineSummary}`;
|
|
148
148
|
|
|
149
|
-
const metrics =
|
|
149
|
+
const metrics = buildMetrics({
|
|
150
150
|
classification: "applied",
|
|
151
151
|
editsAttempted: editMeta.editsAttempted,
|
|
152
152
|
noopEditsCount: editMeta.noopEditsCount,
|
package/src/replace-undo.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { toCwd } from "./paths";
|
|
|
9
9
|
import { toLF, stripBOM, genDiff, restoreEndings } from "./replace-diff";
|
|
10
10
|
import { cntDiff } from "./utils";
|
|
11
11
|
import { loadP, loadGuide } from "./prompts";
|
|
12
|
+
import { buildMetrics } from "./replace-response";
|
|
12
13
|
export interface UndoEntry {
|
|
13
14
|
content: string;
|
|
14
15
|
bom: string;
|
|
@@ -107,11 +108,14 @@ export function regReplaceUndo(pi: ExtensionAPI): void {
|
|
|
107
108
|
},
|
|
108
109
|
],
|
|
109
110
|
details: {
|
|
110
|
-
metrics: {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
metrics: buildMetrics({
|
|
112
|
+
classification: "applied",
|
|
113
|
+
editsAttempted: 1,
|
|
114
|
+
noopEditsCount: 0,
|
|
115
|
+
warningsCount: 0,
|
|
116
|
+
addedLines: linesRemovedByReplace,
|
|
117
|
+
removedLines: linesAddedByReplace,
|
|
118
|
+
}),
|
|
115
119
|
},
|
|
116
120
|
};
|
|
117
121
|
});
|
package/src/replace.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
restoreEndings,
|
|
12
12
|
} from "./replace-diff";
|
|
13
13
|
import { readNormFile } from "./file-reader";
|
|
14
|
-
import { normReq, normalizeFilePath } from "./replace-normalize";
|
|
14
|
+
import { normReq, normalizeFilePath, tryParseContentLines } from "./replace-normalize";
|
|
15
15
|
import { isRec, has, rejectUnknownFields, abortIf } from "./utils";
|
|
16
16
|
import { MAX_HASH_LINES } from "./constants";
|
|
17
17
|
import { resolveTarget, writeAtomic } from "./fs-write";
|
|
@@ -148,13 +148,17 @@ export function assertReq(
|
|
|
148
148
|
throw new Error('[E_BAD_SHAPE] Edit request requires a "changes" array. Each change is { content_lines: [...], hash_range_inclusive: ["<START>", "<END>"] }.');
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
export interface ExecPipelineOptions {
|
|
152
|
+
accessMode?: number;
|
|
153
|
+
signal?: AbortSignal;
|
|
154
|
+
store?: HashStore;
|
|
155
|
+
noPersist?: boolean;
|
|
156
|
+
}
|
|
157
|
+
|
|
151
158
|
export async function execPipeline(
|
|
152
159
|
params: ReqParams,
|
|
153
160
|
cwd: string,
|
|
154
|
-
|
|
155
|
-
signal?: AbortSignal,
|
|
156
|
-
store?: HashStore,
|
|
157
|
-
noPersist?: boolean,
|
|
161
|
+
options?: ExecPipelineOptions,
|
|
158
162
|
): Promise<PipelineResult> {
|
|
159
163
|
|
|
160
164
|
const path = params.path;
|
|
@@ -166,17 +170,17 @@ export async function execPipeline(
|
|
|
166
170
|
throw new Error('[E_BAD_SHAPE] Edit request requires a non-empty "changes" array.');
|
|
167
171
|
}
|
|
168
172
|
|
|
169
|
-
const hashStore = store ?? await loadHashStore();
|
|
173
|
+
const hashStore = options?.store ?? await loadHashStore();
|
|
170
174
|
|
|
171
175
|
const { normalized: originalNormalized, bom, originalEnding, fileHashes: originalHashes, hadUtf8DecodeErrors, absolutePath } = await readNormFile(
|
|
172
|
-
path, cwd, signal, accessMode
|
|
176
|
+
path, cwd, { signal: options?.signal, accessMode: options?.accessMode, maxLines: MAX_HASH_LINES, store: hashStore },
|
|
173
177
|
);
|
|
174
178
|
|
|
175
179
|
const resolved = resEdits(toolEdits);
|
|
176
180
|
const anchorResult = applyEdits(
|
|
177
181
|
originalNormalized,
|
|
178
182
|
resolved,
|
|
179
|
-
signal,
|
|
183
|
+
options?.signal,
|
|
180
184
|
originalHashes,
|
|
181
185
|
path,
|
|
182
186
|
);
|
|
@@ -196,6 +200,7 @@ export async function execPipeline(
|
|
|
196
200
|
}
|
|
197
201
|
}
|
|
198
202
|
|
|
203
|
+
const noPersist = options?.noPersist;
|
|
199
204
|
const resultHashes = await lineHashes(result, absolutePath, {
|
|
200
205
|
content: originalNormalized,
|
|
201
206
|
hashes: originalHashes,
|
|
@@ -248,10 +253,7 @@ export async function compPreview(
|
|
|
248
253
|
const { path, originalNormalized, originalHashes, result, resultHashes } = await execPipeline(
|
|
249
254
|
normalized,
|
|
250
255
|
cwd,
|
|
251
|
-
constants.R_OK,
|
|
252
|
-
undefined,
|
|
253
|
-
undefined,
|
|
254
|
-
true,
|
|
256
|
+
{ accessMode: constants.R_OK, noPersist: true },
|
|
255
257
|
);
|
|
256
258
|
|
|
257
259
|
if (originalNormalized === result) {
|
|
@@ -336,10 +338,7 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
336
338
|
const record = { ...args };
|
|
337
339
|
normalizeFilePath(record);
|
|
338
340
|
if (has(record, "content_lines") && typeof record.content_lines === "string") {
|
|
339
|
-
|
|
340
|
-
const parsed = JSON.parse(record.content_lines as string);
|
|
341
|
-
if (Array.isArray(parsed)) record.content_lines = parsed;
|
|
342
|
-
} catch {}
|
|
341
|
+
tryParseContentLines(record, "content_lines");
|
|
343
342
|
}
|
|
344
343
|
return record;
|
|
345
344
|
}
|
|
@@ -461,8 +460,7 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
461
460
|
} = await execPipeline(
|
|
462
461
|
normalizedParams,
|
|
463
462
|
ctx.cwd,
|
|
464
|
-
constants.R_OK | constants.W_OK,
|
|
465
|
-
signal,
|
|
463
|
+
{ accessMode: constants.R_OK | constants.W_OK, signal },
|
|
466
464
|
);
|
|
467
465
|
|
|
468
466
|
const editsAttempted = opts.flat
|