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 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, undefined);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "0.17.11",
3
+ "version": "0.17.13",
4
4
  "type": "module",
5
5
  "description": "Strict hashline read/replace tool for pi-coding-agent with hash-anchored edits (3-char, 18-bit, perfect hashing)",
6
6
  "main": "index.ts",
@@ -1,17 +1,21 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
 
4
- const parentNodeModules = path.resolve(__dirname, "..", "node_modules");
5
- if (!fs.existsSync(parentNodeModules)) process.exit(0);
6
-
7
- const entries = fs.readdirSync(parentNodeModules, { withFileTypes: true });
8
- for (const entry of entries) {
9
- if (entry.name.startsWith(".better-sqlite3-")) {
10
- const full = path.join(parentNodeModules, entry.name);
11
- try {
12
- fs.rmSync(full, { recursive: true, force: true });
13
- console.error("Cleaned up stale better-sqlite3 build artifact:", entry.name);
14
- } catch {
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 noteUtf8Err = (chunk?: Uint8Array): void => {
88
- if (hadUtf8DecodeErrors) return;
89
- try {
90
- fatalDecoder.decode(chunk, { stream: chunk !== undefined });
91
- } catch (error: unknown) {
92
- if (error instanceof TypeError) {
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
- noteUtf8Err(sample);
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
- noteUtf8Err(chunk);
117
- parts.push(decoder.decode(chunk, { stream: true }));
111
+ parts.push(decodeChunk(chunk, true));
118
112
  position += chunkBytesRead;
119
113
  }
120
- noteUtf8Err();
121
- parts.push(decoder.decode());
114
+ parts.push(decodeChunk(new Uint8Array(0), false));
122
115
 
123
116
  return {
124
117
  kind: "text",
@@ -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
- signal: AbortSignal | undefined,
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
- const { normalized, fileHashes, hadUtf8DecodeErrors } = await readNormFile(
167
- rawPath, ctx.cwd, signal, undefined, file,
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
  {
@@ -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 {
@@ -53,7 +53,7 @@ export interface SuccessInput {
53
53
  }
54
54
 
55
55
 
56
- function buildM(args: {
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 = buildM({
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 = buildM({
149
+ const metrics = buildMetrics({
150
150
  classification: "applied",
151
151
  editsAttempted: editMeta.editsAttempted,
152
152
  noopEditsCount: editMeta.noopEditsCount,
@@ -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
- added_lines: linesRemovedByReplace,
112
- removed_lines: linesAddedByReplace,
113
- classification: "applied" as const,
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
- accessMode: number,
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, undefined, MAX_HASH_LINES, hashStore,
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
- try {
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