pi-hashline-edit-pro 4.4.0 → 4.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/package.json +1 -1
- package/prompts/insert.md +3 -1
- package/prompts/read.md +1 -1
- package/prompts/replace-guidelines.md +6 -4
- package/prompts/replace.md +5 -1
- package/src/anchor-registry.ts +18 -0
- package/src/auto-read-all.ts +1 -1
- package/src/edit-common.ts +19 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.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/prompts/insert.md
CHANGED
|
@@ -1 +1,3 @@
|
|
|
1
|
-
Insert lines after or before one existing line in a text file, addressed by a bare anchor from any served anchor│content row. The anchor line is preserved: `lines` go after it with `direction: "after"` or before it with `direction: "before"`, one string per line, no anchor prefixes, no embedded newlines. Inserted lines are written exactly as given; nothing else in the file changes.
|
|
1
|
+
Insert lines after or before one existing line in a text file, addressed by a bare anchor from any served anchor│content row. The anchor line is preserved: `lines` go after it with `direction: "after"` or before it with `direction: "before"`, one string per line, no anchor prefixes, no embedded newlines. Inserted lines are written exactly as given; nothing else in the file changes.
|
|
2
|
+
|
|
3
|
+
Same-file calls in one message batch: earlier calls reply `In batch N` and the last call shows the combined diff, with one undo for the whole batch.
|
package/prompts/read.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Read a text file and return it as `anchor│content` rows, one per line: a 4-character anchor (letters only), the `│` separator, then the line content. Target lines in replace and insert by anchor, never by content or line number. Page long files with `offset` and `limit`; when the output says truncated, continue with the hinted `offset/limit`. Images attach visually; binary, directory, and UTF-16/UTF-32 text are rejected; an empty file returns one empty row you can replace to seed content.
|
|
1
|
+
Read a text file and return it as `anchor│content` rows, one per line: a 4-character anchor (letters only, case-sensitive), the `│` separator, then the line content. Target lines in replace and insert by anchor, never by content or line number. Page long files with `offset` and `limit`; when the output says truncated, continue with the hinted `offset/limit`. Images attach visually; binary, directory, and UTF-16/UTF-32 text are rejected; an empty file returns one empty row you can replace to seed content.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
- `replace`: edit
|
|
2
|
-
- `replace`:
|
|
3
|
-
- `replace`:
|
|
4
|
-
- `replace`:
|
|
1
|
+
- `replace`: `-anchor│` rows in a post-edit diff are dead anchors; only `+anchor│` and ` anchor│` rows are live. Check the post-edit diff before the next turn's edits on that file.
|
|
2
|
+
- `replace`: same-file same-message calls batch: disjoint ranges, one undo. A call whose anchors resolve nowhere fails alone; its file's batch still commits.
|
|
3
|
+
- `replace`: `[E_STALE_ANCHOR]` means the anchor is not owned in this session: call `read` on the file for fresh anchors. `[E_RANGE_STALE]` means a line in the range changed since it was shown, or was never shown: retry with the fresh anchors the error returns.
|
|
4
|
+
- `replace`: `[E_BATCH_OVERLAP]` means two batch calls share lines: retry with disjoint ranges. `[E_OP_ABORTED]` means another batch call failed or the file changed: fix that call, then retry, or call `read` for fresh anchors.
|
|
5
|
+
- `replace`: use `replace`/`insert`, not `sed -i` or heredocs — anchor edits are verified against what was shown and undoable.
|
|
6
|
+
- `replace`: `replacement_lines`: one string per line, `[""]` is one blank line, `[]` deletes. Pasted `anchor│` prefixes are stripped. Single line: same anchor for `remove_from` and `remove_to`.
|
package/prompts/replace.md
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
Replace a range of lines (or a single line) in a text file, targeted by 4-character anchors from any served anchor│content row. Give `remove_from` and `remove_to` as bare anchors marking the first and last line to remove, and `replacement_lines` as one string per new line with no anchor prefixes and no embedded newlines; `[]` deletes the range.
|
|
1
|
+
Replace a range of lines (or a single line) in a text file, targeted by 4-character anchors from any served anchor│content row. Give `remove_from` and `remove_to` as bare anchors marking the first and last line to remove, and `replacement_lines` as one string per new line with no anchor prefixes and no embedded newlines; `[]` deletes the range.
|
|
2
|
+
|
|
3
|
+
Same-file calls in one message batch: earlier calls reply `In batch N` and the last call shows the combined diff, with one undo for the whole batch.
|
|
4
|
+
|
|
5
|
+
Example: read served `Hasu│old` and `arvm│old2`. Call { "remove_from": "Hasu", "remove_to": "arvm", "replacement_lines": ["new"] }. The post-edit diff shows `-Hasu│old`, `-arvm│old2`, `+Qwer│new`: the `-` rows are dead anchors now; the `+` and ` ` rows are live anchors for the next edit.
|
package/src/anchor-registry.ts
CHANGED
|
@@ -568,6 +568,24 @@ export function ownerOf(anchor: string): OwnedAnchor | undefined {
|
|
|
568
568
|
return current()?.owned.get(anchor);
|
|
569
569
|
}
|
|
570
570
|
|
|
571
|
+
export interface CaseFoldedOwner {
|
|
572
|
+
anchor: string;
|
|
573
|
+
path: string;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export function ownersDifferingOnlyByCase(anchor: string, paths?: ReadonlySet<string>): CaseFoldedOwner[] {
|
|
577
|
+
const state = current();
|
|
578
|
+
if (!state) return [];
|
|
579
|
+
const lower = anchor.toLowerCase();
|
|
580
|
+
const matches: CaseFoldedOwner[] = [];
|
|
581
|
+
for (const [owned, entry] of state.owned) {
|
|
582
|
+
if (owned === anchor || owned.toLowerCase() !== lower) continue;
|
|
583
|
+
if (paths && paths.size > 0 && !paths.has(entry.path)) continue;
|
|
584
|
+
matches.push({ anchor: owned, path: entry.path });
|
|
585
|
+
}
|
|
586
|
+
return matches;
|
|
587
|
+
}
|
|
588
|
+
|
|
571
589
|
export function ownersForPath(path: string): Map<string, string> {
|
|
572
590
|
const claims = new Map<string, string>();
|
|
573
591
|
const state = current();
|
package/src/auto-read-all.ts
CHANGED
|
@@ -27,7 +27,7 @@ const MAX_REPORTED_OMISSIONS = 50;
|
|
|
27
27
|
export const AUTO_READ_ALL_CHUNK_BYTES = 48 * 1024;
|
|
28
28
|
|
|
29
29
|
const HEADER =
|
|
30
|
-
"[hashline auto-read-all] Every non-ignored project file is attached below with live
|
|
30
|
+
"[hashline auto-read-all] Every non-ignored project file is attached below as `=== path ===` then `anchor│content` rows with live anchors.\nEdit directly from the attachment with replace and insert; do not call read for files in [files complete: ...].\nFiles listed as omitted or not attached can be read normally.\nAnchors are case-sensitive and stay valid until their line is edited.";
|
|
31
31
|
|
|
32
32
|
const IMAGE_EXTENSIONS = new Set([
|
|
33
33
|
".avif",
|
package/src/edit-common.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { resolveInCwd } from "./fs-write";
|
|
3
3
|
import { abortIf, makePrepareArguments } from "./utils";
|
|
4
|
-
import { ownerOf } from "./anchor-registry";
|
|
4
|
+
import { ownerOf, ownersDifferingOnlyByCase, type OwnedAnchor } from "./anchor-registry";
|
|
5
5
|
import { parseHashRef, stripAnchorRow } from "./hashline";
|
|
6
6
|
import { readConfig } from "./config";
|
|
7
7
|
import { makeRenderCall, renderEditResult, type RPreview, type FgT } from "./replace-render";
|
|
@@ -37,7 +37,7 @@ export function withReplacePrompts(base: { description: string; snippet: string;
|
|
|
37
37
|
const snippetParts = [base.snippet];
|
|
38
38
|
let guidelines = [...base.guidelines];
|
|
39
39
|
if (!flags.autoRead) {
|
|
40
|
-
description = description.replace(
|
|
40
|
+
description = description.replace(/\n\nExample:[\s\S]*$/, "");
|
|
41
41
|
guidelines = guidelines.filter((guideline) => !guideline.includes("post-edit diff"));
|
|
42
42
|
description = description.replace("and the last call shows the combined diff,", "and the last call shows the combined result,");
|
|
43
43
|
}
|
|
@@ -95,14 +95,27 @@ export function withUndoPrompts(base: { description: string; snippet: string; gu
|
|
|
95
95
|
const guidelines = base.guidelines.map((guideline) => guideline.includes("bad diff") ? "`undo_last_change`: only the last `replace`/`insert` per file is undoable; a `write` clears it, so undo right after a bad edit — review what you're restoring." : guideline);
|
|
96
96
|
return { description: base.description, snippet: base.snippet, guidelines };
|
|
97
97
|
}
|
|
98
|
+
|
|
99
|
+
function staleAnchorMessage(ref: string, hash: string, owners: Array<OwnedAnchor | undefined>): string {
|
|
100
|
+
const knownPaths = new Set<string>();
|
|
101
|
+
for (const owner of owners) {
|
|
102
|
+
if (owner) knownPaths.add(owner.path);
|
|
103
|
+
}
|
|
104
|
+
const folded = ownersDifferingOnlyByCase(hash, knownPaths);
|
|
105
|
+
const hint =
|
|
106
|
+
folded.length > 0
|
|
107
|
+
? ` Anchors are case-sensitive; ${folded.map((match) => `"${match.anchor}"`).join(", ")} differs only in case.`
|
|
108
|
+
: "";
|
|
109
|
+
return `[E_STALE_ANCHOR] "${ref}" is not owned in this session.${hint} Call read() on the target file first.`;
|
|
110
|
+
}
|
|
111
|
+
|
|
98
112
|
export function resolveEditTarget(removeFrom: string, removeTo?: string): string {
|
|
99
113
|
const refs = [removeFrom, removeTo].filter((value): value is string => typeof value === "string");
|
|
100
|
-
const
|
|
114
|
+
const hashes = refs.map((ref) => parseHashRef(stripAnchorRow(ref.trim(), "anchor entry")).hash);
|
|
115
|
+
const owners = hashes.map((hash) => ownerOf(hash));
|
|
101
116
|
const missing = owners.findIndex((owner) => !owner);
|
|
102
117
|
if (missing >= 0) {
|
|
103
|
-
throw new Error(
|
|
104
|
-
`[E_STALE_ANCHOR] "${refs[missing]!}" is not owned in this session. Call read() on the target file first.`,
|
|
105
|
-
);
|
|
118
|
+
throw new Error(staleAnchorMessage(refs[missing]!, hashes[missing]!, owners));
|
|
106
119
|
}
|
|
107
120
|
const paths = new Set(owners.map((owner) => owner!.path));
|
|
108
121
|
if (paths.size > 1) {
|