pi-hashline-edit-pro 2.1.0 → 2.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 CHANGED
@@ -91,7 +91,7 @@ One edit per call, with `hash_bounds` and `new_content` at the top level:
91
91
  Notes:
92
92
 
93
93
  - The request is checked before any file I/O, so a bad request never touches the file.
94
- - Common copy-paste slips are fixed automatically and reported: a leftover `HASH│` prefix, diff-preview rows pasted into the replacement, a reversed range, or a boundary line pasted twice. `file_path` works as an alias for `path`.
94
+ - Common copy-paste slips are fixed automatically and reported: a leftover `HASH│` prefix in `new_content` or `hash_bounds`, diff-preview rows pasted into the replacement, a reversed range, or a boundary line pasted twice. `file_path` works as an alias for `path` in all three tools.
95
95
  - An edit that produces identical content reports `No changes made` and leaves the anchors alone.
96
96
  - After a successful edit you get the post-edit diff with fresh anchors, so you can keep editing without re-reading.
97
97
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "type": "module",
5
5
  "description": "Strict hashline read/replace tool for pi-coding-agent with hash-anchored edits (3-char, 62-symbol, perfect hashing)",
6
6
  "main": "index.ts",
@@ -49,7 +49,8 @@
49
49
  "test:watch": "vitest",
50
50
  "test:coverage": "vitest run --coverage --coverage.thresholds.lines=90 --coverage.thresholds.statements=90 --coverage.thresholds.functions=85 --coverage.thresholds.branches=80",
51
51
  "lint": "eslint \"src/**/*.ts\" \"index.ts\" \"test/**/*.ts\"",
52
- "typecheck": "tsc --noEmit"
52
+ "typecheck": "tsc --noEmit",
53
+ "prepublishOnly": "npm run typecheck && npm run lint && npm test"
53
54
  },
54
55
  "devDependencies": {
55
56
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -1,5 +1,5 @@
1
1
  import { abortIf, rejectUnknownFields, lastNonEmpty, firstNonEmpty, clipLine } from "../utils";
2
- import { HL_BARE_PREFIX_RE, HL_PREFIX_PLUS_RE, HL_PREFIX_MINUS_RE } from "./hash";
2
+ import { HASH_CLASS, HL_BARE_PREFIX_RE, HL_PREFIX_PLUS_RE, HL_PREFIX_MINUS_RE } from "./hash";
3
3
  import { parseHashRef, parseText, type Anchor } from "./parse";
4
4
  import { NEW_CONTENT_NOT_STRING_MSG } from "../constants";
5
5
 
@@ -161,13 +161,32 @@ function assertItem(edit: Record<string, unknown>): void {
161
161
  }
162
162
  }
163
163
 
164
- export function resEdit(edit: HTEdit): HEdit {
164
+ const ANCHOR_ROW_RE = new RegExp(`^([+-]?)(${HASH_CLASS})│`);
165
+
166
+ export function resEdit(edit: HTEdit, warnings?: string[]): HEdit {
165
167
  assertItem(edit as Record<string, unknown>);
166
168
 
167
169
  const replaceLines = parseText(edit.new_content);
170
+ const bounds = edit.hash_bounds.map((ref) => {
171
+ const trimmed = ref.trim();
172
+ const match = trimmed.match(ANCHOR_ROW_RE);
173
+ if (match) {
174
+ let message: string;
175
+ if (match[1] === "+") {
176
+ message = `[E_BAD_REF] Autocorrected: stripped diff-preview marker copied from the diff preview in hash_bounds entry "${trimmed}".`;
177
+ } else if (match[1] === "-") {
178
+ message = `[E_BAD_REF] Autocorrected: stripped leading "-" marker in hash_bounds entry "${trimmed}".`;
179
+ } else {
180
+ message = `[E_BAD_REF] Autocorrected: stripped "HASH│" prefix copied from read output in hash_bounds entry "${trimmed}".`;
181
+ }
182
+ warnings?.push(message);
183
+ return match[2]!;
184
+ }
185
+ return ref;
186
+ }) as [string, string];
168
187
  return {
169
188
  content_lines: replaceLines,
170
- hash_bounds: [parseHashRef(edit.hash_bounds[0]), parseHashRef(edit.hash_bounds[1])],
189
+ hash_bounds: [parseHashRef(bounds[0]), parseHashRef(bounds[1])],
171
190
  };
172
191
  }
173
192
 
package/src/read.ts CHANGED
@@ -12,7 +12,7 @@ import { loadFileKindAndText } from "./file-kind";
12
12
  import { readNormFile } from "./file-reader";
13
13
  import { lineHashes, fmtRegion, HASH_SEP, MAX_HASH_LINES } from "./hashline";
14
14
  import { toCwd } from "./paths";
15
- import { abortIf } from "./utils";
15
+ import { abortIf, isRec, normalizeFilePath } from "./utils";
16
16
  import { fileSnap } from "./file-reader";
17
17
  import { visLines } from "./utils";
18
18
  import { loadP, loadGuide } from "./prompts";
@@ -154,6 +154,12 @@ export function regRead(pi: ExtensionAPI): void {
154
154
  description: R_DESC,
155
155
  promptSnippet: R_SNIPPET,
156
156
  promptGuidelines: readGuide(),
157
+ prepareArguments: (args: unknown) => {
158
+ if (!isRec(args)) return args as any;
159
+ const record = { ...args };
160
+ normalizeFilePath(record);
161
+ return record;
162
+ },
157
163
  parameters: Type.Object({
158
164
  path: Type.String({
159
165
  description: "Path to the file to read (relative or absolute)",
@@ -1,11 +1,4 @@
1
- import { isRec } from "./utils";
2
-
3
- export function normalizeFilePath(record: Record<string, unknown>): void {
4
- if (typeof record.path !== "string" && typeof record.file_path === "string") {
5
- record.path = record.file_path;
6
- delete record.file_path;
7
- }
8
- }
1
+ import { isRec, normalizeFilePath } from "./utils";
9
2
 
10
3
  export function normReq(input: unknown): unknown {
11
4
  if (!isRec(input)) {
@@ -163,62 +163,8 @@ function trimEmpty(lines: string[]): string[] {
163
163
  return lines.slice(start, end);
164
164
  }
165
165
 
166
- function isSectionBoundary(line: string): boolean {
167
- return (
168
- line === "--- Anchors ---" ||
169
- line === "Warnings:" ||
170
- line === "Structure outline:" ||
171
- /^--- Range \d+ ---$/.test(line)
172
- );
173
- }
174
-
175
166
  export function fmtResultMd(text: string): string {
176
- const lines = text.split("\n");
177
- const sections: string[] = [];
178
- let plainLines: string[] = [];
179
-
180
- const flush = () => {
181
- const trimmed = trimEmpty(plainLines);
182
- if (trimmed.length > 0) {
183
- sections.push(trimmed.join("\n"));
184
- }
185
- plainLines = [];
186
- };
187
-
188
- let index = 0;
189
- while (index < lines.length) {
190
- const line = lines[index]!;
191
-
192
- if (line.startsWith("--- Anchors ")) {
193
- flush();
194
- const title = line.replace(/^---\s*/, "").replace(/\s*---$/, "");
195
- index++;
196
- const bodyLines: string[] = [];
197
- while (
198
- index < lines.length &&
199
- !isSectionBoundary(lines[index]!)
200
- ) {
201
- bodyLines.push(lines[index]!);
202
- index++;
203
- }
204
- sections.push(
205
- [
206
- `#### ${title}`,
207
- "```text",
208
- ...trimEmpty(bodyLines),
209
- "```",
210
- ].join("\n"),
211
- );
212
- continue;
213
- }
214
-
215
- plainLines.push(line);
216
- index++;
217
- }
218
-
219
- flush();
220
-
221
- return sections.join("\n\n");
167
+ return trimEmpty(text.split("\n")).join("\n");
222
168
  }
223
169
 
224
170
  export function mkMdTheme(theme: MdTheme) {
@@ -7,7 +7,7 @@ import { contentChecksum } from "./hashline/hasher";
7
7
  import { resolveTarget, writeAtomic } from "./fs-write";
8
8
  import { toCwd } from "./paths";
9
9
  import { toLF, stripBOM, genDiff, restoreEndings, type LineEnding } from "./replace-diff";
10
- import { cntDiff, splitLines, errCode } from "./utils";
10
+ import { cntDiff, splitLines, errCode, isRec, normalizeFilePath } from "./utils";
11
11
  import { loadP, loadGuide } from "./prompts";
12
12
  import { buildMetrics } from "./replace-response";
13
13
  import { changedRange } from "./hashline";
@@ -91,6 +91,12 @@ export function regReplaceUndo(pi: ExtensionAPI): void {
91
91
  description: loadP("../prompts/undo-last-replace.md"),
92
92
  promptSnippet: loadP("../prompts/undo-last-replace-snippet.md"),
93
93
  promptGuidelines: loadGuide("../prompts/undo-last-replace-guidelines.md"),
94
+ prepareArguments: (args: unknown) => {
95
+ if (!isRec(args)) return args as any;
96
+ const record = { ...args };
97
+ normalizeFilePath(record);
98
+ return record;
99
+ },
94
100
  parameters: Type.Object({
95
101
  path: Type.String({
96
102
  description: "Path to the file to undo",
package/src/replace.ts CHANGED
@@ -12,8 +12,8 @@ import {
12
12
  type LineEnding,
13
13
  } from "./replace-diff";
14
14
  import { readNormFile } from "./file-reader";
15
- import { normReq, normalizeFilePath } from "./replace-normalize";
16
- import { isRec, rejectUnknownFields, abortIf } from "./utils";
15
+ import { normReq } from "./replace-normalize";
16
+ import { isRec, rejectUnknownFields, abortIf, normalizeFilePath } from "./utils";
17
17
  import { resolveTarget, writeAtomic } from "./fs-write";
18
18
  import { applyEdit,
19
19
  lineHashes,
@@ -175,10 +175,14 @@ export async function execPipeline(
175
175
 
176
176
  const path = params.path;
177
177
 
178
- const edit = resEdit({
179
- hash_bounds: params.hash_bounds,
180
- new_content: params.new_content,
181
- });
178
+ const editWarnings: string[] = [];
179
+ const edit = resEdit(
180
+ {
181
+ hash_bounds: params.hash_bounds,
182
+ new_content: params.new_content,
183
+ },
184
+ editWarnings,
185
+ );
182
186
 
183
187
  const hashStore = options?.store ?? await loadHashStore();
184
188
  const { normalized: originalNormalized, bom, originalEnding, fileHashes: originalHashes, hadUtf8DecodeErrors, absolutePath } = await readNormFile(
@@ -207,8 +211,7 @@ export async function execPipeline(
207
211
  hashes: originalHashes,
208
212
  removedHashes,
209
213
  }, hashStore, noPersist !== true);
210
- const warnings = [...(anchorResult.warnings ?? [])];
211
-
214
+ const warnings = [...editWarnings, ...(anchorResult.warnings ?? [])];
212
215
  const { totalAddedLines, totalRemovedLines } = countLineChanges(
213
216
  edit, originalHashes, isNoop, anchorResult.autoFixes?.length ?? 0,
214
217
  );
package/src/utils.ts CHANGED
@@ -2,8 +2,11 @@ export function isRec(value: unknown): value is Record<string, unknown> {
2
2
  return typeof value === "object" && value !== null && !Array.isArray(value);
3
3
  }
4
4
 
5
- export function has(record: Record<string, unknown>, key: string): boolean {
6
- return Object.hasOwn(record, key);
5
+ export function normalizeFilePath(record: Record<string, unknown>): void {
6
+ if (typeof record.path !== "string" && typeof record.file_path === "string") {
7
+ record.path = record.file_path;
8
+ delete record.file_path;
9
+ }
7
10
  }
8
11
 
9
12
  export function splitLines(text: string): string[] {