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.
Package details
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
- Exact, never gated, counted overlap-aware (
aainaaais ambiguous, not unique). - If exact fails, hard gates: ≥5 non-space chars, no NUL byte, ≤1M chars, ≤50k lines.
- Increasingly tolerant full-span strategies: line-trimmed → unicode punctuation → collapsed whitespace → escape-normalized.
- More than one candidate for a strategy → throw. Never falls through from ambiguity to a looser matcher.
- Splice verbatim. Uniform LF/CRLF files adapt the replacement's newlines; mixed-ending files take it as-is.
- 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.