pi-hashline-edit-pro 6.5.0 → 6.5.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 +0 -4
- package/package.json +1 -1
- package/src/edit-common.ts +3 -6
- package/src/edit-fidelity.ts +19 -0
- package/src/grep.ts +1 -1
- package/src/insert.ts +2 -7
- package/src/payload-contract.ts +3 -7
- package/src/read.ts +1 -1
- package/src/replace-response.ts +5 -4
- package/tool-prompts/copy.md +1 -1
- package/tool-prompts/grep.md +1 -1
- package/tool-prompts/insert.md +1 -1
- package/tool-prompts/move.md +1 -1
- package/tool-prompts/replace-match.md +1 -1
- package/tool-prompts/replace.md +1 -1
- package/tool-prompts/transfer-guidelines.md +1 -1
package/README.md
CHANGED
|
@@ -190,8 +190,6 @@ Edge cases:
|
|
|
190
190
|
| `remove_to` | 4-char anchor marking the LAST line to remove (inclusive). |
|
|
191
191
|
| `text` | The exact text to write in place of the removed range, as one string: `""` deletes the range. A trailing line break sets the newline of the last line written instead of adding a blank line; a lone newline writes one blank line. Embedded `\r\n`/`\r`/`\n` are preserved; escapes in the tool call are decoded once when the call is parsed, and the tool then writes the string it receives as-is, never decoding again — `\uXXXX` is the character, `\\uXXXX` the literal text. Legacy arrays are converted to text (elements joined with LF); prefer the string form. |
|
|
192
192
|
|
|
193
|
-
`replace_from` and `replace_to` are accepted as aliases for `remove_from` and `remove_to`.
|
|
194
|
-
|
|
195
193
|
Example: read showed `Hasu│old` and `arvm│old2`; to replace both:
|
|
196
194
|
|
|
197
195
|
```json
|
|
@@ -221,8 +219,6 @@ After a successful edit, the diff is capped at 50KB. A row over 50KB is shown as
|
|
|
221
219
|
|
|
222
220
|
`replace_match` changes part of a line — or the same fragment on several lines of a range — without retyping the rest. `replace_from` and `replace_to` are bare anchors from served `anchor│content` rows marking the first and last line of the range searched (the same anchor for a single line). `match` is the exact text to find inside that range, and `replacement` replaces every occurrence of it; every other character stays untouched. Both stay within one line: a single trailing line break is accepted as the line's own terminator and dropped, so the file keeps its separator, while a line break anywhere else is refused with `[E_BAD_SHAPE]` (a `match` that is only that trailing line break leaves nothing to find and is refused; in `replacement` it counts as `""` and deletes the matches). Send `match` as only the fragment that changes — never include text that `replacement` leaves unchanged — and use `replace` with the same anchors when the change covers whole lines; if `match` is an entire single-line range except indentation and trailing punctuation, that is a whole-line change. The anchors bound the search, so `match` only needs to be unique inside the selected range, not file-wide. That makes it the tool for a change the request quotes as a substring: a whole-line `replace` has to reproduce the rest of the lines, so a slipped character becomes a wrong byte, while `replace_match` leaves everything the request did not name untouched. It is enabled by default; turn Replace match off in `/hashline-config` to remove the tool.
|
|
223
221
|
|
|
224
|
-
`remove_from` and `remove_to` are accepted as aliases for `replace_from` and `replace_to`.
|
|
225
|
-
|
|
226
222
|
`match` is matched against each selected line's text (LF line breaks) and every non-overlapping occurrence is replaced, left to right — since `match` stays within one line, so does each replacement. A missing match is refused with `[E_SUBSTRING_NOT_FOUND]` and the current `anchor│content` rows, so the retry needs no `read`. The two boundary anchors are verified against what was last shown; lines strictly inside the range are searched as they currently stand on disk.
|
|
227
223
|
|
|
228
224
|
In a same-message batch it joins the other calls on its file: the batch validates everything against the pre-batch state and commits once, with one undo. A missing `match` fails that call on its own and is left out of the batch; the rest of the batch still commits. The post-edit diff carries fresh anchors, and the edit is undoable with `undo_last_change`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "6.5.
|
|
3
|
+
"version": "6.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hash-anchored read/replace/insert/grep tools for pi-coding-agent. Every line gets a unique 4-char tokenizer-friendly anchor that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
|
|
6
6
|
"main": "index.ts",
|
package/src/edit-common.ts
CHANGED
|
@@ -80,7 +80,6 @@ const UNDO_WRITE_CLEARS_ID = "undo-write-clears";
|
|
|
80
80
|
const UNDO_CROSS_FILE_MOVE_ID = "undo-cross-file-move";
|
|
81
81
|
const NEUTRAL_READ_REREAD = "`read`: call again after an edit when you need anchors you lack.";
|
|
82
82
|
const NEUTRAL_UNDO_WRITE_CLEARS = "`undo_last_change`: only the last `replace`/`replace_match`/`insert`/`copy`/`move` per file is undoable; a `write` clears it, so undo before any other edit or write.";
|
|
83
|
-
const GREP_OPS_PHRASE = "replace, insert, copy, or move";
|
|
84
83
|
const UNDO_OPS_PHRASE = "replace, replace_match, insert, copy, or move";
|
|
85
84
|
const UNDO_GUIDELINE_OPS_PHRASE = "`replace`/`replace_match`/`insert`/`copy`/`move`";
|
|
86
85
|
|
|
@@ -92,12 +91,12 @@ function batchGuideline(flags: EditToolFlags): string {
|
|
|
92
91
|
const tools = operationNames(SHARED_EDIT_OPS, flags);
|
|
93
92
|
const outcome = flags.autoRead ? "diff" : "result";
|
|
94
93
|
const script = flags.codemode ? ` ${SCRIPT_BATCH_GUIDELINE}` : "";
|
|
95
|
-
return `${tools}: same-file calls in one message commit as one batch;
|
|
94
|
+
return `${tools}: same-file calls in one message commit as one batch. If you already know all the edits and their anchors, send them together instead of one turn at a time; an edit that needs anchors from a previous result waits for the next turn. Earlier calls reply \`In batch N (queued)\`, the last shows the combined ${outcome}, and one undo reverts all. A request-invalid call (anchors, shape, or \`match\`) fails alone; a failure while the batch runs aborts it.${script}`;
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
function diffGuideline(flags: EditToolFlags): string {
|
|
99
98
|
const tools = operationNames(SHARED_DIFF_OPS, flags);
|
|
100
|
-
return `${tools}: in the post-edit diff, \`-anchor│\` rows are dead anchors; \`+anchor│\` and \` anchor│\` rows carry live anchors for the next edit.`;
|
|
99
|
+
return `${tools}: in the post-edit diff, \`-anchor│\` rows are dead anchors; \`+anchor│\` and \` anchor│\` rows carry live anchors for the next edit. Rows the diff does not show keep their anchors, so a follow-up edit on them needs no re-read.`;
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
function pathGuideline(flags: EditToolFlags): string {
|
|
@@ -181,9 +180,7 @@ function joinOps(ops: string[], options?: { backtick?: boolean; separator?: "/"
|
|
|
181
180
|
export function withGrepPrompts(base: { description: string; snippet: string; guidelines: Guideline[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
|
182
181
|
const guidelines = base.guidelines.map((guideline) => guideline.text);
|
|
183
182
|
if (flags.codemode) guidelines.push(GREP_SCRIPT_GUIDELINE);
|
|
184
|
-
|
|
185
|
-
const description = base.description.replaceAll(GREP_OPS_PHRASE, joinOps(gatedEditOps(["replace", "insert", "copy", "move"], flags)));
|
|
186
|
-
return { description, snippet: base.snippet, guidelines };
|
|
183
|
+
return { description: base.description, snippet: base.snippet, guidelines };
|
|
187
184
|
}
|
|
188
185
|
|
|
189
186
|
export function withUndoPrompts(base: { description: string; snippet: string; guidelines: Guideline[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
package/src/edit-fidelity.ts
CHANGED
|
@@ -396,6 +396,25 @@ export function annotateLiteralEscapeHints(
|
|
|
396
396
|
});
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
+
const LITERAL_ESCAPE_DETAIL_RE = /^\[H_LITERAL_ESCAPE\] [^:]*: "(.*?)" written as literal text(?: \((.*)\))?\.?$/;
|
|
400
|
+
const LITERAL_ESCAPE_ANCHOR_RE = /([A-Za-z]{4})│/g;
|
|
401
|
+
const UNICODE_LOST_DETAIL_RE = /^\[H_UNICODE_LOST\] ([A-Za-z]{4})│ missing "([^"]*)"/;
|
|
402
|
+
|
|
403
|
+
export function dedupeLiteralEscapeHints(hints: readonly string[]): string[] {
|
|
404
|
+
const suppressed = new Set<string>();
|
|
405
|
+
for (const hint of hints) {
|
|
406
|
+
const literal = LITERAL_ESCAPE_DETAIL_RE.exec(hint);
|
|
407
|
+
if (literal === null) continue;
|
|
408
|
+
const escape = literal[1] ?? "";
|
|
409
|
+
for (const anchor of (literal[2] ?? "").matchAll(LITERAL_ESCAPE_ANCHOR_RE)) suppressed.add(`${anchor[1]}\0${escape}`);
|
|
410
|
+
}
|
|
411
|
+
if (suppressed.size === 0) return [...hints];
|
|
412
|
+
return hints.filter((hint) => {
|
|
413
|
+
const lost = UNICODE_LOST_DETAIL_RE.exec(hint);
|
|
414
|
+
return lost === null || !suppressed.has(`${lost[1]}\0${lost[2]}`);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
399
418
|
function separatorLostHint(oldLines: string[], span: DiffSpan, originalHashes?: readonly string[]): string | undefined {
|
|
400
419
|
if (span.replacementCount !== 0 || span.end < span.start) return undefined;
|
|
401
420
|
const beforeIndex = span.start - 1;
|
package/src/grep.ts
CHANGED
|
@@ -393,7 +393,7 @@ const grepToolSchema = Type.Object(
|
|
|
393
393
|
limit: Type.Optional(
|
|
394
394
|
Type.Integer({
|
|
395
395
|
minimum: 1,
|
|
396
|
-
description: "Maximum number of matched lines to return",
|
|
396
|
+
description: "Maximum number of matched lines to return (default 100)",
|
|
397
397
|
}),
|
|
398
398
|
),
|
|
399
399
|
},
|
package/src/insert.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { LineEnding } from "./normalize";
|
|
|
11
11
|
import { stripAnchorRow } from "./hashline/resolve";
|
|
12
12
|
import { withAnchorSession } from "./anchor-registry";
|
|
13
13
|
import { loadP, loadGuidelines } from "./prompts";
|
|
14
|
-
import { assertInsertReq, normReq, type InsertReq } from "./payload-contract";
|
|
14
|
+
import { assertInsertReq, normReq, pathRequiredSchema, type InsertReq } from "./payload-contract";
|
|
15
15
|
import { anchorCarryIndex, isRec, literalEscapeHints, splitLines } from "./utils";
|
|
16
16
|
import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper, resolveEditTargetWithRequirement, throwIfStrictInput, withInsertPrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
|
|
17
17
|
import type { RPreview, RRState } from "./replace-render";
|
|
@@ -32,11 +32,6 @@ const insertTextSchema = Type.String({
|
|
|
32
32
|
`The exact text to insert; an empty string inserts one blank line. ${TEXT_ENDING_NOTE} Never include the anchor line.`,
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
const insertPathRequiredSchema = Type.String({
|
|
36
|
-
description:
|
|
37
|
-
"Path to the file the anchor was served for; required and must match anchor ownership.",
|
|
38
|
-
});
|
|
39
|
-
|
|
40
35
|
const insertToolSchema = Type.Object(
|
|
41
36
|
{
|
|
42
37
|
anchor: insertAnchorSchema,
|
|
@@ -50,7 +45,7 @@ export function buildInsertToolSchema(requirePath: boolean): typeof insertToolSc
|
|
|
50
45
|
if (!requirePath) return insertToolSchema;
|
|
51
46
|
return Type.Object(
|
|
52
47
|
{
|
|
53
|
-
path:
|
|
48
|
+
path: pathRequiredSchema,
|
|
54
49
|
anchor: insertAnchorSchema,
|
|
55
50
|
direction: insertDirectionSchema,
|
|
56
51
|
text: insertTextSchema,
|
package/src/payload-contract.ts
CHANGED
|
@@ -17,7 +17,7 @@ const removeToSchema = Type.String({
|
|
|
17
17
|
description:
|
|
18
18
|
"4-char anchor of the LAST line to remove.",
|
|
19
19
|
});
|
|
20
|
-
const pathRequiredSchema = Type.String({
|
|
20
|
+
export const pathRequiredSchema = Type.String({
|
|
21
21
|
description:
|
|
22
22
|
"Path to the file the anchors were served for; required and must match anchor ownership.",
|
|
23
23
|
});
|
|
@@ -253,16 +253,12 @@ const replaceMatchToSchema = Type.String({
|
|
|
253
253
|
});
|
|
254
254
|
const replaceMatchMatchSchema = Type.String({
|
|
255
255
|
description:
|
|
256
|
-
"The exact text to find inside the selected lines, copied from the served row: only the fragment that changes, never text that `replacement` leaves unchanged. It must stay within one line and hold text: a single trailing line break is accepted as the line's own terminator, a line break anywhere else is refused, and a value that is only a line break leaves nothing to find and is refused. Every occurrence inside the range is replaced; the text around each match is left untouched.
|
|
256
|
+
"The exact text to find inside the selected lines, copied from the served row: only the fragment that changes, never text that `replacement` leaves unchanged. It must stay within one line and hold text: a single trailing line break is accepted as the line's own terminator, a line break anywhere else is refused, and a value that is only a line break leaves nothing to find and is refused. Every occurrence inside the range is replaced; the text around each match is left untouched.",
|
|
257
257
|
});
|
|
258
258
|
const replaceMatchReplacementSchema = Type.String({
|
|
259
259
|
description:
|
|
260
260
|
`The exact replacement for every matched occurrence within the range. "" deletes the matches. It must stay within one line: a single trailing line break is accepted and dropped (a value that is only a line break counts as ""), and a line break anywhere else is refused. The rest of each line is kept byte-for-byte.`,
|
|
261
261
|
});
|
|
262
|
-
const replaceMatchPathRequiredSchema = Type.String({
|
|
263
|
-
description:
|
|
264
|
-
"Path to the file the anchors were served for; required and must match anchor ownership.",
|
|
265
|
-
});
|
|
266
262
|
|
|
267
263
|
export const replaceMatchToolSchema = Type.Object(
|
|
268
264
|
{
|
|
@@ -278,7 +274,7 @@ export function buildReplaceMatchToolSchema(requirePath: boolean): typeof replac
|
|
|
278
274
|
if (!requirePath) return replaceMatchToolSchema;
|
|
279
275
|
return Type.Object(
|
|
280
276
|
{
|
|
281
|
-
path:
|
|
277
|
+
path: pathRequiredSchema,
|
|
282
278
|
replace_from: replaceMatchFromSchema,
|
|
283
279
|
replace_to: replaceMatchToSchema,
|
|
284
280
|
match: replaceMatchMatchSchema,
|
package/src/read.ts
CHANGED
|
@@ -320,7 +320,7 @@ function buildReadToolSchema(codemode: boolean) {
|
|
|
320
320
|
limit: Type.Optional(
|
|
321
321
|
Type.Integer({
|
|
322
322
|
minimum: 1,
|
|
323
|
-
description: "Maximum number of lines to read from offset",
|
|
323
|
+
description: "Maximum number of lines to read from offset; counted from the anchor row when `offset` is an anchor",
|
|
324
324
|
}),
|
|
325
325
|
),
|
|
326
326
|
...(codemode
|
package/src/replace-response.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { HASH_SEP, type NEdit } from "./hashline";
|
|
|
2
2
|
import type { ReplaceDetails } from "./replace";
|
|
3
3
|
import { genDiff, genPatch, type DiffSpan } from "./replace-diff";
|
|
4
4
|
import { visLines, clipLine } from "./utils";
|
|
5
|
-
import { annotateLiteralEscapeHints, fidelityHints } from "./edit-fidelity";
|
|
5
|
+
import { annotateLiteralEscapeHints, dedupeLiteralEscapeHints, fidelityHints } from "./edit-fidelity";
|
|
6
6
|
import { anchoredLine, anchoredLinesFromDiff, diffAnchorsOmitted, toEditVerb, type EditStructured, type EditVerb, type Json } from "./structured";
|
|
7
7
|
|
|
8
8
|
export type TResult = {
|
|
@@ -164,7 +164,8 @@ export function buildChanged(input: SuccessInput, verb = "replaced", diffContext
|
|
|
164
164
|
});
|
|
165
165
|
const { warnings: noticeWarnings, hints } = splitNotices(fidelity.length > 0 ? [...(warnings ?? []), ...fidelity] : warnings);
|
|
166
166
|
const annotatedHints = annotateLiteralEscapeHints(hints, result, spans, resultHashes);
|
|
167
|
-
const
|
|
167
|
+
const dedupedHints = dedupeLiteralEscapeHints(annotatedHints);
|
|
168
|
+
const noticesBlock = `${warnBlock(noticeWarnings)}${hintBlock(dedupedHints)}`;
|
|
168
169
|
const successPrefix = `Successfully ${verb} in ${path}.`;
|
|
169
170
|
const lineSummary = addedLines > 0 || removedLines > 0
|
|
170
171
|
? ` Added ${addedLines} line(s), removed ${removedLines} line(s).`
|
|
@@ -203,7 +204,7 @@ export function buildChanged(input: SuccessInput, verb = "replaced", diffContext
|
|
|
203
204
|
text,
|
|
204
205
|
diff: diffResult.diff,
|
|
205
206
|
warnings: [...noticeWarnings],
|
|
206
|
-
hints: [...
|
|
207
|
+
hints: [...dedupedHints],
|
|
207
208
|
firstChangedLine: editMeta.firstChangedLine ?? diffResult.firstChangedLine ?? null,
|
|
208
209
|
anchors,
|
|
209
210
|
anchorsOmitted,
|
|
@@ -220,7 +221,7 @@ export function buildChanged(input: SuccessInput, verb = "replaced", diffContext
|
|
|
220
221
|
metrics,
|
|
221
222
|
diffLineNumbers: diffResult.lineNumbers.map((line) => line ?? null),
|
|
222
223
|
...(noticeWarnings.length ? { warnings: [...noticeWarnings] } : {}),
|
|
223
|
-
...(
|
|
224
|
+
...(dedupedHints.length ? { hints: [...dedupedHints] } : {}),
|
|
224
225
|
},
|
|
225
226
|
structuredContent,
|
|
226
227
|
};
|
package/tool-prompts/copy.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Duplicate a range of lines at another anchor
|
|
1
|
+
Duplicate a range of lines at another anchor. The source rows and anchors stay live; the copied lines get fresh anchors in the destination's post-edit diff and keep their source line endings.
|
|
2
2
|
|
|
3
3
|
A copy duplicates the content its source anchors were served from, so a same-message edit to the source file does not change what is copied.
|
package/tool-prompts/grep.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Search text files with ripgrep. Hits come back as `lineNumber:│ anchor│content` rows and context lines as `lineNumber-│ anchor│content`, editable with replace, insert, copy, or move without a new read
|
|
1
|
+
Search text files with ripgrep. Hits come back as `lineNumber:│ anchor│content` rows and context lines as `lineNumber-│ anchor│content`, editable with replace, insert, copy, or move without a new read. A row returned by `grep` is already a served row, so you do not need to `read` the file to edit that line. The `=== path ===` header and line numbers locate the match. Patterns use ripgrep's Rust regex syntax (no backreferences or look-around) through a linear-time engine. Searches respect `.gitignore`, always skip `.git`, and skip binary and image files silently. A match over 500 bytes is shown as a `...` fragment around the hit, but its anchor still covers the whole line. When output is cut by the match limit, raise `limit`; when cut by rows or bytes, refine `pattern` or narrow `path`.
|
package/tool-prompts/insert.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Insert text
|
|
1
|
+
Insert text next to an existing line without removing anything. The anchor line stays in place, and `anchor` is a bare 4-character anchor from any served `anchor│content` row.
|
package/tool-prompts/move.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Relocate a range of lines to another anchor
|
|
1
|
+
Relocate a range of lines to another anchor. Within one file the destination must sit outside the source range; moving a range onto itself reports `No changes made` and leaves the anchors alone. Rows between the source and the destination keep their content but may be re-anchored, so read that span again if you plan to edit it.
|
|
2
2
|
|
|
3
3
|
A cross-file move commits on its own when its source file also has batched edits in the message.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Replace part of a line — or the same fragment on several lines of a range — without retyping the rest. `
|
|
1
|
+
Replace part of a line — or the same fragment on several lines of a range — without retyping the rest. `match` is the exact text to find inside the anchored range, and `replacement` replaces every occurrence; the text around each match is left untouched. Send only the fragment that changes — never text that `replacement` leaves unchanged — and narrow `replace_from`/`replace_to` when only one occurrence should change. `match` only needs to be unique inside the anchored range, not file-wide. For a whole-line rewrite, use `replace` with the same anchors.
|
|
2
2
|
|
|
3
3
|
The two boundary anchors must still match what was last shown; lines strictly inside the range are searched as they are on disk.
|
package/tool-prompts/replace.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Replace a range of lines (or a single line) in a text file by anchor
|
|
1
|
+
Replace a range of lines (or a single line) in a text file by anchor. Deleting every line empties the file, and the result names the empty-line anchor, so a follow-up `replace` can seed content without a `read`.
|
|
2
2
|
|
|
3
3
|
To change only part of a line without retyping the rest, use `replace_match` instead; it preserves every character the request does not name.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
- `copy`/`move`: only the
|
|
1
|
+
- `copy`/`move`: only the first and last marker rows and the destination line need anchors — grep the markers, the body is copied; to append to the end of a file, anchor that file's last line with `direction: "after"`.
|
|
2
2
|
- `copy`/`move`: within one file the insertion point must fall before the source range's first line or after its last; anchoring the block's last line with `direction: "after"` (or its first line with `direction: "before"`) targets the gap right beside the block.
|
|
3
3
|
- `copy`/`move`: `source_from` and `source_to` may live in a different file than `anchor`; an empty destination file is seeded with the copied or moved lines.
|
|
4
4
|
- `move`: a batched cross-file move does not display the source file's diff; its removal commits with the batch, and the source file needs a read for fresh anchors.
|