pi-hashline-edit-pro 0.11.0 → 0.11.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/package.json +1 -1
- package/prompts/replace-guidelines.md +1 -1
- package/prompts/replace.md +18 -18
- package/src/hashline/apply.ts +4 -4
- package/src/hashline/parse.ts +1 -1
- package/src/hashline/resolve.ts +17 -17
- package/src/replace.ts +7 -7
package/package.json
CHANGED
|
@@ -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 `
|
|
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.
|
package/prompts/replace.md
CHANGED
|
@@ -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 `
|
|
16
|
+
2. Copy the 3-character HASH (before `│`) into `hash_range_incl`:
|
|
17
17
|
```json
|
|
18
18
|
{ "path": "src/main.ts", "edits": [
|
|
19
|
-
{ "
|
|
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
|
-
{ "
|
|
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
|
-
{ "
|
|
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
|
-
{ "
|
|
47
|
-
{ "
|
|
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
|
-
{ "
|
|
55
|
+
{ "hash_range_incl": ["F4T", "F4T"], "new_lines": ["F4T│import { x } from \"./x\";"] }
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
Right:
|
|
59
59
|
```json
|
|
60
|
-
{ "
|
|
60
|
+
{ "hash_range_incl": ["F4T", "F4T"], "new_lines": ["import { x } from \"./x\";"] }
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
`
|
|
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: `
|
|
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
|
-
{ "
|
|
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
|
-
{ "
|
|
74
|
+
{ "hash_range_incl": ["F4T", "F4T"], "new_lines": [...] }
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
Rules:
|
|
78
|
-
- `
|
|
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
|
-
- `
|
|
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 `
|
|
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 `{
|
|
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. `
|
|
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.
|
package/src/hashline/apply.ts
CHANGED
|
@@ -76,8 +76,8 @@ function resToSpan(
|
|
|
76
76
|
): RESpan | null {
|
|
77
77
|
const { fileLines, lineStarts, hasTerminalNewline } = lineIndex;
|
|
78
78
|
|
|
79
|
-
const startLine = edit.
|
|
80
|
-
const endLine = edit.
|
|
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.
|
|
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
|
+
}
|
package/src/hashline/parse.ts
CHANGED
|
@@ -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}".
|
|
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").`;
|
package/src/hashline/resolve.ts
CHANGED
|
@@ -9,9 +9,9 @@ export type RAnchor = {
|
|
|
9
9
|
hashMatched: boolean;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
export type HEdit = {
|
|
12
|
+
export type HEdit = { hash_range_incl: [Anchor, Anchor]; new_lines: string[] };
|
|
13
13
|
export type RHEdit = {
|
|
14
|
-
|
|
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
|
-
|
|
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(["
|
|
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 {
|
|
138
|
+
rejectUnknownFields(edit, ITEM_KS, `Edit ${index}`, "Each edit takes only { hash_range_incl, new_lines }.");
|
|
139
139
|
|
|
140
|
-
if ("
|
|
140
|
+
if ("hash_range_incl" in edit && !isStrPair(edit.hash_range_incl)) {
|
|
141
141
|
throw new Error(
|
|
142
|
-
`[E_BAD_SHAPE] Edit ${index} field "
|
|
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.
|
|
151
|
+
if (!isStrPair(edit.hash_range_incl)) {
|
|
152
152
|
throw new Error(
|
|
153
|
-
`[E_BAD_SHAPE] Edit ${index} requires an "
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
251
|
-
const endResolved = tryResolve(edit.
|
|
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.
|
|
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
|
-
|
|
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 };
|
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
|
|
50
|
+
const hashRangeInclSchema = Type.Array(
|
|
51
51
|
Type.String({ description: "anchor (3-char HASH)" }),
|
|
52
52
|
{
|
|
53
|
-
description: "inclusive
|
|
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
|
-
|
|
61
|
+
hash_range_incl: hashRangeInclSchema,
|
|
62
62
|
new_lines: newLinesSchema,
|
|
63
63
|
},
|
|
64
64
|
{ additionalProperties: false },
|
|
@@ -99,10 +99,10 @@ export function assertReq(
|
|
|
99
99
|
throw new Error("[E_BAD_SHAPE] Edit request must be an object.");
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
for (const legacyKey of ["oldText", "newText", "old_text", "new_text", "start", "end", "lines"]) {
|
|
102
|
+
for (const legacyKey of ["oldText", "newText", "old_text", "new_text", "start", "end", "lines", "old_range"]) {
|
|
103
103
|
if (has(request, legacyKey)) {
|
|
104
104
|
throw new Error(
|
|
105
|
-
`[E_LEGACY_SHAPE] "${legacyKey}" is not supported. Use {
|
|
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 {
|
|
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
|
+
}
|