pi-hashline-edit-pro 2.8.3 → 2.8.4
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 +1 -1
- package/prompts/undo-last-change-guidelines.md +1 -1
- package/prompts/undo-last-change.md +1 -1
- package/src/commit.ts +10 -1
- package/src/hashline/parse.ts +21 -3
- package/src/hashline/resolve.ts +3 -8
- package/src/replace-response.ts +1 -1
- package/src/replace-undo.ts +1 -1
- package/src/replace.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hash-anchored read/replace/insert/grep tools for pi-coding-agent. Every line gets a unique 3-char hash (A-Za-z0-9) that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
- `undo_last_change`: only the last `replace`/`insert` per file can be undone; a successful `write` clears it, so call immediately after a bad diff (look for `-anchor│` lines you wanted to keep).
|
|
2
|
-
- `undo_last_change`: if the file changed after the edit you get `[E_UNDO_STALE]` — the record is kept
|
|
2
|
+
- `undo_last_change`: if the file changed after the edit you get `[E_UNDO_STALE]` — nothing is reverted and the record is kept; do not modify the file to force an undo, verify with `read` and stop; if the file was deleted it is restored.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Undo the last `replace` or `insert` on a file, restoring previous content, BOM and line endings. Use after a bad edit when the diff showed wrong lines removed. If the file was deleted, it is restored; if changed elsewhere, you get `[E_UNDO_STALE]
|
|
1
|
+
Undo the last `replace` or `insert` on a file, restoring previous content, BOM and line endings. Use after a bad edit when the diff showed wrong lines removed. If the file was deleted, it is restored; if changed elsewhere, you get `[E_UNDO_STALE]`, nothing is reverted, and the record is kept — do not modify the file to make an undo possible; call `read` to verify the current state and stop. If output says truncated, use `read` to see full file.
|
package/src/commit.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PipelineResult } from "./replace";
|
|
2
|
-
import { abortIf } from "./utils";
|
|
2
|
+
import { abortIf, clipLine } from "./utils";
|
|
3
3
|
import { buildChanged, buildNoop, type RMeta, type TResult } from "./replace-response";
|
|
4
4
|
import { saveUndo } from "./replace-undo";
|
|
5
5
|
import { safeSnapId } from "./file-reader";
|
|
@@ -22,6 +22,12 @@ export interface CommitMeta {
|
|
|
22
22
|
onNoopDedup?: () => void;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function boundaryDedupWarning(lineTexts: string[]): string {
|
|
26
|
+
const quoted = lineTexts.map((line) => `"${clipLine(line, 80)}"`).join(", ");
|
|
27
|
+
const plural = lineTexts.length > 1;
|
|
28
|
+
return `Boundary dedup: ${quoted} already ${plural ? "exist" : "exists"} next to the edited range, so ${plural ? "they were" : "it was"} not added again. Use insert only if you truly want ${plural ? "duplicates" : "a duplicate"}.`;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promise<TResult> {
|
|
26
32
|
const { path, absolutePath, mutationTargetPath, signal } = meta;
|
|
27
33
|
const warnings = [...(meta.prefixWarnings ?? []), ...pipe.warnings];
|
|
@@ -54,6 +60,9 @@ export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promis
|
|
|
54
60
|
"Non-UTF-8 bytes were shown as U+FFFD; this edit rewrote the file as UTF-8.",
|
|
55
61
|
);
|
|
56
62
|
}
|
|
63
|
+
if (pipe.boundaryRemovedLineTexts.length > 0) {
|
|
64
|
+
warnings.push(boundaryDedupWarning(pipe.boundaryRemovedLineTexts));
|
|
65
|
+
}
|
|
57
66
|
|
|
58
67
|
abortIf(signal);
|
|
59
68
|
const undo = await saveUndo(mutationTargetPath, {
|
package/src/hashline/parse.ts
CHANGED
|
@@ -53,6 +53,23 @@ function parseRef(ref: string): Anchor {
|
|
|
53
53
|
|
|
54
54
|
export const parseHashRef = parseRef;
|
|
55
55
|
|
|
56
|
+
const JSON_ENVELOPE_RE = /^\s*\["(.*)"\]\.\s*$/;
|
|
57
|
+
|
|
58
|
+
function unwrapJsonEnvelope(line: string, warnings?: string[]): string {
|
|
59
|
+
const match = line.match(JSON_ENVELOPE_RE);
|
|
60
|
+
if (!match) return line;
|
|
61
|
+
const withoutDot = line.trim().slice(0, -1);
|
|
62
|
+
try {
|
|
63
|
+
JSON.parse(withoutDot);
|
|
64
|
+
return line;
|
|
65
|
+
} catch {
|
|
66
|
+
warnings?.push(
|
|
67
|
+
'[E_BAD_SHAPE] Unwrapped JSON array syntax from a replacement_lines element.',
|
|
68
|
+
);
|
|
69
|
+
return match[1]!;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
56
73
|
export function parseText(edit: string[], warnings?: string[]): string[] {
|
|
57
74
|
if (!Array.isArray(edit) || edit.some((line) => typeof line !== "string")) {
|
|
58
75
|
throw new Error(NEW_CONTENT_NOT_ARRAY_MSG);
|
|
@@ -60,13 +77,14 @@ export function parseText(edit: string[], warnings?: string[]): string[] {
|
|
|
60
77
|
const out: string[] = [];
|
|
61
78
|
let split = false;
|
|
62
79
|
for (const line of edit) {
|
|
63
|
-
const
|
|
64
|
-
|
|
80
|
+
const unwrapped = unwrapJsonEnvelope(line, warnings);
|
|
81
|
+
const normalized = unwrapped.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
82
|
+
if (normalized !== unwrapped) split = true;
|
|
65
83
|
out.push(...normalized.split("\n"));
|
|
66
84
|
}
|
|
67
85
|
if (split) {
|
|
68
86
|
warnings?.push(
|
|
69
|
-
"[E_BAD_SHAPE]
|
|
87
|
+
"[E_BAD_SHAPE] replacement_lines contained embedded newlines; split into one line each.",
|
|
70
88
|
);
|
|
71
89
|
}
|
|
72
90
|
return out;
|
package/src/hashline/resolve.ts
CHANGED
|
@@ -195,7 +195,7 @@ export function stripAnchorRow(
|
|
|
195
195
|
: match[1] === "-"
|
|
196
196
|
? 'leading "-" marker'
|
|
197
197
|
: '"anchor│" prefix';
|
|
198
|
-
warnings?.push(`[E_BAD_REF] Stripped ${marker} from ${entryLabel} "${trimmed}".`);
|
|
198
|
+
warnings?.push(`[E_BAD_REF] Stripped ${marker} from ${entryLabel} "${clipLine(trimmed, 48)}".`);
|
|
199
199
|
return match[2]!;
|
|
200
200
|
}
|
|
201
201
|
|
|
@@ -240,13 +240,8 @@ export function stripBarePrefixes(
|
|
|
240
240
|
const locations = stripped
|
|
241
241
|
.map((s) => `replacement_lines line ${s.lineIndex + 1}`)
|
|
242
242
|
.join(", ");
|
|
243
|
-
const matchedCount = stripped.filter((s) => s.matched).length;
|
|
244
|
-
const guidance =
|
|
245
|
-
matchedCount === 0
|
|
246
|
-
? " Verify it was pasted from read output."
|
|
247
|
-
: "";
|
|
248
243
|
warnings.push(
|
|
249
|
-
`[E_BARE_HASH_PREFIX] Stripped "anchor│" prefix from ${locations}
|
|
244
|
+
`[E_BARE_HASH_PREFIX] Stripped "anchor│" prefix from ${locations}.`
|
|
250
245
|
);
|
|
251
246
|
return { ...edit, content_lines: contentLines };
|
|
252
247
|
}
|
|
@@ -290,7 +285,7 @@ export function swapReversedRanges(
|
|
|
290
285
|
return edit;
|
|
291
286
|
}
|
|
292
287
|
warnings.push(
|
|
293
|
-
`[E_BAD_OP]
|
|
288
|
+
`[E_BAD_OP] Swapped reversed remove_from/remove_to.`
|
|
294
289
|
);
|
|
295
290
|
return { ...edit, hash_bounds: [endRef, startRef] as [Anchor, Anchor] };
|
|
296
291
|
}
|
package/src/replace-response.ts
CHANGED
|
@@ -100,7 +100,7 @@ export function buildNoop(input: NoopInput, noopNoun = "Replacement"): TResult {
|
|
|
100
100
|
: "The edit produced identical content.";
|
|
101
101
|
const dedupNote =
|
|
102
102
|
boundaryRemovedLines !== undefined && boundaryRemovedLines > 0
|
|
103
|
-
? `\nBoundary dedup removed ${boundaryRemovedLines} line(s)
|
|
103
|
+
? `\nBoundary dedup removed ${boundaryRemovedLines} line(s); resend the same edit to apply it literally.`
|
|
104
104
|
: "";
|
|
105
105
|
|
|
106
106
|
const text = `No changes made to ${path}\nClassification: noop\n${noopDetailsText}${dedupNote}${warnBlock(warnings)}`;
|
package/src/replace-undo.ts
CHANGED
|
@@ -146,7 +146,7 @@ export function regUndo(pi: ExtensionAPI): void {
|
|
|
146
146
|
content: [
|
|
147
147
|
{
|
|
148
148
|
type: "text",
|
|
149
|
-
text: `[E_UNDO_STALE] Cannot undo last change on ${path}: the file
|
|
149
|
+
text: `[E_UNDO_STALE] Cannot undo last change on ${path}: the file was modified after the edit, so nothing was reverted. The current content already contains your applied edit plus that external change and is most likely the correct state. Do not modify the file to make an undo possible and do not revert your own edit. The undo record is kept. Call read() to verify the current state, then stop.`
|
|
150
150
|
},
|
|
151
151
|
],
|
|
152
152
|
isError: true,
|
package/src/replace.ts
CHANGED
|
@@ -63,6 +63,7 @@ export interface PipelineResult {
|
|
|
63
63
|
totalRemovedLines: number;
|
|
64
64
|
hadBoundaryDedup: boolean;
|
|
65
65
|
boundaryRemovedLines: number;
|
|
66
|
+
boundaryRemovedLineTexts: string[];
|
|
66
67
|
identity: FileIdentity;
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -91,7 +92,7 @@ async function resolveMissingPath(
|
|
|
91
92
|
if (matches.length === 1) {
|
|
92
93
|
return {
|
|
93
94
|
path: matches[0]!,
|
|
94
|
-
warning: `[E_BAD_SHAPE]
|
|
95
|
+
warning: `[E_BAD_SHAPE] Missing "path" resolved to ${matches[0]}.`,
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
98
|
if (matches.length > 1) {
|
|
@@ -226,6 +227,7 @@ export async function execPipeline(
|
|
|
226
227
|
totalRemovedLines,
|
|
227
228
|
hadBoundaryDedup: (anchorResult.autoFixes?.length ?? 0) > 0,
|
|
228
229
|
boundaryRemovedLines: anchorResult.autoFixes?.length ?? 0,
|
|
230
|
+
boundaryRemovedLineTexts: anchorResult.autoFixes?.map((fix) => fix.removedLine) ?? [],
|
|
229
231
|
identity,
|
|
230
232
|
};
|
|
231
233
|
}
|