pi-tian-edit-safe

A stricter edit tool for the pi coding agent with full-span match verification, verbatim replacement, and an array-only edits schema.

Packages

Package details

extension

Install pi-tian-edit-safe from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-tian-edit-safe
Package
pi-tian-edit-safe
Version
0.1.4
Published
Jul 31, 2026
Downloads
253/mo · 253/wk
Author
tian.zuo
License
MIT
Types
extension
Size
38 KB
Dependencies
0 dependencies · 2 peers
Pi manifest JSON
{
  "extensions": [
    "./index.ts"
  ]
}

Security note

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

README

pi-edit-safe

A drop-in replacement for the pi coding agent's built-in edit tool, with a stricter matcher that refuses to silently edit the wrong place — and a call shape that weaker models can actually use.

Exposes one call shape — {path, edits: [...]}, always an array, even for a single replacement — so the model has nothing to choose per call. Multi-edit applies in order (sequential), the contract models assume from multi-edit tools elsewhere, instead of pi's all-against-the-original-file semantic.

The looser shapes models emit anyway are still folded onto that form before validation, so a stray legacy call is normalized rather than rejected — they are just not advertised.

Install: npm:pi-tian-edit-safe · npm package pi-tian-edit-safe · workspace packages/pi-edit-safe

Call shape

One shape, always an array:

{ "path": "src/a.ts", "edits": [
	{ "oldText": "...", "newText": "..." },
	{ "oldText": "...", "newText": "..." }
] }

Use a one-element edits array for a single replacement. path and edits are both required and edits has minItems: 1. Nothing else is advertised to the model.

Tolerated but not advertised

prepareArguments runs before schema validation (per pi's extension contract) and folds these onto the canonical form: file_path/filePath/filename for path; old_string/new_string, oldString/newString, old_str/new_str for the pair (per entry or top-level); top-level oldText/newText shorthand; edits as a JSON string; and edits as a single object.

This keeps old resumed sessions and off-contract calls working without widening the public schema. Exact duplicate entries are rejected loudly. When a later edit fails because an earlier edit in the same call rewrote its target, the error says exactly that.

Design stance

Decision typical fuzzy edit tools pi-edit-safe
Partial-signal strategies (first/last-line anchor) yes none
On ambiguity fall through to next candidate throw immediately
Fuzzy candidate validation per-candidate similarity threshold full-span structural equality + exactly-one + occurs-once
Overlapping exact matches often missed caught (overlap-aware counting)
Unicode punctuation drift (smart quotes, em dash, NBSP) varies strict unicode strategy, span-only
Untouched bytes may be normalized whole-file never touched (slice + verbatim splice)
Line endings whole-file normalize/restore span-boundary only; mixed-ending files never flattened
newText write String.replace ($& hazard) slice-join (literal)
Multi-edit semantics all edits vs the original file sequential, in order
Call shapes offered to the model varies exactly one (edits[])

Fuzzy matching only ever locates a span. The replacement is spliced in verbatim — it is never re-indented or rewritten.

Matching order

  1. Exact, never gated, counted overlap-aware (aa in aaa is ambiguous, not unique).
  2. If exact fails, hard gates: ≥5 non-space chars, no NUL byte, ≤1M chars, ≤50k lines.
  3. Increasingly tolerant full-span strategies: line-trimmed → unicode punctuation → collapsed whitespace → escape-normalized.
  4. More than one candidate for a strategy → throw. Never falls through from ambiguity to a looser matcher.
  5. Splice verbatim. Uniform LF/CRLF files adapt the replacement's newlines; mixed-ending files take it as-is.
  6. Apply all entries in memory, in order, then write once — a later failure leaves the file unchanged.

Result details

Returns pi's built-in EditToolDetails shape (diff, patch, firstChangedLine) alongside a edits[] array of per-edit provenance (matchedVia, startLine, endLine).

Because pi resolves renderer inheritance per slot (toolDefinition.renderCall ?? builtInToolDefinition.renderCall), this override deliberately defines neither renderCall nor renderResult, and so inherits pi's streaming diff preview and final diff rendering for free.

The diff is computed against the real bytes on both sides. pi's built-in diffs its LF-normalized view instead, which is why the two disagree on mixed-line-ending files (see bench case 11).

Disable

PI_EDIT_SAFE_DISABLE=1 pi   # falls back to the built-in edit

Tests and A/B bench

npm test -w pi-tian-edit-safe    # 45 unit tests (Node's built-in runner)
npm run bench -w pi-tian-edit-safe

The bench imports pi's actual shipped edit-diff.js functions and composes them exactly as dist/core/tools/edit.js does, then runs the same 12 (file, edits) cases through both pipelines and reports every divergence, including "canary" lines that must stay byte-identical.

These are deterministic corruption regressions. They are not a model-level eval — no cross-model pass@1 / token / retry benchmark exists for this tool yet.

Attribution

Fuzzy-strategy concepts adapted from opencode (MIT, via cline MIT and gemini-cli Apache-2.0); the unicode normalization table mirrors pi's own normalizer (MIT), applied span-only. Design philosophy reproduced independently. See NOTICES.md.