@criogaid/pi-hashline-edit

Hashline-style file editing for pi — line-anchored edits verified by content hash, replacing oldText/newText matching

Packages

Package details

extension

Install @criogaid/pi-hashline-edit from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@criogaid/pi-hashline-edit
Package
@criogaid/pi-hashline-edit
Version
0.10.0
Published
Nov 5, 2026
Downloads
811/mo · 361/wk
Author
criogaid
License
MIT
Types
extension
Size
1.1 MB
Dependencies
3 dependencies · 2 peers
Pi manifest JSON
{
  "extensions": [
    "./src/index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

@criogaid/pi-hashline-edit

CI npm version npm downloads license

English · 简体中文

Hash-anchored file editing for Pi. The model edits lines by citing anchors from read or grep; every anchor is checked against the current file before anything is written. Stale or mistaken edits are rejected with enough context for the model to correct them, instead of silently changing the wrong lines.

Why use it

  • Edits land where the model meant. Each line carries a short LINE#HASH anchor. If the file changed since the model looked, the edit is rejected, and the report points to where the line moved.
  • No text matching. The model never has to reproduce old text exactly, so whitespace drift, duplicate snippets, and long blocks stop causing failed or misplaced edits.
  • Files stay byte-faithful. BOMs, CRLF/LF line endings, final newlines, and untouched bytes are preserved. Invalid UTF-8 and NUL bytes are rejected before writing.
  • Safe publication. Writes are atomic (temporary file, then link or rename). Read-modify-write calls bind publication to the bytes they read; failures report whether publication happened.
  • Fewer round trips. A mutation can carry a follow-up command such as npm test (Action Fusion).
  • JavaScript regex everywhere. grep and replace use the JavaScript regex semantics the model already knows, with ripgrep speed for finding candidate files.
  • Output sized to the model. Result sizes scale with the selected model's context window, so small models are not flooded and large models are not starved.

Install

pi install npm:@criogaid/pi-hashline-edit

Requires Pi 0.99.1 or later. The extension works on local files; for remote or custom storage, set hashlineEdit.enabled to false and reload Pi to restore the built-in tools.

Quick start

Pi's model uses the tools automatically. A read returns lines with anchors:

greet.ts · 3 lines
1#ZR63│export function greet() {
2#GFYR│  return "hello";
3#FNSZ│}

The model copies an anchor into edit:

{
  "path": "greet.ts",
  "edits": [{ "action": "replace", "anchor": "2#GFYR", "body": ["  return \"hello, hashline\";"] }]
}

The edit succeeds only if line 2 still has that content. The TUI shows the diff; the model receives a short JSON report.

Tools

The extension replaces Pi's read, grep, edit, and write, and adds replace.

Tool What it does
read Shows UTF-8 text with anchors, paginated within the model's output budget. Images and files containing NUL bytes go to Pi's built-in read.
grep Searches files and directories with JavaScript regex or literal text; returns anchored matches, file paths, or counts in traversal order; a limit keeps the first matches in that order. Directory searches respect ignore rules and skip .git and linked directories, even beneath a directory named in path; naming an ignored file or one of those directories itself searches it.
edit Replaces, inserts, or deletes whole lines by anchor. All operations in one call apply to the same snapshot, or none apply.
replace Applies literal or regex substitutions across one file. Every rule must match, and overlapping matches reject the batch.
write Creates a file (mode: "create") or overwrites one (mode: "overwrite"), using the content exactly as given.

Grep visits input paths in the order supplied and uses ripgrep's sorted directory traversal within each root. Repeated candidate paths are searched once, at their first occurrence. This is not global lexicographic sorting of full paths.

Grep matches the LF view with native JavaScript regexes. Without multiline, each physical line is searched separately; with it, the whole file is searched and every physical line touched by a match is returned. Context rows do not count toward limit. Content output re-reads matched files and omits any whose revision changed; files and count report the searched snapshot without a second read. Incomplete scans state their diagnostics or scan limit in the report.

Edit operations:

action Effect
replace / delete Replace or remove a line, or an inclusive range with end.
insert_before / insert_after Insert lines next to an anchor.
prepend / append Insert lines at the start or end of the file.

One call takes at most 64 operations. Ranges in one call must not overlap, and each gap between lines takes at most one insertion. An insertion at the start of a replaced or deleted range goes before it.

Every tool asks for constrained sampling (Pi's constrainedSampling, a strict JSON schema) where the provider supports it, so the model cannot emit fields or values the schema does not declare, or exceed edit's operation limit. Optional fields the call does not use are then written as null, including each edit operation's fields that its action does not take. For the same reason, grep's pattern, path, and glob take arrays only; on other providers Pi wraps a single string into an array.

The model sees each tool's full parameter schema; the definitions live in the tool files under src/pi/.

Range operations verify the cited anchor and end. Interior lines have no individual anchor check. Changes elsewhere before the call do not invalidate unchanged anchors; publication rechecks the bytes read while applying the call and rejects a detected change.

When an edit is rejected

Argument and anchor validation failures leave the file untouched. The report names the error, the fields involved, and a next instruction. For a stale anchor it also shows where the line most likely moved, with its new anchor and nearby lines, so the model can confirm and resubmit without re-reading the whole file. Recovery never edits on its own; every retry is verified again.

A failure during or after publication states NOT_PUBLISHED, PUBLISHED, or UNKNOWN. Follow the report's next before retrying: an error after publication can leave the requested change on disk.

Argument mistakes, including a reversed range, are reported per field, with what the field means and how to fix it, before any file is touched.

Action Fusion

edit, replace, and write accept an optional then_run command that runs right after a successful change:

{
  "path": "src/foo.ts",
  "replacements": [{ "find": "oldName", "literal": true, "replace": "newName" }],
  "then_run": { "command": "npm test", "timeout": 60 }
}

The command runs through the session's own Bash tool, so approvals, overrides, and hooks still apply. It starts only if the change succeeded and the file is still as written. A failing command never rolls back the file. The TUI shows the change and the command as separate cards, with a live countdown when a timeout is set.

Command cards use Pi's native tool component for mouse clicks and the global expand shortcut. Each card keeps its own expansion while output streams or the screen redraws. The built-in Bash renderer displays the command's original content and details, which are also saved for session restore; custom Bash renderer overrides are not exposed by the supported Pi API.

Configuration

Add hashlineEdit to Pi's global settings (~/.pi/agent/settings.json) or a project's .pi/settings.json:

{
  "hashlineEdit": {
    "enabled": true,
    "actionFusion": true,
    "hashLen": 4,
    "shiftRadius": 15,
    "read": { "defaultLimit": 2000 },
    "grep": { "defaultLimit": 100, "defaultContext": 0, "regexTimeoutMs": 5000 },
    "replace": { "regexTimeoutMs": 5000 }
  }
}
Setting Default Meaning
enabled true Register the extension's tools.
actionFusion true Allow then_run on mutation tools.
hashLen 4 Anchor hash length, 2–8.
shiftRadius 15 Lines searched around a stale anchor before the whole file, 0–100; 0 disables recovery search.
read.defaultLimit 2000 Lines returned when the call gives no limit, matching Pi's built-in read.
read.maxKiB Model-based Fixed cap on read output, 1–4096 KiB.
grep.defaultLimit 100 Matching lines returned when the call gives no limit.
grep.defaultContext 0 Context lines around each match, 0–20.
grep.regexTimeoutMs / replace.regexTimeoutMs 5000 Time limit for one regex evaluation, 3000–300000 ms.

A project's hashlineEdit replaces the global one as a whole; invalid values fall back to their defaults. Reload Pi after changing settings. The configuration schema is the source of truth.

Output budgets

The selected model's declared context capacity sets the output budgets; remaining context is never used. For a known capacity, anchored read rows and grep payloads receive one byte per context token, up to 4 MiB. Diagnostic blocks receive a quarter of that budget (at least 512 bytes), and free-text fields a quarter of the block budget (at least 128 bytes). Grep previews keep one UTF-16 code unit per four field bytes. The definitions and fallback values live in budgets.ts.

When model capacity is unknown or invalid, the defaults are 256 KiB for read rows, 50 KiB for grep payloads, 16 KiB per diagnostic block, 4 KiB per free-text field, and 500 UTF-16 code units per grep preview. An explicit read.maxKiB overrides only the read cap. Each execution keeps the budget snapshot it started with.

Read reports pagination when its default line limit or byte budget cuts a window; a caller-supplied limit alone does not add a pagination report. A first row that cannot fit gets a lineTooLong fact. Grep reports payload and preview truncation separately, and plain previews for invalid UTF-8 never carry edit anchors.

Limits

  • Anchors are short checksums: they catch changed and shifted lines, but can collide and do not prove the model read the line.
  • Only UTF-8 text is edited; UTF-16 and legacy code pages are not supported.
  • Revision checks catch most concurrent changes, but there is no cross-process lock or multi-file transaction.
  • Overwrites keep symlinks pointing at their target; FIFOs, other non-regular files, and files with multiple hard links are not overwritten.
  • Grep's search limits allow multiline files up to 64 MiB and individual physical lines up to 64 × 1024² UTF-16 code units. Matching runs in at most four workers, each with a 512 MiB V8 old-generation heap limit; individual regex evaluations use the configured timeout.
  • When discovery falls back to every file in scope, it stops queuing files once their combined sizes reach the 256 MiB whole-scope budget; the last queued file can cross that threshold. The report states when this prevents another file from being scanned. Ripgrep protocol records have a 16 MiB limit. Unsupported or over-limit candidate analysis widens discovery; JavaScript still decides the matches.

Development

Command Checks
npm run format:check Formatting.
npm run typecheck Types for source, tests, and benchmarks.
npm test Core and tool tests.
npm run test:integration Real ripgrep, large files, installed packages, and real Pi sessions.
npm run test:all Both suites, as in CI.
npm run test:cartesian Slow Cartesian regex matrices; run after changing regex translation or prefiltering.

Result fields and error codes are defined in report-schema.ts. Module responsibilities are in src/core/AGENTS.md and src/pi/AGENTS.md. THIRD_PARTY_NOTICES.md lists borrowed techniques and test cases.