pi-hashline-edit-pro 0.13.1 → 0.13.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/index.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { readFile } from "fs/promises";
3
3
  import { join, isAbsolute } from "path";
4
- import { lineHashes, initHasher, fmtRegion } from "./src/hashline";
4
+ import { initHasher } from "./src/hashline";
5
5
  import { regReplace } from "./src/replace";
6
- import { regRead, formatPaginationHint } from "./src/read";
6
+ import { regRead, fmtReadPreview } from "./src/read";
7
7
  import { toLF, stripBOM } from "./src/replace-diff";
8
8
  import { visLines } from "./src/utils";
9
9
  import { AUTO_READ_MAX } from "./src/constants";
@@ -47,29 +47,18 @@ export default function (pi: ExtensionAPI): void {
47
47
  const content = await readFile(absolutePath, "utf-8");
48
48
  const { text: rawContent } = stripBOM(content);
49
49
  const normalized = toLF(rawContent);
50
- const visibleLines = visLines(normalized);
51
50
 
52
- if (visibleLines.length === 0) return;
51
+ if (visLines(normalized).length === 0) return;
53
52
 
54
- const truncated = visibleLines.length > AUTO_READ_MAX;
55
- const displayLines = truncated ? visibleLines.slice(0, AUTO_READ_MAX) : visibleLines;
53
+ const preview = fmtReadPreview(normalized, { limit: AUTO_READ_MAX });
54
+ if (!preview.text) return;
56
55
 
57
- const hashes = lineHashes(normalized);
58
- const selectedHashes = hashes.slice(0, displayLines.length);
59
- const hashlineOutput = fmtRegion(selectedHashes, displayLines);
60
-
61
- const paginationHint = truncated
62
- ? `\n\n${formatPaginationHint(1, AUTO_READ_MAX, visibleLines.length, AUTO_READ_MAX + 1)}`
63
- : "";
64
-
65
- if (hashlineOutput) {
66
- return {
67
- content: [
68
- ...(event.content ?? []),
69
- { type: "text", text: `\n\n--- Auto-read (hashline anchors) ---\n${hashlineOutput}${paginationHint}` },
70
- ],
71
- };
72
- }
56
+ return {
57
+ content: [
58
+ ...(event.content ?? []),
59
+ { type: "text", text: `\n\n--- Auto-read (hashline anchors) ---\n${preview.text}` },
60
+ ],
61
+ };
73
62
  } catch (error) {
74
63
  console.error("Auto-read after write failed:", error);
75
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "0.13.1",
3
+ "version": "0.13.3",
4
4
  "description": "Strict hashline read/replace tool for pi-coding-agent with hash-anchored edits (3-char, 18-bit, perfect hashing)",
5
5
  "main": "index.ts",
6
6
  "repository": {
package/src/constants.ts CHANGED
@@ -1,16 +1,4 @@
1
1
  export const AUTO_READ_MAX = 2000;
2
- export const ANCHOR_BUDGET = 50 * 1024;
3
-
4
- /**
5
- * Context lines of post-edit anchor surfacing. Intentionally 0: after a
6
- * successful `replace` the response text is empty by design (the model calls
7
- * `read` for fresh anchors — see "Chained edits" in the README). With 0,
8
- * `affRange` returns null, so the anchor-block branch in `buildChanged`
9
- * (`src/replace-response.ts`) is dormant. Set this > 0 (and likely bump
10
- * MAX_OUT) to revive surfacing a small anchor window after each edit.
11
- */
12
- export const CTX_LINES = 0;
13
- export const MAX_OUT = 12;
14
2
  export const SNIFF_BYTES = 8192;
15
3
  export const MAX_BYTES = 100 * 1024 * 1024;
16
4
 
@@ -12,7 +12,6 @@ import {
12
12
  type BDupWarn,
13
13
  } from "./resolve";
14
14
  import { cntLines } from "../utils";
15
- import { CTX_LINES, MAX_OUT } from "../constants";
16
15
 
17
16
  type LIdx = {
18
17
  fileLines: string[];
@@ -271,6 +270,7 @@ export function applyEdits(
271
270
  content: string;
272
271
  firstChangedLine: number | undefined;
273
272
  lastChangedLine: number | undefined;
273
+ resultHashes?: string[];
274
274
  warnings?: string[];
275
275
  noopEdits?: NEdit[];
276
276
  } {
@@ -350,52 +350,12 @@ export function applyEdits(
350
350
  content: result,
351
351
  firstChangedLine: range?.firstChangedLine,
352
352
  lastChangedLine: range?.lastChangedLine,
353
+ resultHashes,
353
354
  ...(warnings.length ? { warnings } : {}),
354
355
  ...(noopEdits.length ? { noopEdits } : {}),
355
356
  };
356
357
  }
357
358
 
358
- export function affRange(params: {
359
- firstChangedLine: number | undefined;
360
- lastChangedLine: number | undefined;
361
- resultLineCount: number;
362
- contextLines?: number;
363
- maxOutputLines?: number;
364
- }): { start: number; end: number } | null {
365
- const {
366
- firstChangedLine,
367
- lastChangedLine,
368
- resultLineCount,
369
- contextLines = CTX_LINES,
370
- maxOutputLines = MAX_OUT,
371
- } = params;
372
-
373
- if (firstChangedLine === undefined || lastChangedLine === undefined) {
374
- return null;
375
- }
376
-
377
- if (contextLines === 0) {
378
- return null;
379
- }
380
-
381
- if (resultLineCount === 0) {
382
- return null;
383
- }
384
-
385
- const start = Math.max(1, firstChangedLine - contextLines);
386
- const end = Math.min(resultLineCount, lastChangedLine + contextLines);
387
-
388
- if (end < start) {
389
- return null;
390
- }
391
-
392
- if (end - start + 1 > maxOutputLines) {
393
- return null;
394
- }
395
-
396
- return { start, end };
397
- }
398
-
399
359
  export function fmtRegion(
400
360
  hashes: string[],
401
361
  lines: string[],
@@ -55,6 +55,9 @@ function getH(): Hasher {
55
55
  hasherP = xxhash().then((h) => {
56
56
  hasher = h;
57
57
  return h;
58
+ }).catch((err) => {
59
+ console.error("xxhash-wasm initialization failed:", err);
60
+ throw err;
58
61
  });
59
62
 
60
63
  export function initHasher(): Promise<Hasher> {
@@ -34,7 +34,6 @@ export {
34
34
  export {
35
35
  buildIdx,
36
36
  applyEdits,
37
- affRange,
38
37
  fmtRegion,
39
38
  changedRange,
40
39
  } from "./apply";
@@ -9,6 +9,24 @@ function tryParseJSON<T>(value: unknown, guard: (v: unknown) => v is T): T | und
9
9
  return undefined;
10
10
  }
11
11
 
12
+ /**
13
+ * Coerces an array of edit items: JSON-string items → objects,
14
+ * JSON-string content_lines → string arrays. Shared by the `changes`
15
+ * and `edits` normalization branches.
16
+ */
17
+ function coerceEditArray(items: unknown[]): unknown[] {
18
+ return items
19
+ .map((item: unknown) => tryParseJSON(item, isRec) ?? item)
20
+ .map((change: unknown) => {
21
+ if (!isRec(change)) return change;
22
+ if (typeof change.content_lines !== "string") return change;
23
+ const parsed = tryParseJSON(change.content_lines, (v): v is string[] =>
24
+ Array.isArray(v) && v.every((i) => typeof i === "string"),
25
+ );
26
+ return parsed ? { ...change, content_lines: parsed } : change;
27
+ });
28
+ }
29
+
12
30
  export function normReq(input: unknown): unknown {
13
31
  if (!isRec(input)) {
14
32
  return input;
@@ -24,36 +42,30 @@ export function normReq(input: unknown): unknown {
24
42
  const hasChangesField = has(record, "changes");
25
43
  const hasEditsField = has(record, "edits");
26
44
 
27
- if (hasChangesField) {
45
+ if (hasChangesField) {
28
46
  const raw = tryParseJSON(record.changes, Array.isArray) ?? record.changes;
29
47
  if (Array.isArray(raw)) {
30
- record.changes = raw
31
- .map((item: unknown) => tryParseJSON(item, isRec) ?? item)
32
- .map((change: unknown) => {
33
- if (!isRec(change)) return change;
34
- if (typeof change.content_lines !== "string") return change;
35
- const parsed = tryParseJSON(change.content_lines, (v): v is string[] =>
36
- Array.isArray(v) && v.every((i) => typeof i === "string"),
37
- );
38
- return parsed ? { ...change, content_lines: parsed } : change;
39
- });
48
+ record.changes = coerceEditArray(raw);
49
+ } else {
50
+ // Single object (JSON string or literal) wrap in array
51
+ const single = typeof raw === "string"
52
+ ? tryParseJSON(raw, isRec)
53
+ : isRec(raw) ? raw : undefined;
54
+ if (single) record.changes = coerceEditArray([single]);
40
55
  }
41
- } else if (hasEditsField) {
56
+ } else if (hasEditsField) {
42
57
  const raw = tryParseJSON(record.edits, Array.isArray) ?? record.edits;
43
58
  if (Array.isArray(raw)) {
44
- record.changes = raw
45
- .map((item: unknown) => tryParseJSON(item, isRec) ?? item)
46
- .map((change: unknown) => {
47
- if (!isRec(change)) return change;
48
- if (typeof change.content_lines !== "string") return change;
49
- const parsed = tryParseJSON(change.content_lines, (v): v is string[] =>
50
- Array.isArray(v) && v.every((i) => typeof i === "string"),
51
- );
52
- return parsed ? { ...change, content_lines: parsed } : change;
53
- });
59
+ record.changes = coerceEditArray(raw);
60
+ } else {
61
+ const single = typeof raw === "string"
62
+ ? tryParseJSON(raw, isRec)
63
+ : isRec(raw) ? raw : undefined;
64
+ if (single) record.changes = coerceEditArray([single]);
54
65
  }
55
66
  delete record.edits;
56
- }
67
+ }
57
68
 
58
69
  return record;
59
70
  }
71
+
@@ -1,12 +1,9 @@
1
1
  import type { ReplaceDetails } from "./replace";
2
2
  import { genDiff } from "./replace-diff";
3
3
  import {
4
- affRange,
5
4
  lineHashes,
6
- fmtRegion,
7
5
  } from "./hashline";
8
6
  import { visLines } from "./utils";
9
- import { ANCHOR_BUDGET } from "./constants";
10
7
 
11
8
  type TResult = {
12
9
  content: Array<{ type: "text"; text: string }>;
@@ -153,34 +150,9 @@ export function buildChanged(input: SuccessInput): TResult {
153
150
  const addedLines = cntDiff(diffResult.diff, "+");
154
151
  const removedLines = cntDiff(diffResult.diff, "-");
155
152
  const warningsBlock = warnBlock(warnings);
156
- const anchorRange = affRange({
157
- firstChangedLine: editMeta.firstChangedLine,
158
- lastChangedLine: editMeta.lastChangedLine,
159
- resultLineCount: resultLines.length,
160
- });
161
- const anchorsBlock = anchorRange
162
- ? (() => {
163
- const region = resultLines.slice(
164
- anchorRange.start - 1,
165
- anchorRange.end,
166
- );
167
- const regionHashes = resultHashes.slice(
168
- anchorRange.start - 1,
169
- anchorRange.end,
170
- );
171
- const formatted = fmtRegion(regionHashes, region);
172
- const block = `--- Anchors ---\n${formatted}`;
173
- return Buffer.byteLength(block, "utf8") <=
174
- ANCHOR_BUDGET
175
- ? block
176
- : "Anchors omitted; use read for subsequent edits.";
177
- })()
178
- : resultLines.length === 0
179
- ? "File is empty. Use replace to insert content."
180
- : "";
181
- const text = [anchorsBlock, warningsBlock.trimStart()]
182
- .filter((section) => section.length > 0)
183
- .join("\n\n");
153
+ const text = resultLines.length === 0
154
+ ? "File is empty. Use replace to insert content."
155
+ : warningsBlock.trimStart();
184
156
 
185
157
  const metrics = buildM({
186
158
  classification: "applied",
package/src/replace.ts CHANGED
@@ -99,6 +99,7 @@ interface PipelineResult {
99
99
  firstChangedLine?: number;
100
100
  lastChangedLine?: number;
101
101
  originalHashes?: string[];
102
+ resultHashes?: string[];
102
103
  }
103
104
 
104
105
  const E_DESC = loadP("../prompts/replace.md");
@@ -173,6 +174,7 @@ async function execPipeline(
173
174
  noopEdits: anchorResult.noopEdits,
174
175
  firstChangedLine: anchorResult.firstChangedLine,
175
176
  lastChangedLine: anchorResult.lastChangedLine,
177
+ resultHashes: anchorResult.resultHashes,
176
178
  originalHashes,
177
179
  };
178
180
  }
@@ -184,7 +186,7 @@ export async function compPreview(
184
186
  try {
185
187
  const normalized = normReq(request);
186
188
  assertReq(normalized);
187
- const { path, originalNormalized, result } = await execPipeline(
189
+ const { path, originalNormalized, result, resultHashes } = await execPipeline(
188
190
  normalized,
189
191
  cwd,
190
192
  constants.R_OK,
@@ -196,7 +198,7 @@ export async function compPreview(
196
198
  };
197
199
  }
198
200
 
199
- return { diff: genDiff(originalNormalized, result, 4, lineHashes(result)).diff };
201
+ return { diff: genDiff(originalNormalized, result, 4, resultHashes).diff };
200
202
  } catch (error: unknown) {
201
203
  return { error: error instanceof Error ? error.message : String(error) };
202
204
  }
@@ -340,6 +342,7 @@ const toolDef: ToolDef = {
340
342
  noopEdits,
341
343
  firstChangedLine,
342
344
  lastChangedLine,
345
+ resultHashes,
343
346
  } = await execPipeline(
344
347
  normalizedParams,
345
348
  ctx.cwd,
@@ -390,7 +393,7 @@ const toolDef: ToolDef = {
390
393
  path,
391
394
  originalNormalized,
392
395
  result,
393
- resultHashes: lineHashes(result),
396
+ resultHashes,
394
397
  warnings,
395
398
  snapshotId: updatedSnapshotId,
396
399
  editMeta,