pi-hashline-edit-pro 4.0.1 → 4.2.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 +17 -12
- package/index.ts +70 -23
- package/package.json +1 -1
- package/prompts/insert-guidelines.md +2 -1
- package/prompts/insert.md +1 -1
- package/prompts/read-guidelines.md +1 -0
- package/prompts/read.md +1 -1
- package/prompts/replace-guidelines.md +5 -5
- package/prompts/replace.md +1 -1
- package/src/anchor-registry.ts +11 -8
- package/src/batch.ts +614 -0
- package/src/boundary-bypass.ts +6 -0
- package/src/commit.ts +1 -1
- package/src/config-ui.ts +108 -0
- package/src/config.ts +48 -1
- package/src/edit-common.ts +115 -1
- package/src/fs-write.ts +6 -0
- package/src/grep.ts +84 -1
- package/src/hashline/alphabet.ts +1 -1
- package/src/hashline/apply.ts +128 -84
- package/src/hashline/index.ts +3 -0
- package/src/hashline/resolve.ts +5 -4
- package/src/insert.ts +127 -74
- package/src/payload-contract.ts +50 -2
- package/src/read.ts +10 -11
- package/src/replace-render.ts +2 -2
- package/src/replace-response.ts +3 -1
- package/src/replace.ts +94 -37
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Replace a line by its anchor:
|
|
|
40
40
|
|
|
41
41
|
The result is the post-edit diff with fresh anchors, so you can keep editing without re-reading. Lines you did not touch keep their anchors. After a `write`, an auto-read block gives you the new anchors. The most recent `replace` or `insert` on a file can be reverted, even after a restart.
|
|
42
42
|
|
|
43
|
-
The extension registers five tools: `read`, `replace`, `insert`, `anchor_grep`, and `undo_last_change`. The built-in `edit` tool is disabled. `replace` and `insert` take no `path` parameter: the file is resolved from the anchors' session ownership alone, so an edit can only land on the file the anchors were served for.
|
|
43
|
+
The extension registers five tools: `read`, `replace`, `insert`, `anchor_grep`, and `undo_last_change`. The built-in `edit` tool is disabled. `replace` and `insert` take no `path` parameter by default: the file is resolved from the anchors' session ownership alone, so an edit can only land on the file the anchors were served for. Opt in with `/hashline-config` to require `path` in `replace` and `insert` for RPC visibility (for example pimacs.el); anchors still resolve the target and `path` must match.
|
|
44
44
|
|
|
45
45
|
### read
|
|
46
46
|
|
|
@@ -89,7 +89,7 @@ Single line: use the same anchor for `remove_from` and `remove_to`. `replace_fro
|
|
|
89
89
|
|
|
90
90
|
The request is checked before any file I/O, so a bad request never touches the file.
|
|
91
91
|
|
|
92
|
-
Common copy-paste slips are fixed automatically and reported as warnings: a leftover `anchor│` prefix in `replacement_lines` or the anchor fields (
|
|
92
|
+
Common copy-paste slips are fixed automatically and reported as warnings: a leftover `anchor│` prefix in `replacement_lines` or the anchor fields (a prefix of 4 to 5 characters before `│`, for example `ab12│`), diff-preview rows pasted into the replacement, a reversed range, and a boundary line pasted twice. New lines that re-include a block adjacent to the range are stripped when that block is unique in the file. The whole run is stripped as one unit, so re-including an unchanged block next to the range never duplicates it. Boundary dedup has three modes in `/hashline-config`: `on` strips with a warning, `off` applies edits literally, and `strict` rejects the edit with `[E_BOUNDARY_STRICT]` when any replacement line would be stripped.
|
|
93
93
|
|
|
94
94
|
Every line in the removed range must match what was last shown to you. The extension records the `anchor│content` rows it serves (`read` output, `anchor_grep` output, the auto-read block after `write`, the `+anchor│` and ` anchor│` rows of post-edit diffs, the current-range rows of `[E_RANGE_STALE]` feedback, and the context rows of stale-anchor feedback) and verifies the whole range against that record before writing. A line that changed on disk since it was shown, or an anchor that is not owned in this session, refuses the edit with `[E_RANGE_STALE]` or `[E_STALE_ANCHOR]` and returns the current range with fresh anchors, so the retry needs no `read`. An owned anchor enters the served record when its row is shown (after a restart, restored ownership counts as shown), so a file with no owned anchors cannot be edited by anchor at all; call `read` first. An owned line that was never shown — for example beyond an auto-read preview's truncation cap — is refused with `[E_RANGE_STALE]` and returns the current range, so the retry still needs no `read`.
|
|
95
95
|
|
|
@@ -97,7 +97,7 @@ An edit that produces identical content reports `No changes made` and leaves the
|
|
|
97
97
|
|
|
98
98
|
After a successful edit, the diff is capped at 50KB. A row over 50KB is shown as a marker that keeps the row's anchor, and only the rows shown in the capped diff are recorded as served. The same caps apply to the `insert` and `undo_last_change` diffs, to the interactive previews, and to `details.patch`.
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
Multiple `replace` and `insert` calls on the same file in one message are grouped per file into one batch that validates every call against the pre-batch state and then applies them together on the batch's last call: earlier calls reply `In batch` (`In batch N` when several files batch) and the batch's last call shows the combined diff, with one undo reverting the whole batch. Batched calls must target disjoint ranges; overlapping ranges, or any failing call, aborts the whole batch with nothing written. Verify each batch diff before the next turn's edits on that file.
|
|
101
101
|
|
|
102
102
|
### insert
|
|
103
103
|
|
|
@@ -115,7 +115,7 @@ The same safety machinery as `replace` applies: undo is saved before the write (
|
|
|
115
115
|
|
|
116
116
|
### anchor_grep
|
|
117
117
|
|
|
118
|
-
`anchor_grep` is an anchored search backed by ripgrep. It is
|
|
118
|
+
`anchor_grep` is an anchored search backed by ripgrep. It is enabled by default; disable it in `/hashline-config` (or set `anchorGrepEnabled` to `false` in the config file). While it is enabled, the built-in grep is disabled. Disabling it removes the tool and restores the built-in grep only if that was active before the extension loaded.
|
|
119
119
|
|
|
120
120
|
Every matching line, and each requested context line, is returned as `lineNumber │ anchor│content`. The `anchor│content` part is served exactly like `read` output, so you can target it with `replace` or `insert` without a separate `read`; the line-number gutter and `=== path ===` header give filename and line for navigation.
|
|
121
121
|
|
|
@@ -139,7 +139,7 @@ Output is capped at `limit` matched lines, 2000 rows, and 50KB of text, whicheve
|
|
|
139
139
|
|
|
140
140
|
`undo_last_change` reverts the most recent successful `replace` or `insert` on a file, restoring the exact previous content, BOM and line endings included, plus the previous anchors.
|
|
141
141
|
|
|
142
|
-
- History is per-file and single-level: only the most recent `replace` or `insert` can be reverted.
|
|
142
|
+
- History is per-file and single-level: only the most recent `replace` or `insert` can be reverted. A same-turn batch of `replace`/`insert` calls on one file counts as one entry: one undo reverts the whole batch.
|
|
143
143
|
- History is persisted and survives session restarts. A failed `write` does not clear it.
|
|
144
144
|
- Every applied `replace` or `insert` is undoable; the undo record is saved before the edit is written.
|
|
145
145
|
- A successful `write` clears the history for that file.
|
|
@@ -151,9 +151,9 @@ Output is capped at `limit` matched lines, 2000 rows, and 50KB of text, whicheve
|
|
|
151
151
|
|
|
152
152
|
Auto-read is enabled by default. After a successful `write`, the extension reads the file and appends an `--- Auto-read (hashline anchors) ---` block, so you get fresh `anchor│content` anchors without a separate `read` call.
|
|
153
153
|
|
|
154
|
-
After `replace`, `insert`, and `undo_last_change`, the result shows the post-edit diff. The `+anchor│` and ` anchor│` rows carry the current anchors, so follow-up edits can anchor on the diff directly. The `-anchor│` rows show removed lines with their old anchors, which are stale after the edit. When the context line next to a change is blank or whitespace-only, one more context line is shown in that direction, so the change stays anchored to visible content. Call `read` when you want the full file's anchors.
|
|
154
|
+
After `replace`, `insert`, and `undo_last_change`, the result shows the post-edit diff. Inside a same-turn batch, only the batch's last call shows the combined diff, headed by a `batch:` line (`batch N:` when several files batch); earlier calls reply `In batch` (`In batch N` when several files batch). The `+anchor│` and ` anchor│` rows carry the current anchors, so follow-up edits can anchor on the diff directly. The `-anchor│` rows show removed lines with their old anchors, which are stale after the edit. When the context line next to a change is blank or whitespace-only, one more context line is shown in that direction, so the change stays anchored to visible content. Call `read` when you want the full file's anchors.
|
|
155
155
|
|
|
156
|
-
Auto-read keeps the same 50KB and 2000-line budget as `read`.
|
|
156
|
+
Auto-read keeps the same 50KB and 2000-line budget as `read`. Change it in `/hashline-config`; both settings persist across sessions.
|
|
157
157
|
|
|
158
158
|
## Tool result details
|
|
159
159
|
|
|
@@ -162,7 +162,7 @@ All five tools return machine-readable metadata in `details` alongside the model
|
|
|
162
162
|
| Tool | `details` |
|
|
163
163
|
| --- | --- |
|
|
164
164
|
| `read` | `truncation` (set when output was truncated), `snapshotId` (a `v2\|path\|ino\|mtime\|ctime\|size` fingerprint), `nextOffset` (use as the next `offset`), and `metrics` with `truncated` and `next_offset`. |
|
|
165
|
-
| `replace`, `insert` | `diff` (post-edit diff, capped, with current anchors on `+HASH│` and ` HASH│` rows), `patch` (a standard unified patch for external tools, capped like the diff), `patchTruncated` (true when the patch was cut and can no longer be applied as-is), `firstChangedLine`, `snapshotId`, `classification` (`"noop"` when nothing changed), and `metrics`: `edits_attempted`, `edits_noop`, `warnings`, `classification` (`"applied"` or `"noop"`), `changed_lines` (`{ first, last }`), `added_lines`, `removed_lines`. |
|
|
165
|
+
| `replace`, `insert` | `diff` (post-edit diff, capped, with current anchors on `+HASH│` and ` HASH│` rows; a same-turn batch reports the combined diff on its last call and an empty diff on earlier calls), `patch` (a standard unified patch for external tools, capped like the diff), `patchTruncated` (true when the patch was cut and can no longer be applied as-is), `firstChangedLine`, `snapshotId`, `classification` (`"noop"` when nothing changed), `batch` (`{ id, size, last, total }` marking same-turn batch membership), and `metrics`: `edits_attempted`, `edits_noop`, `warnings`, `classification` (`"applied"` or `"noop"`), `changed_lines` (`{ first, last }`), `added_lines`, `removed_lines`. |
|
|
166
166
|
| `undo_last_change` | `diff` (the undo diff with restored anchors), `patch`, `patchTruncated`, and `metrics` in the same shape as `replace`. |
|
|
167
167
|
| `anchor_grep` | `metrics` with `matches` (capped at `limit`), `files`, and `truncated`; `truncation` (the standard pi truncation report) when output was cut; and `linesTruncated` (true when long lines were shown as fragments). |
|
|
168
168
|
|
|
@@ -170,16 +170,18 @@ All five tools return machine-readable metadata in `details` alongside the model
|
|
|
170
170
|
|
|
171
171
|
| Command | Description |
|
|
172
172
|
| --- | --- |
|
|
173
|
-
| `/
|
|
174
|
-
| `/toggle-anchor-grep` | Enable or disable the `anchor_grep` tool. The built-in grep is disabled while `anchor_grep` is on. Persists across sessions. |
|
|
173
|
+
| `/hashline-config` | Open the settings window: auto-read anchors, `anchor_grep` tool, required `path`, strict input, and boundary dedup. Persists across sessions. |
|
|
175
174
|
| `/clear-anchors` | Clear the session's anchor claims. Anchors are re-claimed on the next `read`. |
|
|
176
175
|
|
|
177
|
-
Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a setting is first
|
|
176
|
+
Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a setting is first changed in `/hashline-config`:
|
|
178
177
|
|
|
179
178
|
```json
|
|
180
179
|
{
|
|
181
180
|
"autoRead": true,
|
|
182
|
-
"anchorGrepEnabled":
|
|
181
|
+
"anchorGrepEnabled": true,
|
|
182
|
+
"requirePath": false,
|
|
183
|
+
"strictInput": false,
|
|
184
|
+
"boundaryDedupMode": "on"
|
|
183
185
|
}
|
|
184
186
|
```
|
|
185
187
|
|
|
@@ -228,10 +230,13 @@ Codes starting with `E_` are errors (the operation failed); codes starting with
|
|
|
228
230
|
| `[E_UNDO_UNAVAILABLE]` | Undo history could not be persisted to the hash store; the edit was refused and the file was left unchanged. |
|
|
229
231
|
| `[E_RANGE_STALE]` | A line in the replaced range no longer matches what was last shown (the file changed on disk, or the line was never shown). The edit was refused; the current range is returned with fresh anchors. |
|
|
230
232
|
| `[W_BOUNDARY_BYPASS]` | The boundary anti-duplication was turned off for one replace call (an identical replacement had previously been cut to a noop); the duplicate lines were applied literally. The dedup is restored for the next call. |
|
|
233
|
+
| `[E_BOUNDARY_STRICT]` | Strict boundary dedup rejected the edit because replacement lines re-include edge lines; resend without those lines. |
|
|
231
234
|
| `[E_FILE_TOO_LARGE]` | The file exceeds the 1,353,139-line hashline limit or the 100MB size limit. |
|
|
232
235
|
| `[E_REGISTRY]` | The anchor registry was not initialized; a serve or edit ran outside an initialized session. |
|
|
233
236
|
| `[E_WRITE_HASH_ECHO]` | A `write` `content` line begins with the exact `anchor│` served for this file at the same line. The write is refused, file byte-identical; retry with bare content (remove the copied anchors). |
|
|
234
237
|
| `[E_PATH_CHANGED]` | A write target changed identity after it was read; the write was refused to avoid following a swapped symlink or overwriting a replacement file. |
|
|
238
|
+
| `[E_BATCH_OVERLAP]` | Batched `replace`/`insert` calls target overlapping ranges; the whole batch was refused with nothing written. Retry with disjoint ranges. |
|
|
239
|
+
| `[E_BATCH_ABORTED]` | A same-turn edit batch aborted (a member failed, or the file changed mid-turn); nothing was written. The first failure is quoted; fix it and retry the batch. |
|
|
235
240
|
| `[E_UNSAFE_REGEX]` | A grep regex can trigger excessive backtracking; simplify it or search with `literal: true`. |
|
|
236
241
|
|
|
237
242
|
## Troubleshooting
|
package/index.ts
CHANGED
|
@@ -7,17 +7,24 @@ import { regGrep } from "./src/grep";
|
|
|
7
7
|
import { regUndo, clearUndo } from "./src/replace-undo";
|
|
8
8
|
import { regRead, fmtReadPreview } from "./src/read";
|
|
9
9
|
import type { RMetrics } from "./src/replace-response";
|
|
10
|
+
import type { ReplaceDetails } from "./src/replace";
|
|
10
11
|
import { extractWarnings } from "./src/replace-render";
|
|
11
12
|
import { MAX_HASH_LINES } from "./src/hashline";
|
|
12
13
|
import {
|
|
13
14
|
readConfig,
|
|
14
15
|
toggleAutoRead,
|
|
15
16
|
toggleAnchorGrep,
|
|
17
|
+
toggleRequirePath,
|
|
18
|
+
toggleStrictInput,
|
|
19
|
+
cycleBoundaryDedupMode,
|
|
16
20
|
} from "./src/config";
|
|
17
21
|
import { loadHashStore, persistSnapshot, pruneMissing } from "./src/hash-store";
|
|
18
22
|
import { initRegistry, gcRegistrySidecars, clearRegistry, freeAnchors, markServed as markServedScoped } from "./src/anchor-registry";
|
|
19
23
|
import { buildServedMap } from "./src/served";
|
|
20
24
|
import { clearBoundaryBypass } from "./src/boundary-bypass";
|
|
25
|
+
import { finalizeTurn, planAssistantMessage } from "./src/batch";
|
|
26
|
+
import { currentEditFlags } from "./src/edit-common";
|
|
27
|
+
import { HashlineConfigOverlay } from "./src/config-ui";
|
|
21
28
|
import { registerWriteHook } from "./src/write-hook";
|
|
22
29
|
import { readNormFile } from "./src/file-reader";
|
|
23
30
|
import { loadFileKindAndText } from "./src/file-kind";
|
|
@@ -37,6 +44,17 @@ export default function (pi: ExtensionAPI): void {
|
|
|
37
44
|
let autoRead = true;
|
|
38
45
|
let grepWasActive = false;
|
|
39
46
|
|
|
47
|
+
async function refreshEditTools(): Promise<void> {
|
|
48
|
+
try {
|
|
49
|
+
const flags = await currentEditFlags();
|
|
50
|
+
regRead(pi, flags);
|
|
51
|
+
regReplace(pi, flags);
|
|
52
|
+
regInsert(pi, flags);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("Failed to refresh edit tools:", error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
pi.on("session_start", async (_event, ctx) => {
|
|
41
59
|
const active = pi.getActiveTools();
|
|
42
60
|
grepWasActive = active.includes("grep");
|
|
@@ -56,6 +74,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
56
74
|
await gcRegistrySidecars();
|
|
57
75
|
const config = await readConfig();
|
|
58
76
|
autoRead = config.autoRead;
|
|
77
|
+
await refreshEditTools();
|
|
59
78
|
pi.setActiveTools(
|
|
60
79
|
pi.getActiveTools().filter((t) =>
|
|
61
80
|
config.anchorGrepEnabled ? t !== "grep" : t !== "anchor_grep",
|
|
@@ -67,31 +86,40 @@ export default function (pi: ExtensionAPI): void {
|
|
|
67
86
|
}
|
|
68
87
|
});
|
|
69
88
|
|
|
70
|
-
pi.registerCommand("
|
|
71
|
-
description: "
|
|
89
|
+
pi.registerCommand("hashline-config", {
|
|
90
|
+
description: "Open the hashline settings window (auto-read, grep, path, strict input, dedup)",
|
|
72
91
|
handler: async (_args, ctx) => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
if (!ctx.hasUI) {
|
|
93
|
+
ctx.ui.notify("/hashline-config requires interactive mode", "error");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
await ctx.ui.custom<void>(async (tui, theme, _keybindings, done) => {
|
|
97
|
+
const overlay = new HashlineConfigOverlay({
|
|
98
|
+
tui,
|
|
99
|
+
theme,
|
|
100
|
+
done,
|
|
101
|
+
onToggle: async (key) => {
|
|
102
|
+
if (key === "autoRead") autoRead = await toggleAutoRead();
|
|
103
|
+
else if (key === "anchorGrepEnabled") {
|
|
104
|
+
const enabled = await toggleAnchorGrep();
|
|
105
|
+
const active = pi.getActiveTools();
|
|
106
|
+
pi.setActiveTools(enabled ? [...new Set([...active.filter((t) => t !== "grep"), "anchor_grep"])] : [...new Set([...active.filter((t) => t !== "anchor_grep"), ...(grepWasActive ? ["grep"] : [])])]);
|
|
107
|
+
}
|
|
108
|
+
else if (key === "requirePath") await toggleRequirePath();
|
|
109
|
+
else if (key === "strictInput") await toggleStrictInput();
|
|
110
|
+
else await cycleBoundaryDedupMode();
|
|
111
|
+
await refreshEditTools();
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
await overlay.load();
|
|
115
|
+
return overlay;
|
|
116
|
+
}, {
|
|
117
|
+
overlay: true,
|
|
118
|
+
overlayOptions: { anchor: "center", width: "90%", minWidth: 60, maxHeight: "90%" },
|
|
119
|
+
});
|
|
91
120
|
},
|
|
92
121
|
});
|
|
93
122
|
|
|
94
|
-
|
|
95
123
|
pi.registerCommand("clear-anchors", {
|
|
96
124
|
description: "Clear the session's anchor claims (path-free resolution state); anchors are re-claimed on the next read",
|
|
97
125
|
handler: async (_args, ctx) => {
|
|
@@ -99,6 +127,21 @@ export default function (pi: ExtensionAPI): void {
|
|
|
99
127
|
ctx.ui.notify(`Anchor claims cleared for this session`, "info");
|
|
100
128
|
},
|
|
101
129
|
});
|
|
130
|
+
pi.on("message_end", async (event, ctx) => {
|
|
131
|
+
try {
|
|
132
|
+
await planAssistantMessage(event.message, ctx.cwd);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error("Failed to plan edit batch:", error);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
pi.on("turn_end", async (event) => {
|
|
138
|
+
try {
|
|
139
|
+
const ids = (event.toolResults ?? []).map((result) => (result as { toolCallId?: unknown }).toolCallId).filter((id): id is string => typeof id === "string");
|
|
140
|
+
await finalizeTurn(ids);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error("Failed to finalize edit batch:", error);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
102
145
|
pi.on("tool_result", async (event, ctx) => {
|
|
103
146
|
if (event.isError) return;
|
|
104
147
|
|
|
@@ -164,7 +207,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
164
207
|
const metrics = (event.details as { metrics?: RMetrics } | undefined)?.metrics;
|
|
165
208
|
if (metrics?.classification === "noop") return;
|
|
166
209
|
|
|
167
|
-
const
|
|
210
|
+
const batched = (event.details as { batch?: { last?: boolean } } | undefined)?.batch;
|
|
211
|
+
if (batched?.last === false) return;
|
|
212
|
+
const toolDetails = event.details as ReplaceDetails | undefined;
|
|
213
|
+
const diff = toolDetails?.diff;
|
|
214
|
+
const detailWarnings = Array.isArray(toolDetails?.warnings) ? toolDetails.warnings.filter((w): w is string => typeof w === "string") : [];
|
|
168
215
|
if (typeof diff !== "string") return;
|
|
169
216
|
const hasDiff = diff.length > 0;
|
|
170
217
|
|
|
@@ -175,7 +222,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
175
222
|
)
|
|
176
223
|
.map((entry) => entry.text)
|
|
177
224
|
.join("\n");
|
|
178
|
-
const warnings = extractWarnings(rendered);
|
|
225
|
+
const warnings = detailWarnings.length ? `Warnings:\n${detailWarnings.join("\n")}` : extractWarnings(rendered);
|
|
179
226
|
const hint = hasDiff ? (warnings ? `${diff}\n\n${warnings}` : diff) : warnings ? `[post-edit] applied successfully; the diff is empty (whitespace-only change).\n\n${warnings}` : "[post-edit] applied successfully; the diff is empty (whitespace-only change).";
|
|
180
227
|
return {
|
|
181
228
|
content: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.2.0",
|
|
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",
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
- `insert`: the anchor must have been shown by `read`, a post-edit diff (`+anchor│`/` anchor│`), or any served `anchor│content` row. Empty file: `read` shows one `anchor│` row — insert `after` it.
|
|
1
|
+
- `insert`: the anchor must have been shown by `read`, a post-edit diff (`+anchor│`/` anchor│`), or any served `anchor│content` row. Empty file: `read` shows one `anchor│` row — insert `after` it.
|
|
2
|
+
- `insert`: same-file calls in one message join the file's batch: earlier calls reply `In batch` (`In batch N` when several files batch), the last call shows the combined diff.
|
package/prompts/insert.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Insert lines after or before one existing line in a text file, addressed by a bare anchor from read output or a diff 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. Lines are added literally, even when they duplicate neighbors.
|
|
1
|
+
Insert lines after or before one existing line in a text file, addressed by a bare anchor from read output or a diff 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. Lines are added literally, even when they duplicate neighbors. Multiple `replace`/`insert` calls on the same file in one message form one batch per file with a single combined diff and a single undo.
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
- `read`: view files with `read`, not `bash` (`sed`/`grep`/`cat`) — only `read` rows carry usable anchors.
|
|
1
2
|
- `read`: call before `replace` when you need fresh anchors.
|
|
2
3
|
- `read`: call again after an edit when you need anchors you lack — post-edit diff `+anchor│`/` anchor│` rows and any served `anchor│content` rows already carry fresh anchors for the changed range.
|
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 alphanumeric anchor, 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
|
|
1
|
+
Read a text file and return it as `anchor│content` rows, one per line: a 4-character alphanumeric anchor, 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,5 +1,5 @@
|
|
|
1
|
-
- `replace`:
|
|
2
|
-
- `replace`: `replacement_lines` takes bare lines without `│`; `[""]` is one blank line; pasted `anchor│` prefixes are stripped automatically.
|
|
3
|
-
- `replace`:
|
|
4
|
-
- `replace`:
|
|
5
|
-
- `replace`: if `replacement_lines` re-include the boundary line adjacent to the range, it is deduplicated automatically, shown as `dedup│content` rows in the diff (not editable, never use `dedup` as an anchor).
|
|
1
|
+
- `replace`: edit with `replace`/`insert`, not `sed -i` or heredocs — anchor edits are verified against what was shown and undoable.
|
|
2
|
+
- `replace`: `replacement_lines` takes bare lines without `│`; `[""]` is one blank line; pasted `anchor│` prefixes are stripped automatically (single line: same anchor for `remove_from` and `remove_to`).
|
|
3
|
+
- `replace`: post-edit diff `+anchor│`/` anchor│` rows are fresh anchors for the next edit — no new `read` needed. Same-file calls in one message form one batch with a single combined diff; check each batch diff before the next turn's edits on that file.
|
|
4
|
+
- `replace`: batched calls must target disjoint ranges and all be valid; an overlap or any failure aborts the whole batch with nothing applied.
|
|
5
|
+
- `replace`: if `replacement_lines` re-include the boundary line adjacent to the range, it is deduplicated automatically, shown as `dedup│content` rows in the diff (not editable, never use `dedup` as an anchor).
|
package/prompts/replace.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Replace a range of lines (or a single line) in a text file, targeted by 4-character anchors from read output. 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. Every line in the range must match what was last shown; if the file drifted, the edit is refused and the current range is returned with fresh anchors, so retry without re-reading. Anchor follow-up edits on the `+anchor│` and ` anchor│` rows of the post-edit diff instead of re-reading.
|
|
1
|
+
Replace a range of lines (or a single line) in a text file, targeted by 4-character anchors from read output. 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. Every line in the range must match what was last shown; if the file drifted, the edit is refused and the current range is returned with fresh anchors, so retry without re-reading. Anchor follow-up edits on the `+anchor│` and ` anchor│` rows of the post-edit diff instead of re-reading. Multiple `replace`/`insert` calls on the same file in one message form one batch per file: earlier calls reply `In batch` (`In batch N` when several files batch) and the last call shows the combined diff, with one undo for the whole batch.
|
package/src/anchor-registry.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mkdir, readFile, readdir, rm, stat } from "fs/promises";
|
|
2
|
-
import { appendFileSync } from "fs";
|
|
1
|
+
import { chmod, mkdir, readFile, readdir, rm, stat } from "fs/promises";
|
|
2
|
+
import { appendFileSync, chmodSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { createHash } from "crypto";
|
|
5
5
|
import { sessionClaimsDir } from "./paths";
|
|
@@ -130,7 +130,11 @@ export async function initRegistry(sessionFile: string | undefined): Promise<voi
|
|
|
130
130
|
seedServedFromOwned(folded);
|
|
131
131
|
registries.set(key, folded);
|
|
132
132
|
try {
|
|
133
|
-
await mkdir(sessionClaimsDir(), { recursive: true });
|
|
133
|
+
await mkdir(sessionClaimsDir(), { recursive: true, mode: 0o700 });
|
|
134
|
+
if (process.platform !== "win32") {
|
|
135
|
+
try { await chmod(sessionClaimsDir(), 0o700); } catch (error) { if (errCode(error) !== "ENOENT") console.error("Failed to secure anchor registry directory:", error); }
|
|
136
|
+
try { await chmod(currentSidecar, 0o600); } catch (error) { if (errCode(error) !== "ENOENT") console.error("Failed to secure anchor registry sidecar:", error); }
|
|
137
|
+
}
|
|
134
138
|
appendEvent({ kind: "session", sessionFile } satisfies RegistryEvent);
|
|
135
139
|
} catch (error) {
|
|
136
140
|
console.error("Failed to initialize anchor registry sidecar:", error);
|
|
@@ -146,6 +150,9 @@ function appendEvent(event: RegistryEvent): void {
|
|
|
146
150
|
if (!currentSidecar) return;
|
|
147
151
|
try {
|
|
148
152
|
appendFileSync(currentSidecar, JSON.stringify(event) + "\n", "utf-8");
|
|
153
|
+
if (process.platform !== "win32") {
|
|
154
|
+
try { chmodSync(currentSidecar, 0o600); } catch (error) { if (errCode(error) !== "ENOENT") console.error("Failed to secure anchor registry sidecar:", error); }
|
|
155
|
+
}
|
|
149
156
|
} catch (error) {
|
|
150
157
|
console.error("Failed to append registry event:", error);
|
|
151
158
|
}
|
|
@@ -180,7 +187,7 @@ export function mintAnchor(state: SessionState): string {
|
|
|
180
187
|
}
|
|
181
188
|
}
|
|
182
189
|
throw new Error(
|
|
183
|
-
`${ANCHOR_POOL_EXHAUSTED_PREFIX};
|
|
190
|
+
`${ANCHOR_POOL_EXHAUSTED_PREFIX}; use write for very large files.`,
|
|
184
191
|
);
|
|
185
192
|
}
|
|
186
193
|
|
|
@@ -507,12 +514,8 @@ export async function allocateFileAnchors(
|
|
|
507
514
|
const lines = splitLines(content);
|
|
508
515
|
const checksums = lines.map((line) => contentChecksum(hashSource(line)));
|
|
509
516
|
if (options?.previous?.spans) {
|
|
510
|
-
const sessionState = current()!;
|
|
511
517
|
const prevChecksums = splitLines(options.previous.content).map((line) => contentChecksum(hashSource(line)));
|
|
512
518
|
const aligned = alignOwnershipWithSpans(path, options.previous.hashes, prevChecksums, checksums, options.previous.spans, { shadow });
|
|
513
|
-
for (let i = 0; i < aligned.anchors.length; i++) {
|
|
514
|
-
sessionState.owned.set(aligned.anchors[i]!, { path, checksum: checksums[i]! });
|
|
515
|
-
}
|
|
516
519
|
if (!shadow && options.persist !== false) {
|
|
517
520
|
persistSnapshot(store, path, content, aligned.anchors, checksums);
|
|
518
521
|
}
|