pi-hashline-edit-pro 4.3.7 → 4.3.8
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 +3 -3
- package/package.json +1 -1
- package/src/utils.ts +70 -3
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ Edge cases:
|
|
|
73
73
|
| --- | --- |
|
|
74
74
|
| `remove_from` | 4-char anchor marking the FIRST line to remove (inclusive). |
|
|
75
75
|
| `remove_to` | 4-char anchor marking the LAST line to remove (inclusive). |
|
|
76
|
-
| `replacement_lines` | Replacement lines, one element per line. Mirror the removed lines exactly, blank lines included: `[]` deletes the range, `[""]` is a single blank line, `["a", ""]` is a line followed by a blank line. Never embed `\n` inside an element. |
|
|
76
|
+
| `replacement_lines` | Replacement lines, one element per line. Mirror the removed lines exactly, blank lines included: `[]` deletes the range, `[""]` is a single blank line, `["a", ""]` is a line followed by a blank line. Never embed `\n` inside an element. A lone string is accepted too: it is split on newlines, and stringified array text is unwrapped. |
|
|
77
77
|
|
|
78
78
|
Example: read showed `Hasu│old` and `arvm│old2`; to replace both:
|
|
79
79
|
|
|
@@ -89,7 +89,7 @@ Single line: use the same anchor for `remove_from` and `remove_to`. `replace_fro
|
|
|
89
89
|
|
|
90
90
|
The request is checked before any file I/O, so a bad request never touches the file.
|
|
91
91
|
|
|
92
|
-
Common copy-paste slips are fixed automatically: a leftover `anchor│` prefix in `replacement_lines` or the anchor fields (a prefix of 4 to 5 letters before `│`, for example `abde│`), diff-preview rows pasted into the replacement, and a boundary line pasted twice are reported as warnings, while a reversed range,
|
|
92
|
+
Common copy-paste slips are fixed automatically: a leftover `anchor│` prefix in `replacement_lines` or the anchor fields (a prefix of 4 to 5 letters before `│`, for example `abde│`), diff-preview rows pasted into the replacement, and a boundary line pasted twice are reported as warnings, while a reversed range, stringified array text (even with a trailing JS method call, for example `[…].map(s => s)`), and embedded newlines are corrected silently. New lines that re-include a block adjacent to the range are stripped when that block is unique in the file. The whole run is stripped as one unit, so re-including an unchanged block next to the range never duplicates it. Boundary dedup has three modes in `/hashline-config`: `on` strips with a warning, `off` applies edits literally, and `strict` rejects the edit with `[E_BOUNDARY_STRICT]` when any replacement line would be stripped.
|
|
93
93
|
Content containing a NUL byte (`U+0000`) is rejected with `[E_BAD_SHAPE]` before any file I/O: writing it would make the file binary, so use an empty replacement to delete. This applies to `replace`'s `replacement_lines` and `insert`'s `lines`.
|
|
94
94
|
|
|
95
95
|
Every line in the removed range must match what was last shown to you. The extension records the `anchor│content` rows it serves (`read` output, `anchor_grep` output, the auto-read block after `write`, the `+anchor│` and ` anchor│` rows of post-edit diffs, the current-range rows of `[E_RANGE_STALE]` feedback, and the context rows of stale-anchor feedback) and verifies the whole range against that record before writing. A line that changed on disk since it was shown, or an anchor that is not owned in this session, refuses the edit with `[E_RANGE_STALE]` or `[E_STALE_ANCHOR]` and returns the current range with fresh anchors, so the retry needs no `read`. An owned anchor enters the served record when its row is shown (after a restart, restored ownership counts as shown), so a file with no owned anchors cannot be edited by anchor at all; call `read` first. An owned line that was never shown — for example beyond an auto-read preview's truncation cap — is refused with `[E_RANGE_STALE]` and returns the current range, so the retry still needs no `read`.
|
|
@@ -110,7 +110,7 @@ Batched calls must target disjoint ranges; overlapping ranges, or any failing ca
|
|
|
110
110
|
| --- | --- |
|
|
111
111
|
| `anchor` | 4-char anchor marking the line next to which the lines go. The anchor line is preserved. A pasted `+Hasu│x` diff row or `anchor│` prefix is stripped automatically with a warning. |
|
|
112
112
|
| `direction` | `"after"` inserts below the anchor line, `"before"` above it. |
|
|
113
|
-
| `lines` | Lines to insert, one element per line. `[""]` is a blank line. Never include the anchor line, and never embed `\n` inside an element. |
|
|
113
|
+
| `lines` | Lines to insert, one element per line. `[""]` is a blank line. Never include the anchor line, and never embed `\n` inside an element. A lone string is split on newlines, and stringified array text is unwrapped. |
|
|
114
114
|
|
|
115
115
|
Lines are applied literally: nothing is removed, and a line that duplicates its neighbor is kept. `replace`'s boundary anti-duplication never runs for `insert`. Inserting nothing (`lines: []`) reports a noop. To seed an empty file, read it and insert after the `anchor│` empty-line row.
|
|
116
116
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hash-anchored read/replace/insert/grep tools for pi-coding-agent. Every line gets a unique 4-char tokenizer-friendly anchor that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
|
|
6
6
|
"main": "index.ts",
|
package/src/utils.ts
CHANGED
|
@@ -36,6 +36,7 @@ export function normalizeRequest(input: unknown): unknown {
|
|
|
36
36
|
const record: Record<string, unknown> = { ...input };
|
|
37
37
|
normalizeFilePath(record);
|
|
38
38
|
normalizeAnchors(record);
|
|
39
|
+
normalizeEditLines(record);
|
|
39
40
|
return record;
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -211,6 +212,47 @@ function stripCodeFence(text: string): string {
|
|
|
211
212
|
return fenced ? fenced[1]!.trim() : trimmed;
|
|
212
213
|
}
|
|
213
214
|
|
|
215
|
+
function arrayLiteralEnd(text: string): number {
|
|
216
|
+
let depth = 0;
|
|
217
|
+
let quote: string | undefined;
|
|
218
|
+
let escaped = false;
|
|
219
|
+
for (let index = 0; index < text.length; index++) {
|
|
220
|
+
const char = text[index]!;
|
|
221
|
+
if (escaped) {
|
|
222
|
+
escaped = false;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (char === "\\") {
|
|
226
|
+
escaped = true;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (quote !== undefined) {
|
|
230
|
+
if (char === quote) quote = undefined;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (char === '"' || char === "'") {
|
|
234
|
+
quote = char;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (char === "[") depth += 1;
|
|
238
|
+
else if (char === "]") {
|
|
239
|
+
depth -= 1;
|
|
240
|
+
if (depth === 0) return index + 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return -1;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function stripTrailingMemberCall(text: string): string {
|
|
247
|
+
if (!text.startsWith("[")) return text;
|
|
248
|
+
const end = arrayLiteralEnd(text);
|
|
249
|
+
if (end < 0) return text;
|
|
250
|
+
const rest = text.slice(end);
|
|
251
|
+
if (rest.trim().length === 0) return text;
|
|
252
|
+
if (!rest.trimStart().startsWith(".") || !rest.trimEnd().endsWith(")")) return text;
|
|
253
|
+
return text.slice(0, end);
|
|
254
|
+
}
|
|
255
|
+
|
|
214
256
|
function jsonStringArray(text: string): string[] | undefined {
|
|
215
257
|
try {
|
|
216
258
|
const parsed: unknown = JSON.parse(text);
|
|
@@ -288,7 +330,7 @@ function scanArrayText(inner: string): string[] | undefined {
|
|
|
288
330
|
|
|
289
331
|
function decodeArrayText(value: unknown): string[] | undefined {
|
|
290
332
|
if (typeof value !== "string") return undefined;
|
|
291
|
-
const trimmed = stripCodeFence(value);
|
|
333
|
+
const trimmed = stripTrailingMemberCall(stripCodeFence(value));
|
|
292
334
|
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return undefined;
|
|
293
335
|
const decoded = jsonStringArray(trimmed);
|
|
294
336
|
if (decoded !== undefined && decoded.length > 0) return decoded;
|
|
@@ -298,8 +340,8 @@ function decodeArrayText(value: unknown): string[] | undefined {
|
|
|
298
340
|
|
|
299
341
|
function looksLikeStringArray(value: unknown): boolean {
|
|
300
342
|
if (typeof value !== "string") return false;
|
|
301
|
-
const trimmed = stripCodeFence(value);
|
|
302
|
-
return
|
|
343
|
+
const trimmed = stripTrailingMemberCall(stripCodeFence(value));
|
|
344
|
+
return /^\[\s*['"]/.test(trimmed) && !trimmed.endsWith("].");
|
|
303
345
|
}
|
|
304
346
|
|
|
305
347
|
export function decodeStringArray(value: unknown, warnings?: string[], label = "replacement_lines"): string[] | undefined {
|
|
@@ -318,3 +360,28 @@ export function decodeStringArray(value: unknown, warnings?: string[], label = "
|
|
|
318
360
|
}
|
|
319
361
|
return undefined;
|
|
320
362
|
}
|
|
363
|
+
|
|
364
|
+
function splitEditLines(text: string): string[] {
|
|
365
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function normalizeLineFieldValue(value: unknown): string[] | undefined {
|
|
369
|
+
const candidate = typeof value === "string"
|
|
370
|
+
? value
|
|
371
|
+
: Array.isArray(value) && value.length === 1 && typeof value[0] === "string"
|
|
372
|
+
? value[0]
|
|
373
|
+
: undefined;
|
|
374
|
+
if (candidate === undefined) return undefined;
|
|
375
|
+
const decoded = decodeStringArray(candidate);
|
|
376
|
+
if (decoded !== undefined) return decoded;
|
|
377
|
+
if (/^\[\s*\]$/.test(stripTrailingMemberCall(stripCodeFence(candidate)))) return [];
|
|
378
|
+
return splitEditLines(candidate);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function normalizeEditLines(record: Record<string, unknown>): void {
|
|
382
|
+
for (const key of ["replacement_lines", "lines"]) {
|
|
383
|
+
if (!(key in record)) continue;
|
|
384
|
+
const lines = normalizeLineFieldValue(record[key]);
|
|
385
|
+
if (lines !== undefined) record[key] = lines;
|
|
386
|
+
}
|
|
387
|
+
}
|