pi-hashline-edit-pro 2.3.0 → 2.4.0

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 CHANGED
@@ -28,8 +28,9 @@ kQm│}
28
28
  ```json
29
29
  {
30
30
  "path": "src/main.ts",
31
- "hash_bounds": ["szJ", "szJ"],
32
- "new_content": " console.log('hi');"
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 `hash_bounds` and `new_content` at the top level:
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
- "hash_bounds": ["szJ", "kQm"],
82
- "new_content": " console.log('hi');\n}"
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
- | `hash_bounds` | Pair of 3-char hashes from `read` output marking the first and last line of the range to replace (inclusive). |
89
- | `new_content` | Replacement content as a single string with `\n` line separators; every `\n` separates lines, so a trailing `\n` adds a final empty line mirror the replaced range's 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
+ | `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 `new_content` or `hash_bounds`, 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.
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 `new_content` must be a string with `\n` line separators). |
154
- | `[E_BAD_REF]` | An anchor in `hash_bounds` is not a bare 3-char hash. |
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 `new_content` line is a diff-preview row (`+HASH│`, `-HASH│`, `- │`). The marker is stripped automatically with a warning. |
158
- | `[E_BARE_HASH_PREFIX]` | A `new_content` line starts with a hash-like `HASH│` prefix. The prefix is stripped automatically with a warning. |
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.0",
3
+ "version": "2.4.0",
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,7 @@
1
- - `replace`: hash_bounds marks the exact lines that are REMOVED, and new_content is their complete replacement applied in order; nothing outside the range changes. Every line inside the range that is not reproduced byte-exact in new_content is deleted from the file — including closing braces and other structural lines.
2
- - `replace`: minimize the replaced range — anchor only the lines that actually change, so few unchanged lines must be reproduced byte-exact.
3
- - `replace`: to replace a single line, repeat its hash in both positions of hash_bounds: ["<HASH>", "<HASH>"] — never extend the range to neighboring lines for a one-line edit.
1
+ - `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
+ - `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, set both remove_from and remove_to to the same hash: remove_from: "<HASH>", remove_to: "<HASH>".
4
4
  - `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 new_content separates lines, so a trailing `\n` adds a final empty line. Mirror the replaced range's lines exactly: a range that ends on a blank line must end new_content 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 new_content 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 (e.g. `"\n"` is a single blank line).
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. `-HASH│` rows show removed lines with their old hashes; those hashes are stale after the edit.
7
- - `replace`: do not issue multiple replace calls on the same file in one message — parallel edits on one file split attention across the diffs, and removed lines are easy to miss. Issue the next edit only after verifying the previous diff.
5
+ - `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.
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. Issue the next edit only after verifying the previous diff.
@@ -1 +1 @@
1
- Replace lines in a text file via HASH anchors from read, performing one edit per tool call
1
+ Replace lines in a text file via HASH anchors from read, anchoring exactly the lines that change; one edit per tool call
@@ -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. `-HASH│` rows show removed lines with their old hashes; those hashes are stale after the undo.
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] "new_content" must be a string with \\n line separators, not an array.`
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.`;
@@ -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}". hash_bounds must contain the 3-char hash only — remove everything from "│" onward.`;
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").`;
@@ -39,8 +39,9 @@ export interface NEdit {
39
39
  }
40
40
 
41
41
  export type HTEdit = {
42
- new_content: string;
43
- hash_bounds: [string, string];
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 hash_bounds of your next replace call.`
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 hash_bounds of your next replace call.`
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(["new_content", "hash_bounds"]);
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 { new_content, hash_bounds }.");
136
+ rejectUnknownFields(edit, ITEM_KS, "Edit", "The edit takes only { replacement_text, remove_from, remove_to }.");
144
137
 
145
- if ("hash_bounds" in edit && !isStrPair(edit.hash_bounds)) {
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 "hash_bounds" must be a pair of anchor strings [start, end].`,
145
+ `[E_BAD_SHAPE] Field "remove_to" must be an anchor string (3-char hash).`,
148
146
  );
149
147
  }
150
- if (!("new_content" in edit)) {
151
- throw new Error(`[E_BAD_SHAPE] The edit requires a "new_content" field. Provide the replacement text (use "" to delete).`);
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.new_content !== "string") {
151
+ if (typeof edit.replacement_text !== "string") {
154
152
  throw new Error(NEW_CONTENT_NOT_STRING_MSG);
155
153
  }
156
- if (!isStrPair(edit.hash_bounds)) {
154
+ if (typeof edit.remove_from !== "string" || typeof edit.remove_to !== "string") {
157
155
  throw new Error(
158
- `[E_BAD_SHAPE] The edit requires a "hash_bounds" pair of anchor strings [start, end].`,
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.new_content);
169
- const bounds = edit.hash_bounds.map((ref) => {
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 hash_bounds entry "${trimmed}".`;
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 hash_bounds entry "${trimmed}".`;
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 hash_bounds entry "${trimmed}".`;
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) => `new_content line ${s.lineIndex + 1}`)
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) => `new_content line ${i + 1}`).join(", ");
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: hash_bounds was reversed (start ${startRef.hash} is after end ${endRef.hash}); swapped the pair.`
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
  }
@@ -32,14 +32,19 @@ export function getPreviewInput(
32
32
  return null;
33
33
  }
34
34
 
35
- if (!Array.isArray(normalized.hash_bounds) || typeof normalized.new_content !== "string") {
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
- hash_bounds: normalized.hash_bounds as [string, string],
42
- new_content: normalized.new_content,
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 newContentSchema = Type.String({
49
+ const replacementTextSchema = Type.String({
50
50
  description:
51
- "Replacement content as a single string with \\n line separators; every \\n separates lines, so a trailing \\n adds a final empty line. Mirror the replaced range's 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."
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 hashBoundsSchema = Type.Array(
55
- Type.String({ description: "A 3-char HASH from read output" }),
56
- {
57
- description: "Pair of 3-char hashes from read output marking the first and last line of the range to replace (inclusive).",
58
- minItems: 2,
59
- maxItems: 2,
60
- },
61
- );
54
+ const removeFromSchema = Type.String({
55
+ description: "3-char HASH from read output marking the FIRST line to remove (inclusive)",
56
+ });
57
+
58
+ const removeToSchema = Type.String({
59
+ description: "3-char HASH from read output marking 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
- hash_bounds: hashBoundsSchema,
67
- new_content: newContentSchema,
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
- hash_bounds: [string, string];
74
- new_content: string;
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", "new_content", "hash_bounds"]);
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 (!Array.isArray(request.hash_bounds) || typeof request.new_content !== "string") {
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 both "hash_bounds" and "new_content" at the top level.',
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 bounds = request.hash_bounds;
131
- if (!Array.isArray(bounds) || bounds.length !== 2) return undefined;
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 bounds) {
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
- hash_bounds: params.hash_bounds,
219
- new_content: params.new_content,
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
  );