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 +1 -1
- package/package.json +3 -2
- package/src/hashline/resolve.ts +22 -3
- package/src/read.ts +7 -1
- package/src/replace-normalize.ts +1 -8
- package/src/replace-render.ts +1 -55
- package/src/replace-undo.ts +7 -1
- package/src/replace.ts +11 -8
- package/src/utils.ts +5 -2
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
|
|
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.
|
|
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",
|
package/src/hashline/resolve.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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)",
|
package/src/replace-normalize.ts
CHANGED
|
@@ -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)) {
|
package/src/replace-render.ts
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/src/replace-undo.ts
CHANGED
|
@@ -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
|
|
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
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
6
|
-
|
|
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[] {
|