pi-hashline-edit-pro 0.11.0 → 0.11.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
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": {
@@ -1,3 +1,3 @@
1
1
  - Use `replace` with HASH anchors for all file changes; batch every change to one file into a single `replace` call.
2
2
  - After a successful `replace`, the response text is empty (warnings only). Call `read` to get fresh anchors for follow-up edits.
3
- - On `[E_STALE_ANCHOR]`, call `read` to get fresh anchors, copy the 3-character HASH from each line into `old_range`, and retry.
3
+ - On `[E_STALE_ANCHOR]`, call `read` to get fresh anchors, copy the 3-character HASH from each line into `hash_range_incl`, and retry.
@@ -13,10 +13,10 @@ read({ path: "src/main.ts" })
13
13
  // VRW│const z = 3;
14
14
  ```
15
15
 
16
- 2. Copy the 3-character HASH (before `│`) into `old_range`:
16
+ 2. Copy the 3-character HASH (before `│`) into `hash_range_incl`:
17
17
  ```json
18
18
  { "path": "src/main.ts", "edits": [
19
- { "old_range": ["MQX", "MQX"], "new_lines": ["const x = 99;"] }
19
+ { "hash_range_incl": ["MQX", "MQX"], "new_lines": ["const x = 99;"] }
20
20
  ] }
21
21
  ```
22
22
 
@@ -25,14 +25,14 @@ Examples:
25
25
  1. Single line replace:
26
26
  ```json
27
27
  { "path": "src/main.ts", "edits": [
28
- { "old_range": ["MQX", "MQX"], "new_lines": ["const x = 1;"] }
28
+ { "hash_range_incl": ["MQX", "MQX"], "new_lines": ["const x = 1;"] }
29
29
  ] }
30
30
  ```
31
31
 
32
32
  2. Range replace (3 lines → 3 new lines):
33
33
  ```json
34
34
  { "path": "src/main.ts", "edits": [
35
- { "old_range": ["ZPM", "VRW"], "new_lines": [
35
+ { "hash_range_incl": ["ZPM", "VRW"], "new_lines": [
36
36
  "function greet(name) {",
37
37
  " return `Hello, ${name}`;",
38
38
  "}"
@@ -43,8 +43,8 @@ Examples:
43
43
  3. Multiple regions in one call (delete two non-adjacent ranges):
44
44
  ```json
45
45
  { "path": "src/server.ts", "edits": [
46
- { "old_range": ["aB3", "xY7"], "new_lines": [] },
47
- { "old_range": ["MQX", "ZPM"], "new_lines": [] }
46
+ { "hash_range_incl": ["aB3", "xY7"], "new_lines": [] },
47
+ { "hash_range_incl": ["MQX", "ZPM"], "new_lines": [] }
48
48
  ] }
49
49
  ```
50
50
 
@@ -52,38 +52,38 @@ Examples:
52
52
 
53
53
  Wrong:
54
54
  ```json
55
- { "old_range": ["F4T", "F4T"], "new_lines": ["F4T│import { x } from \"./x\";"] }
55
+ { "hash_range_incl": ["F4T", "F4T"], "new_lines": ["F4T│import { x } from \"./x\";"] }
56
56
  ```
57
57
 
58
58
  Right:
59
59
  ```json
60
- { "old_range": ["F4T", "F4T"], "new_lines": ["import { x } from \"./x\";"] }
60
+ { "hash_range_incl": ["F4T", "F4T"], "new_lines": ["import { x } from \"./x\";"] }
61
61
  ```
62
62
 
63
- `old_range` uses the hash anchor. `new_lines` uses literal file content only — the same text that appears after the `│` in `read` output.
63
+ `hash_range_incl` uses the hash anchor. `new_lines` uses literal file content only — the same text that appears after the `│` in `read` output.
64
64
 
65
- ⚠️ Common mistake: `old_range` is only the 3-character HASH, not the full `HASH│content` line.
65
+ ⚠️ Common mistake: `hash_range_incl` is only the 3-character HASH, not the full `HASH│content` line.
66
66
 
67
67
  Wrong:
68
68
  ```json
69
- { "old_range": ["F4T│import { x } from \"./x\";", "F4T│import { x } from \"./x\";"], "new_lines": [...] }
69
+ { "hash_range_incl": ["F4T│import { x } from \"./x\";", "F4T│import { x } from \"./x\";"], "new_lines": [...] }
70
70
  ```
71
71
 
72
72
  Right:
73
73
  ```json
74
- { "old_range": ["F4T", "F4T"], "new_lines": [...] }
74
+ { "hash_range_incl": ["F4T", "F4T"], "new_lines": [...] }
75
75
  ```
76
76
 
77
77
  Rules:
78
- - `old_range` is a pair `[start, end]`. A single-line replace is `old_range: ["X", "X"]`.
78
+ - `hash_range_incl` is a pair `[start, end]`. A single-line replace is `hash_range_incl: ["X", "X"]`.
79
79
  - To delete a range, use `new_lines: []`.
80
- - `old_range` elements are HASH anchors only (e.g. `aB3`). Do not include `│` or line content.
80
+ - `hash_range_incl` elements are HASH anchors only (e.g. `aB3`). Do not include `│` or line content.
81
81
  - `new_lines` is literal file content — each string becomes exactly one line in the file. No `HASH│` prefix, no `+`/`-` diff markers.
82
82
  - Don't add `""` for spacing unless you actually want a new blank line.
83
83
  - Copy anchors from the most recent `read` of the file. Do not guess or construct them.
84
84
  - All edits in one call must be non-conflicting. The runtime rejects with `[E_EDIT_CONFLICT]` if two ranges overlap.
85
85
  - If `new_lines` matches current content, the replace is classified as `noop` (file unchanged).
86
- - The `old_range` is inclusive — both anchors and every line between them are replaced. If your replacement content includes lines that already exist in the file (e.g. closing brackets), make sure those lines are within your range, otherwise they will appear twice.
86
+ - The `hash_range_incl` is inclusive — both anchors and every line between them are replaced. If your replacement content includes lines that already exist in the file (e.g. closing brackets), make sure those lines are within your range, otherwise they will appear twice.
87
87
 
88
88
  On success, the response text is empty (or contains only warnings if present). Call `read` to get fresh anchors for follow-up edits.
89
89
 
@@ -92,9 +92,9 @@ Error recovery:
92
92
  - `[E_BAD_REF]` — malformed HASH. Re-read and try again.
93
93
  - `[E_BAD_OP]` — invalid operation (e.g. start line > end line).
94
94
  - `[E_BAD_SHAPE]` — malformed request or edit item (missing fields, wrong types, unknown fields).
95
- - `[E_LEGACY_SHAPE]` — old `oldText`/`newText` format detected. Use `{old_range, new_lines}` instead.
95
+ - `[E_LEGACY_SHAPE]` — old `oldText`/`newText` or `old_range` format detected. Use `{hash_range_incl, new_lines}` instead.
96
96
  - `[E_EDIT_CONFLICT]` — two edits overlap on the same line range. Make edits non-overlapping.
97
97
  - `[E_AMBIGUOUS_ANCHOR]` — hash collision. Call `read` to get fresh anchors.
98
- - `[E_BARE_HASH_PREFIX]` — a `new_lines` entry starts with `HASH│`. Remove the hash prefix; keep only the literal line content that appears after `│` in `read` output. `old_range` uses hashes, `new_lines` does not.
98
+ - `[E_BARE_HASH_PREFIX]` — a `new_lines` entry starts with `HASH│`. Remove the hash prefix; keep only the literal line content that appears after `│` in `read` output. `hash_range_incl` uses hashes, `new_lines` does not.
99
99
  - `[E_INVALID_PATCH]` — diff prefixes (`+`/`-`) in `new_lines`. Use literal content only.
100
- - `[E_WOULD_EMPTY]` — edit would empty a non-empty file.
100
+ - `[E_WOULD_EMPTY]` — edit would empty a non-empty file.
@@ -76,8 +76,8 @@ function resToSpan(
76
76
  ): RESpan | null {
77
77
  const { fileLines, lineStarts, hasTerminalNewline } = lineIndex;
78
78
 
79
- const startLine = edit.old_range[0].line;
80
- const endLine = edit.old_range[1].line;
79
+ const startLine = edit.hash_range_incl[0].line;
80
+ const endLine = edit.hash_range_incl[1].line;
81
81
  const originalLines = fileLines.slice(startLine - 1, endLine);
82
82
  if (
83
83
  originalLines.length === edit.new_lines.length &&
@@ -87,7 +87,7 @@ function resToSpan(
87
87
  ) {
88
88
  noopEdits.push({
89
89
  editIndex: index,
90
- loc: edit.old_range[0].hash,
90
+ loc: edit.hash_range_incl[0].hash,
91
91
  currentContent: originalLines.join("\n"),
92
92
  });
93
93
  return null;
@@ -422,4 +422,4 @@ export function changedRange(
422
422
  }
423
423
 
424
424
  return { firstChangedLine, lastChangedLine };
425
- }
425
+ }
@@ -20,7 +20,7 @@ function diagRef(ref: string): string {
20
20
  }
21
21
 
22
22
  if (trimmed.includes("│")) {
23
- return `[E_BAD_REF] Invalid anchor "${trimmed}". old_range must contain the 3-char hash only — remove everything from "│" onward.`;
23
+ return `[E_BAD_REF] Invalid anchor "${trimmed}". hash_range_incl must contain the 3-char hash only — remove everything from "│" onward.`;
24
24
  }
25
25
 
26
26
  return `[E_BAD_REF] Invalid anchor "${trimmed}". Expected a 3-char base64 anchor (e.g. "aB3").`;
@@ -9,9 +9,9 @@ export type RAnchor = {
9
9
  hashMatched: boolean;
10
10
  };
11
11
 
12
- export type HEdit = { old_range: [Anchor, Anchor]; new_lines: string[] };
12
+ export type HEdit = { hash_range_incl: [Anchor, Anchor]; new_lines: string[] };
13
13
  export type RHEdit = {
14
- old_range: [RAnchor, RAnchor];
14
+ hash_range_incl: [RAnchor, RAnchor];
15
15
  new_lines: string[];
16
16
  };
17
17
 
@@ -37,7 +37,7 @@ export interface NEdit {
37
37
  }
38
38
 
39
39
  export type HTEdit = {
40
- old_range: [string, string];
40
+ hash_range_incl: [string, string];
41
41
  new_lines: string[];
42
42
  };
43
43
 
@@ -118,7 +118,7 @@ export function fmtMismatch(
118
118
  return out.join("\n");
119
119
  }
120
120
 
121
- const ITEM_KS = new Set(["old_range", "new_lines"]);
121
+ const ITEM_KS = new Set(["hash_range_incl", "new_lines"]);
122
122
 
123
123
  function isStrArr(value: unknown): value is string[] {
124
124
  return (
@@ -135,11 +135,11 @@ function isStrPair(value: unknown): value is [string, string] {
135
135
  }
136
136
 
137
137
  function assertItem(edit: Record<string, unknown>, index: number): void {
138
- rejectUnknownFields(edit, ITEM_KS, `Edit ${index}`, "Each edit takes only { old_range, new_lines }.");
138
+ rejectUnknownFields(edit, ITEM_KS, `Edit ${index}`, "Each edit takes only { hash_range_incl, new_lines }.");
139
139
 
140
- if ("old_range" in edit && !isStrPair(edit.old_range)) {
140
+ if ("hash_range_incl" in edit && !isStrPair(edit.hash_range_incl)) {
141
141
  throw new Error(
142
- `[E_BAD_SHAPE] Edit ${index} field "old_range" must be a pair of anchor strings [start, end].`,
142
+ `[E_BAD_SHAPE] Edit ${index} field "hash_range_incl" must be a pair of anchor strings [start, end].`,
143
143
  );
144
144
  }
145
145
  if (!("new_lines" in edit)) {
@@ -148,9 +148,9 @@ function assertItem(edit: Record<string, unknown>, index: number): void {
148
148
  if ("new_lines" in edit && !isStrArr(edit.new_lines)) {
149
149
  throw new Error(`[E_BAD_SHAPE] Edit ${index} field "new_lines" must be a string array.`);
150
150
  }
151
- if (!isStrPair(edit.old_range)) {
151
+ if (!isStrPair(edit.hash_range_incl)) {
152
152
  throw new Error(
153
- `[E_BAD_SHAPE] Edit ${index} requires an "old_range" pair of anchor strings [start, end].`,
153
+ `[E_BAD_SHAPE] Edit ${index} requires an "hash_range_incl" pair of anchor strings [start, end].`,
154
154
  );
155
155
  }
156
156
 
@@ -163,7 +163,7 @@ export function resEdits(edits: HTEdit[]): HEdit[] {
163
163
 
164
164
  const replaceLines = parseText(edit.new_lines);
165
165
  result.push({
166
- old_range: [parseHashRef(edit.old_range[0]), parseHashRef(edit.old_range[1])],
166
+ hash_range_incl: [parseHashRef(edit.hash_range_incl[0]), parseHashRef(edit.hash_range_incl[1])],
167
167
  new_lines: replaceLines,
168
168
  });
169
169
  }
@@ -216,12 +216,12 @@ export function assertNoBarePrefix(
216
216
  : `${matchedCount} match file line hashes — strong evidence the prefix was copied from read output.`;
217
217
 
218
218
  throw new Error(
219
- `[E_BARE_HASH_PREFIX] ${suspects.length} edit line(s) start with a hash-like prefix (${locations}). Example: ${JSON.stringify(exampleLine)}. ${linesHint} Remove the "HASH│" prefix from each affected new_lines entry; keep only the literal line content that appears after "│" in read output. Remember: old_range uses hash anchors, new_lines uses file content only.`
219
+ `[E_BARE_HASH_PREFIX] ${suspects.length} edit line(s) start with a hash-like prefix (${locations}). Example: ${JSON.stringify(exampleLine)}. ${linesHint} Remove the "HASH│" prefix from each affected new_lines entry; keep only the literal line content that appears after "│" in read output. Remember: hash_range_incl uses hash anchors, new_lines uses file content only.`
220
220
  );
221
221
  }
222
222
 
223
223
  export function descEdit(edit: RHEdit): string {
224
- return `replace ${edit.old_range[0].hash}-${edit.old_range[1].hash}`;
224
+ return `replace ${edit.hash_range_incl[0].hash}-${edit.hash_range_incl[1].hash}`;
225
225
  }
226
226
 
227
227
  export function valEdits(
@@ -247,14 +247,14 @@ export function valEdits(
247
247
 
248
248
  for (const edit of edits) {
249
249
  abortIf(signal);
250
- const startResolved = tryResolve(edit.old_range[0]);
251
- const endResolved = tryResolve(edit.old_range[1]);
250
+ const startResolved = tryResolve(edit.hash_range_incl[0]);
251
+ const endResolved = tryResolve(edit.hash_range_incl[1]);
252
252
  if (!startResolved || !endResolved) {
253
253
  continue;
254
254
  }
255
255
  if (startResolved.line > endResolved.line) {
256
256
  throw new Error(
257
- `[E_BAD_OP] Range start line ${startResolved.line} must be <= end line ${endResolved.line} (anchors ${edit.old_range[0].hash} and ${edit.old_range[1].hash}).`,
257
+ `[E_BAD_OP] Range start line ${startResolved.line} must be <= end line ${endResolved.line} (anchors ${edit.hash_range_incl[0].hash} and ${edit.hash_range_incl[1].hash}).`,
258
258
  );
259
259
  }
260
260
  const endLine = endResolved.line;
@@ -293,7 +293,7 @@ export function valEdits(
293
293
  });
294
294
  }
295
295
  resolved.push({
296
- old_range: [startResolved, endResolved],
296
+ hash_range_incl: [startResolved, endResolved],
297
297
  new_lines: edit.new_lines,
298
298
  });
299
299
  }
@@ -301,4 +301,4 @@ export function valEdits(
301
301
  return { resolved, mismatches, boundaryWarnings };
302
302
  }
303
303
 
304
- export { warnUnicodeEsc };
304
+ export { warnUnicodeEsc };
@@ -12,6 +12,16 @@ function coerceEditsArray(edits: unknown): unknown {
12
12
  }
13
13
  }
14
14
 
15
+ function normalizeEditItem(item: unknown): unknown {
16
+ if (!isRec(item)) return item;
17
+ const record: Record<string, unknown> = { ...item };
18
+ // Rename old_range to hash_range_incl for backward compatibility
19
+ if (has(record, "old_range") && !has(record, "hash_range_incl")) {
20
+ record.hash_range_incl = record.old_range;
21
+ delete record.old_range;
22
+ }
23
+ return record;
24
+ }
15
25
 
16
26
  export function normReq(input: unknown): unknown {
17
27
  if (!isRec(input)) {
@@ -31,5 +41,10 @@ export function normReq(input: unknown): unknown {
31
41
  record.edits = coerceEditsArray(record.edits);
32
42
  }
33
43
 
44
+ // Normalize each edit item: rename old_range to hash_range_incl
45
+ if (Array.isArray(record.edits)) {
46
+ record.edits = record.edits.map(normalizeEditItem);
47
+ }
48
+
34
49
  return record;
35
50
  }
package/src/replace.ts CHANGED
@@ -47,10 +47,10 @@ const newLinesSchema = Type.Array(Type.String(), {
47
47
  "literal replacement file content, one string per line. Must not include the HASH│ prefix from read output.",
48
48
  });
49
49
 
50
- const oldRangeSchema = Type.Array(
50
+ const hashRangeInclSchema = Type.Array(
51
51
  Type.String({ description: "anchor (3-char HASH)" }),
52
52
  {
53
- description: "inclusive line range to replace [start_hash, end_hash]. Each element must be the 3-character hash anchor only; do not include the │ separator or line content.",
53
+ description: "inclusive hash range to replace [start_hash, end_hash]. Each element must be the 3-character hash anchor only; do not include the │ separator or line content.",
54
54
  minItems: 2,
55
55
  maxItems: 2,
56
56
  },
@@ -58,7 +58,7 @@ const oldRangeSchema = Type.Array(
58
58
 
59
59
  const editItemSchema = Type.Object(
60
60
  {
61
- old_range: oldRangeSchema,
61
+ hash_range_incl: hashRangeInclSchema,
62
62
  new_lines: newLinesSchema,
63
63
  },
64
64
  { additionalProperties: false },
@@ -102,7 +102,7 @@ export function assertReq(
102
102
  for (const legacyKey of ["oldText", "newText", "old_text", "new_text", "start", "end", "lines"]) {
103
103
  if (has(request, legacyKey)) {
104
104
  throw new Error(
105
- `[E_LEGACY_SHAPE] "${legacyKey}" is not supported. Use {old_range: ["<START>", "<END>"], new_lines: [...]}.`
105
+ `[E_LEGACY_SHAPE] "${legacyKey}" is not supported. Use {hash_range_incl: ["<START>", "<END>"], new_lines: [...]}.`
106
106
  );
107
107
  }
108
108
  }
@@ -114,7 +114,7 @@ export function assertReq(
114
114
  }
115
115
 
116
116
  if (has(request, "edits") && !Array.isArray(request.edits)) {
117
- throw new Error('[E_BAD_SHAPE] Edit request requires an "edits" array when provided. Each edit is { old_range: ["<START>", "<END>"], new_lines: [...] }.');
117
+ throw new Error('[E_BAD_SHAPE] Edit request requires an "edits" array when provided. Each edit is { hash_range_incl: ["<START>", "<END>"], new_lines: [...] }.');
118
118
  }
119
119
  }
120
120
 
@@ -415,4 +415,4 @@ const toolDef: ToolDef = {
415
415
 
416
416
  export function regReplace(pi: ExtensionAPI): void {
417
417
  pi.registerTool(toolDef);
418
- }
418
+ }