pi-hashline-edit-pro 2.3.1 → 2.4.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 +15 -12
- package/package.json +1 -1
- package/prompts/replace-guidelines.md +6 -5
- package/prompts/replace-snippet.md +1 -1
- package/prompts/replace.md +1 -1
- package/prompts/undo-last-replace-guidelines.md +1 -1
- package/src/constants.ts +1 -1
- package/src/hashline/parse.ts +1 -1
- package/src/hashline/resolve.ts +27 -29
- package/src/replace-render.ts +8 -3
- package/src/replace.ts +29 -23
package/README.md
CHANGED
|
@@ -28,8 +28,9 @@ kQm│}
|
|
|
28
28
|
```json
|
|
29
29
|
{
|
|
30
30
|
"path": "src/main.ts",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
31
|
+
"remove_from": "szJ",
|
|
32
|
+
"remove_to": "szJ",
|
|
33
|
+
"replacement_text": " console.log('hi');"
|
|
33
34
|
}
|
|
34
35
|
```
|
|
35
36
|
|
|
@@ -73,25 +74,27 @@ Edge cases:
|
|
|
73
74
|
|
|
74
75
|
The built-in `edit` tool is disabled. `replace` is the only edit path, and it takes the hash anchors from `read` output.
|
|
75
76
|
|
|
76
|
-
One edit per call, with `
|
|
77
|
+
One edit per call, with `remove_from`, `remove_to`, and `replacement_text` at the top level:
|
|
77
78
|
|
|
78
79
|
```json
|
|
79
80
|
{
|
|
80
81
|
"path": "src/main.ts",
|
|
81
|
-
"
|
|
82
|
-
"
|
|
82
|
+
"remove_from": "szJ",
|
|
83
|
+
"remove_to": "kQm",
|
|
84
|
+
"replacement_text": " console.log('hi');\n}"
|
|
83
85
|
}
|
|
84
86
|
```
|
|
85
87
|
|
|
86
88
|
| Field | Description |
|
|
87
89
|
| --- | --- |
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
+
| `remove_from` | 3-char hash from `read` output marking the FIRST line to remove (inclusive). |
|
|
91
|
+
| `remove_to` | 3-char hash from `read` output marking the LAST line to remove (inclusive). |
|
|
92
|
+
| `replacement_text` | Replacement text as a single string with `\n` line separators; every `\n` separates lines, so a trailing `\n` adds a final empty line — mirror the removed lines exactly, blank lines included (a replacement that is only blank lines is written as one `\n` per blank line). Use `""` to delete the range. |
|
|
90
93
|
|
|
91
94
|
Notes:
|
|
92
95
|
|
|
93
96
|
- 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 in `
|
|
97
|
+
- Common copy-paste slips are fixed automatically and reported: a leftover `HASH│` prefix in `replacement_text` or `remove_from`/`remove_to`, diff-preview rows pasted into the replacement, a reversed range, or a boundary line pasted twice. New lines that re-include a block adjacent to the range are stripped automatically when that block is unique in the file — the whole run is stripped as one unit (including repeated structural lines like `}`), so re-including an unchanged block next to the range never duplicates it. A missing `path` is resolved from the anchors when they uniquely identify a file in the hash store (reported as a warning); when the anchors match multiple known files the request is rejected with the candidate paths named. `file_path` works as an alias for `path` in all three tools.
|
|
95
98
|
- An edit that produces identical content reports `No changes made` and leaves the anchors alone.
|
|
96
99
|
- After a successful edit you get the post-edit diff with fresh anchors, so you can keep editing without re-reading.
|
|
97
100
|
- Do not issue multiple replace calls on the same file in one message; parallel edits split attention across the post-edit diffs and removed lines are easy to miss. Verify each diff before the next edit on that file.
|
|
@@ -150,12 +153,12 @@ A no-op replace never changes the file, so anchors remain valid. On first run af
|
|
|
150
153
|
|
|
151
154
|
| Code | Meaning |
|
|
152
155
|
| --- | --- |
|
|
153
|
-
| `[E_BAD_SHAPE]` | Request envelope or edit item has unknown, missing, or wrongly-typed fields (for example `
|
|
154
|
-
| `[E_BAD_REF]` | An anchor in `
|
|
156
|
+
| `[E_BAD_SHAPE]` | Request envelope or edit item has unknown, missing, or wrongly-typed fields (for example `replacement_text` must be a string with `\n` line separators). |
|
|
157
|
+
| `[E_BAD_REF]` | An anchor in `remove_from`/`remove_to` is not a bare 3-char hash. |
|
|
155
158
|
| `[E_STALE_ANCHOR]` | An anchor does not match any line in the current file; call `read` for fresh anchors. |
|
|
156
159
|
| `[E_AMBIGUOUS_ANCHOR]` | An anchor matches multiple lines; call `read` for fresh anchors. |
|
|
157
|
-
| `[E_INVALID_PATCH]` | A `
|
|
158
|
-
| `[E_BARE_HASH_PREFIX]` | A `
|
|
160
|
+
| `[E_INVALID_PATCH]` | A `replacement_text` line is a diff-preview row (`+HASH│`, `-HASH│`, `- │`). The marker is stripped automatically with a warning. |
|
|
161
|
+
| `[E_BARE_HASH_PREFIX]` | A `replacement_text` line starts with a hash-like `HASH│` prefix. The prefix is stripped automatically with a warning. |
|
|
159
162
|
| `[E_BAD_OP]` | Range start line is after range end line. The pair is swapped automatically with a warning. |
|
|
160
163
|
| `[E_WOULD_EMPTY]` | An edit would empty a non-empty file; use `write` instead. |
|
|
161
164
|
| `[E_NOT_FOUND]` | The path does not exist. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hash-anchored read/replace/undo 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,7 +1,8 @@
|
|
|
1
|
-
- `replace`:
|
|
1
|
+
- `replace`: remove_from and remove_to take ONLY the bare 3-char hash — read row `ve7│function hello() {` means `"remove_from": "ve7"`. Never paste the line content, a code line, a paragraph, or the whole `HASH│content` row into these fields.
|
|
2
|
+
- `replace`: remove_from and remove_to mark the exact lines that are REMOVED, and replacement_text is their complete replacement applied in order; nothing outside the range changes. Every line inside the range that is not reproduced byte-exact in replacement_text is deleted from the file.
|
|
2
3
|
- `replace`: keep the range as tight as the change — anchor only the first and last line that actually change, never a whole function, class, or import block when only part of it changes.
|
|
3
|
-
- `replace`: to replace a single line,
|
|
4
|
+
- `replace`: to replace a single line, set both remove_from and remove_to to the same hash: remove_from: "<HASH>", remove_to: "<HASH>".
|
|
4
5
|
- `replace`: when copying a line from read output, remove its HASH│ prefix and keep the leading whitespace exactly as shown.
|
|
5
|
-
- `replace`: every `\n` in
|
|
6
|
-
- `replace`: when auto-read shows the post-edit diff, its rows are the fresh anchors for the new file — `+HASH│` and ` HASH│` rows carry current hashes and unchanged lines keep their previous hashes, so you can anchor follow-up edits on the diff without re-reading.
|
|
7
|
-
- `replace`: do not issue multiple replace calls on the same file in one message
|
|
6
|
+
- `replace`: every `\n` in replacement_text separates lines, so a trailing `\n` adds a final empty line. Mirror the removed lines exactly: a range that ends on a blank line must end replacement_text with `\n` (e.g. `"code\n"`), and a replacement whose last line is not blank must not end with `\n`. To add a blank line after a line, end replacement_text with an explicit empty line after it (e.g. `"X\n"` adds a blank after X). A replacement that is only blank lines is written as one `\n` per blank line.
|
|
7
|
+
- `replace`: when auto-read shows the post-edit diff, its rows are the fresh anchors for the new file — `+HASH│` and ` HASH│` rows carry current hashes and unchanged lines keep their previous hashes, so you can anchor follow-up edits on the diff without re-reading.
|
|
8
|
+
- `replace`: do not issue multiple replace calls on the same file in one message. Issue the next edit only after verifying the previous diff.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Replace lines in a text file via HASH anchors from read,
|
|
1
|
+
Replace lines in a text file via bare 3-char HASH anchors from read — hash only, never line content; anchor exactly the lines that change; one edit per tool call
|
package/prompts/replace.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Replace a range of lines in a text file, targeted by the 3-char HASH anchors from read
|
|
1
|
+
Replace a range of lines in a text file, targeted by the 3-char HASH anchors from read output. remove_from and remove_to must each be a BARE 3-character hash: copy only the hash from the leftmost column of a read row (row `ve7│function hello() {` means `"remove_from": "ve7"`). Never pass the line content, a code line, or a paragraph into these fields.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
- `undo_last_replace`: reverts only the most recent replace on the file — any write to the file clears the undo history, so call it immediately after a bad replace. A replace is bad when its post-edit diff shows `-HASH│` rows for lines you meant to keep (a closing brace, import, or declaration).
|
|
2
|
-
- `undo_last_replace`: when auto-read shows the post-edit diff, its `+HASH│` and ` HASH│` rows are the fresh anchors for the restored file, so follow-up edits can anchor on the diff without re-reading.
|
|
2
|
+
- `undo_last_replace`: when auto-read shows the post-edit diff, its `+HASH│` and ` HASH│` rows are the fresh anchors for the restored file, so follow-up edits can anchor on the diff without re-reading.
|
package/src/constants.ts
CHANGED
|
@@ -6,5 +6,5 @@ export const MAX_READ_LINE_BYTES = 200 * 1024;
|
|
|
6
6
|
export const HASH_STORE_BUSY_TIMEOUT = 1000;
|
|
7
7
|
export const HASH_STORE_VERSION = 5;
|
|
8
8
|
export const NEW_CONTENT_NOT_STRING_MSG =
|
|
9
|
-
`[E_BAD_SHAPE] "
|
|
9
|
+
`[E_BAD_SHAPE] "replacement_text" must be a string with \\n line separators, not an array.`
|
|
10
10
|
+ ` Do not pass an array of lines — pass the replacement text as one string: "line1\\nline2". Use "" to delete a range.`;
|
package/src/hashline/parse.ts
CHANGED
|
@@ -18,7 +18,7 @@ function diagRef(ref: string): string {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
if (trimmed.includes("│")) {
|
|
21
|
-
return `[E_BAD_REF] Invalid anchor "${trimmed}".
|
|
21
|
+
return `[E_BAD_REF] Invalid anchor "${trimmed}". remove_from and remove_to must contain the 3-char hash only — remove everything from "│" onward.`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
return `[E_BAD_REF] Invalid anchor "${trimmed}". Expected a 3-char alphanumeric anchor (e.g. "aB3").`;
|
package/src/hashline/resolve.ts
CHANGED
|
@@ -39,8 +39,9 @@ export interface NEdit {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export type HTEdit = {
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
replacement_text: string;
|
|
43
|
+
remove_from: string;
|
|
44
|
+
remove_to: string;
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
function resAnchorFromMap(
|
|
@@ -88,7 +89,7 @@ export function fmtMismatch(
|
|
|
88
89
|
const refList = notFound.map((m) => `"${m.ref.hash}"`).join(", ");
|
|
89
90
|
if (notFound.length > 0) {
|
|
90
91
|
out.push(
|
|
91
|
-
`[E_STALE_ANCHOR] ${notFound.length} stale anchor${notFound.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}: ${refList}. The file content has changed since those anchors were read. Call read() to get fresh anchors, then copy the 3-char HASH of the start and end of the range you are replacing into
|
|
92
|
+
`[E_STALE_ANCHOR] ${notFound.length} stale anchor${notFound.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}: ${refList}. The file content has changed since those anchors were read. Call read() to get fresh anchors, then copy the 3-char HASH of the start and end of the range you are replacing into remove_from and remove_to of your next replace call.`
|
|
92
93
|
);
|
|
93
94
|
for (const m of notFound) {
|
|
94
95
|
const ctx = m.context;
|
|
@@ -106,7 +107,7 @@ export function fmtMismatch(
|
|
|
106
107
|
if (ambiguous.length > 0) {
|
|
107
108
|
if (out.length > 0) out.push("");
|
|
108
109
|
out.push(
|
|
109
|
-
`[E_AMBIGUOUS_ANCHOR] ${ambiguous.length} ambiguous anchor${ambiguous.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}. Call read() to get fresh anchors, then copy the 3-char HASH of the start and end of the range you are replacing into
|
|
110
|
+
`[E_AMBIGUOUS_ANCHOR] ${ambiguous.length} ambiguous anchor${ambiguous.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}. Call read() to get fresh anchors, then copy the 3-char HASH of the start and end of the range you are replacing into remove_from and remove_to of your next replace call.`
|
|
110
111
|
);
|
|
111
112
|
for (const m of ambiguous) {
|
|
112
113
|
const sample = (m.candidates ?? []).slice(0, 5);
|
|
@@ -129,33 +130,30 @@ export function fmtMismatch(
|
|
|
129
130
|
return out.join("\n");
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
const ITEM_KS = new Set(["
|
|
133
|
-
|
|
134
|
-
function isStrPair(value: unknown): value is [string, string] {
|
|
135
|
-
return (
|
|
136
|
-
Array.isArray(value) &&
|
|
137
|
-
value.length === 2 &&
|
|
138
|
-
value.every((item) => typeof item === "string")
|
|
139
|
-
);
|
|
140
|
-
}
|
|
133
|
+
const ITEM_KS = new Set(["replacement_text", "remove_from", "remove_to"]);
|
|
141
134
|
|
|
142
135
|
function assertItem(edit: Record<string, unknown>): void {
|
|
143
|
-
rejectUnknownFields(edit, ITEM_KS, "Edit", "The edit takes only {
|
|
136
|
+
rejectUnknownFields(edit, ITEM_KS, "Edit", "The edit takes only { replacement_text, remove_from, remove_to }.");
|
|
144
137
|
|
|
145
|
-
if ("
|
|
138
|
+
if ("remove_from" in edit && typeof edit.remove_from !== "string") {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`[E_BAD_SHAPE] Field "remove_from" must be an anchor string (3-char hash).`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if ("remove_to" in edit && typeof edit.remove_to !== "string") {
|
|
146
144
|
throw new Error(
|
|
147
|
-
`[E_BAD_SHAPE] Field "
|
|
145
|
+
`[E_BAD_SHAPE] Field "remove_to" must be an anchor string (3-char hash).`,
|
|
148
146
|
);
|
|
149
147
|
}
|
|
150
|
-
if (!("
|
|
151
|
-
throw new Error(`[E_BAD_SHAPE] The edit requires a "
|
|
148
|
+
if (!("replacement_text" in edit)) {
|
|
149
|
+
throw new Error(`[E_BAD_SHAPE] The edit requires a "replacement_text" field. Provide the replacement text (use "" to delete).`);
|
|
152
150
|
}
|
|
153
|
-
if (typeof edit.
|
|
151
|
+
if (typeof edit.replacement_text !== "string") {
|
|
154
152
|
throw new Error(NEW_CONTENT_NOT_STRING_MSG);
|
|
155
153
|
}
|
|
156
|
-
if (
|
|
154
|
+
if (typeof edit.remove_from !== "string" || typeof edit.remove_to !== "string") {
|
|
157
155
|
throw new Error(
|
|
158
|
-
`[E_BAD_SHAPE] The edit requires
|
|
156
|
+
`[E_BAD_SHAPE] The edit requires "remove_from" and "remove_to" anchor strings (3-char hashes from read output).`,
|
|
159
157
|
);
|
|
160
158
|
}
|
|
161
159
|
}
|
|
@@ -165,18 +163,18 @@ const ANCHOR_ROW_RE = new RegExp(`^([+-]?)(${HASH_CLASS})│`);
|
|
|
165
163
|
export function resEdit(edit: HTEdit, warnings?: string[]): HEdit {
|
|
166
164
|
assertItem(edit as Record<string, unknown>);
|
|
167
165
|
|
|
168
|
-
const replaceLines = parseText(edit.
|
|
169
|
-
const bounds = edit.
|
|
166
|
+
const replaceLines = parseText(edit.replacement_text);
|
|
167
|
+
const bounds = [edit.remove_from, edit.remove_to].map((ref) => {
|
|
170
168
|
const trimmed = ref.trim();
|
|
171
169
|
const match = trimmed.match(ANCHOR_ROW_RE);
|
|
172
170
|
if (match) {
|
|
173
171
|
let message: string;
|
|
174
172
|
if (match[1] === "+") {
|
|
175
|
-
message = `[E_BAD_REF] Autocorrected: stripped diff-preview marker copied from the diff preview in
|
|
173
|
+
message = `[E_BAD_REF] Autocorrected: stripped diff-preview marker copied from the diff preview in remove_from/remove_to entry "${trimmed}".`;
|
|
176
174
|
} else if (match[1] === "-") {
|
|
177
|
-
message = `[E_BAD_REF] Autocorrected: stripped leading "-" marker in
|
|
175
|
+
message = `[E_BAD_REF] Autocorrected: stripped leading "-" marker in remove_from/remove_to entry "${trimmed}".`;
|
|
178
176
|
} else {
|
|
179
|
-
message = `[E_BAD_REF] Autocorrected: stripped "HASH│" prefix copied from read output in
|
|
177
|
+
message = `[E_BAD_REF] Autocorrected: stripped "HASH│" prefix copied from read output in remove_from/remove_to entry "${trimmed}".`;
|
|
180
178
|
}
|
|
181
179
|
warnings?.push(message);
|
|
182
180
|
return match[2]!;
|
|
@@ -215,7 +213,7 @@ export function stripBarePrefixes(
|
|
|
215
213
|
});
|
|
216
214
|
if (stripped.length === 0) return edit;
|
|
217
215
|
const locations = stripped
|
|
218
|
-
.map((s) => `
|
|
216
|
+
.map((s) => `replacement_text line ${s.lineIndex + 1}`)
|
|
219
217
|
.join(", ");
|
|
220
218
|
const matchedCount = stripped.filter((s) => s.matched).length;
|
|
221
219
|
const evidence =
|
|
@@ -251,7 +249,7 @@ export function stripDiffPrefixes(
|
|
|
251
249
|
return line;
|
|
252
250
|
});
|
|
253
251
|
if (stripped.length === 0) return edit;
|
|
254
|
-
const locations = stripped.map((i) => `
|
|
252
|
+
const locations = stripped.map((i) => `replacement_text line ${i + 1}`).join(", ");
|
|
255
253
|
warnings.push(
|
|
256
254
|
`[E_INVALID_PATCH] Autocorrected: stripped diff-preview marker copied from the diff preview in ${locations}.`
|
|
257
255
|
);
|
|
@@ -278,7 +276,7 @@ export function swapReversedRanges(
|
|
|
278
276
|
return edit;
|
|
279
277
|
}
|
|
280
278
|
warnings.push(
|
|
281
|
-
`[E_BAD_OP] Autocorrected:
|
|
279
|
+
`[E_BAD_OP] Autocorrected: remove_from and remove_to were reversed (remove_from ${startRef.hash} is after remove_to ${endRef.hash}); swapped the pair.`
|
|
282
280
|
);
|
|
283
281
|
return { ...edit, hash_bounds: [endRef, startRef] as [Anchor, Anchor] };
|
|
284
282
|
}
|
package/src/replace-render.ts
CHANGED
|
@@ -32,14 +32,19 @@ export function getPreviewInput(
|
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
if (
|
|
35
|
+
if (
|
|
36
|
+
typeof normalized.remove_from !== "string" ||
|
|
37
|
+
typeof normalized.remove_to !== "string" ||
|
|
38
|
+
typeof normalized.replacement_text !== "string"
|
|
39
|
+
) {
|
|
36
40
|
return null;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
const request: ReqParams = {
|
|
40
44
|
path: normalized.path,
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
remove_from: normalized.remove_from,
|
|
46
|
+
remove_to: normalized.remove_to,
|
|
47
|
+
replacement_text: normalized.replacement_text,
|
|
43
48
|
};
|
|
44
49
|
return request;
|
|
45
50
|
}
|
package/src/replace.ts
CHANGED
|
@@ -46,32 +46,33 @@ import { loadP, loadGuide } from "./prompts";
|
|
|
46
46
|
import { saveUndo } from "./replace-undo";
|
|
47
47
|
import { loadHashStore, findSnapshotPaths, type HashStore } from "./hash-store";
|
|
48
48
|
|
|
49
|
-
const
|
|
49
|
+
const replacementTextSchema = Type.String({
|
|
50
50
|
description:
|
|
51
|
-
"Replacement
|
|
51
|
+
"Replacement text as a single string with \\n line separators; every \\n separates lines, so a trailing \\n adds a final empty line. Mirror the removed lines exactly, blank lines included. A replacement that is only blank lines is written as one \\n per blank line. Use \"\" to delete the range."
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
54
|
+
const removeFromSchema = Type.String({
|
|
55
|
+
description: "Bare 3-char HASH only (e.g. \"aB3\") — copy just the hash from the leftmost column of a read row like `aB3│content`; never the line content. Marks the FIRST line to remove (inclusive)",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const removeToSchema = Type.String({
|
|
59
|
+
description: "Bare 3-char HASH only (e.g. \"aB3\") — copy just the hash from the leftmost column of a read row like `aB3│content`; never the line content. Marks the LAST line to remove (inclusive)",
|
|
60
|
+
});
|
|
62
61
|
|
|
63
62
|
export const editToolSchema = Type.Object(
|
|
64
63
|
{
|
|
65
64
|
path: Type.Optional(Type.String({ description: "Path to edit. Required — always provide it explicitly; it is only auto-resolved from the anchors as a fallback when omitted by mistake." })),
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
remove_from: removeFromSchema,
|
|
66
|
+
remove_to: removeToSchema,
|
|
67
|
+
replacement_text: replacementTextSchema,
|
|
68
68
|
},
|
|
69
69
|
{ additionalProperties: false },
|
|
70
70
|
);
|
|
71
71
|
export type ReqParams = {
|
|
72
72
|
path: string;
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
remove_from: string;
|
|
74
|
+
remove_to: string;
|
|
75
|
+
replacement_text: string;
|
|
75
76
|
};
|
|
76
77
|
|
|
77
78
|
export type ReplaceDetails = {
|
|
@@ -101,7 +102,7 @@ interface PipelineResult {
|
|
|
101
102
|
|
|
102
103
|
const PREVIEW_DEBOUNCE_MS = 150;
|
|
103
104
|
|
|
104
|
-
const ROOT_KS = new Set(["path", "
|
|
105
|
+
const ROOT_KS = new Set(["path", "remove_from", "remove_to", "replacement_text"]);
|
|
105
106
|
|
|
106
107
|
export function assertReq(
|
|
107
108
|
request: unknown,
|
|
@@ -116,9 +117,13 @@ export function assertReq(
|
|
|
116
117
|
throw new Error('[E_BAD_SHAPE] Edit request requires a non-empty "path" string.');
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
if (
|
|
120
|
+
if (
|
|
121
|
+
typeof request.remove_from !== "string" ||
|
|
122
|
+
typeof request.remove_to !== "string" ||
|
|
123
|
+
typeof request.replacement_text !== "string"
|
|
124
|
+
) {
|
|
120
125
|
throw new Error(
|
|
121
|
-
'[E_BAD_SHAPE] Edit request requires
|
|
126
|
+
'[E_BAD_SHAPE] Edit request requires "remove_from", "remove_to", and "replacement_text" at the top level.',
|
|
122
127
|
);
|
|
123
128
|
}
|
|
124
129
|
}
|
|
@@ -127,11 +132,11 @@ async function resolveMissingPath(
|
|
|
127
132
|
request: Record<string, unknown>,
|
|
128
133
|
): Promise<{ path: string; warning: string } | undefined> {
|
|
129
134
|
if (typeof request.path === "string") return undefined;
|
|
130
|
-
const
|
|
131
|
-
|
|
135
|
+
const from = request.remove_from;
|
|
136
|
+
const to = request.remove_to;
|
|
137
|
+
if (typeof from !== "string" || typeof to !== "string") return undefined;
|
|
132
138
|
const hashes: string[] = [];
|
|
133
|
-
for (const ref of
|
|
134
|
-
if (typeof ref !== "string") return undefined;
|
|
139
|
+
for (const ref of [from, to]) {
|
|
135
140
|
try {
|
|
136
141
|
hashes.push(parseHashRef(ref).hash);
|
|
137
142
|
} catch {
|
|
@@ -215,8 +220,9 @@ export async function execPipeline(
|
|
|
215
220
|
const editWarnings: string[] = [];
|
|
216
221
|
const edit = resEdit(
|
|
217
222
|
{
|
|
218
|
-
|
|
219
|
-
|
|
223
|
+
remove_from: params.remove_from,
|
|
224
|
+
remove_to: params.remove_to,
|
|
225
|
+
replacement_text: params.replacement_text,
|
|
220
226
|
},
|
|
221
227
|
editWarnings,
|
|
222
228
|
);
|