npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pi-mono-multi-edit

v1.7.3

Published

Pi extension that replaces edit with batch edits and Codex-style patches

Downloads

222

Readme

Multi-Edit — Enhanced Edit Tool

A pi extension that replaces the built-in edit tool with a more powerful version that supports batch edits across multiple files and Codex-style patch payloads — all validated against a virtual filesystem before any real changes are written.

Origins

Initially derived from mitsuhiko/agent-stuff's pi-extensions/multi-edit.ts. The substrate has since been substantially rewritten — the patch engine is now a recursive-descent parser over a line cursor with indexOf-based hunk anchoring, the diff renderer is a two-pass design, and the classic edit path gained atomic multi-file rollback, eager write-permission preflight, curly-quote fallback matching, a read-cache backed workspace, and context-guard:file-modified event integration. Shape-level divergences (notably the Hunk { oldBlock, newBlock } shape vs upstream's UpdateChunk { oldLines[], newLines[] }) and dropped compatibility corners are documented below.

Overview

The standard edit tool handles one oldText → newText replacement at a time. Multi-Edit extends it with three modes so an agent can make many targeted changes in a single tool call, dramatically reducing round-trips and the risk of partial edits leaving the codebase in an inconsistent state.

All modes run a preflight pass on a virtual (in-memory) copy of the filesystem first. If any replacement fails, no real files are touched.

Modes

1. Single (classic)

Identical to the built-in edit tool. Provide path, oldText, and newText.

{
  "path": "src/index.ts",
  "oldText": "const foo = 1;",
  "newText": "const foo = 2;",
}

2. Multi (batch array)

Pass a multi array of edit objects. Each item has path, oldText, and newText. A top-level path can be set as a default that individual items inherit when they omit their own path.

{
  "path": "src/utils.ts", // inherited by items that omit path
  "multi": [
    {
      "oldText": "import foo from 'foo';",
      "newText": "import foo from '@scope/foo';",
    },
    {
      "path": "src/other.ts", // overrides the top-level path
      "oldText": "const bar = 0;",
      "newText": "const bar = 42;",
    },
  ],
}

You can also mix a top-level single edit with multi — the top-level edit is prepended as the first item in the batch:

{
  "path": "src/index.ts",
  "oldText": "version: 1",
  "newText": "version: 2",
  "multi": [{ "oldText": "// old comment", "newText": "// new comment" }],
}

3. Patch (Codex-style)

Pass a patch string delimited by *** Begin Patch / *** End Patch. This format supports adding, deleting, and updating files with hunk-based diffs — similar to the patch format used by OpenAI Codex.

*** Begin Patch
*** Add File: src/new-file.ts
+export const greeting = "hello";
*** Delete File: src/deprecated.ts
*** Update File: src/existing.ts
@@ function oldName() {
-function oldName() {
+function newName() {
*** End Patch

Supported operations inside a patch:

| Header | Effect | | ------------------------- | ------------------------------------------------------------------- | | *** Add File: <path> | Creates (or overwrites) the file with +-prefixed lines as content | | *** Delete File: <path> | Removes the file (errors if it doesn't exist) | | *** Update File: <path> | Applies one or more @@-delimited hunks to the file |

Note: *** Move to: (rename) operations are not supported and will throw an error.

Codex apply_patch compatibility

The patch engine implements a pragmatic subset of the Codex apply_patch format. The following edge cases are intentionally not supported and raise a parse error instead of degrading silently:

| Feature | Status | Notes | | --------------------------------- | --------- | ----------------------------------------------------------------------------------------- | | @@ hunk header | Required | Every hunk inside an *** Update File: block must start with @@ | | Trailing-whitespace tolerance | Supported | Hunks fall back to per-line trimEnd matching when exact indexOf misses | | Full trim / unicode-normalized | Dropped | Only trimEnd is supported — normalize curly quotes or dashes in the patch before sending | | *** End of File sentinel hunks | Dropped | Use a normal hunk anchored on the last real line | | *** Move to: rename | Rejected | Emit an Add + Delete pair instead |

These restrictions keep the parser simpler and more predictable than a full 4-pass fuzzy matcher while still catching the most common class of whitespace mismatch.

Key Features

Preflight Validation

Before writing a single byte to disk, every edit is applied to a virtual (in-memory) snapshot of the affected files. If any replacement fails — wrong oldText, file not found, missing context — the entire operation is aborted and no real files are modified. The preflight also checks write permissions against the real filesystem, so read-only targets fail fast before any virtual apply is attempted.

Atomic Multi-File Rollback

When a classic batch spans multiple files, the applier snapshots each file's pre-edit content before writing it. If a later file in the batch fails mid-write, every file already written is restored from its snapshot on a best-effort basis — the original failure is still surfaced, but the filesystem ends up in its pre-batch state.

Positional Ordering for Same-File Edits

When multiple edits target the same file, they are automatically sorted by their position in the original file content (top-to-bottom). This ensures the forward-search cursor works correctly regardless of the order the model listed the edits.

Quote-Normalized Matching for Classic Edits

Classic oldText lookups escalate through an ordered list of normalizer passes applied to both oldText and file content. The first pass that locates the transformed string wins:

  1. Exact — character-for-character match
  2. Curly → straight quotes' / ' / " / " in the model's oldText are rewritten to ASCII before the second search
  3. Trailing whitespace tolerance — per-line trimEnd on both sides catches the most frequent class of mismatch (model generates trailing spaces the file doesn't have, or vice versa)

Extending the chain is a matter of appending another normalizer to the MATCH_PASSES array in classic.ts.

Patch @@ hunks also support a trimEnd fallback — when the exact indexOf misses, the applier retries with per-line trailing-whitespace stripping on both the hunk and the file content.

Redundant Edit Detection

If the same oldText → newText pair appears more than once in a multi batch for the same file (e.g. the model over-counted occurrences), subsequent duplicates are skipped gracefully with a success status rather than raising an error.

Diff Generation

Every successful edit returns a unified diff attached to the tool result so the agent and user can inspect exactly what changed. For multi-file operations, per-file diffs are concatenated. The first changed line number is also surfaced for UI scrolling.

Path Inheritance

In multi mode, items that omit path automatically inherit the top-level path. This is convenient when most edits target a single file with one or two exceptions.

Parameters

| Parameter | Type | Description | | --------- | ----------------------- | ------------------------------------------------------------------------------------------------------------ | | path | string (optional) | Target file path (absolute or relative to cwd). Serves as default for multi items. | | oldText | string (optional) | Exact text to find and replace. Must match including all whitespace. | | newText | string (optional) | Replacement text. | | multi | EditItem[] (optional) | Array of { path?, oldText, newText } objects for batch mode. | | patch | string (optional) | Codex-style patch payload (*** Begin Patch … *** End Patch). Mutually exclusive with all other parameters. |

EditItem shape:

{
  path?: string;   // inherits top-level path if omitted
  oldText: string;
  newText: string;
}

Dependencies

| Package | Role | | ------------------------------- | --------------------------------------------------- | | @earendil-works/pi-coding-agent | ExtensionAPI type and tool registration | | @sinclair/typebox | Runtime JSON Schema / TypeBox parameter definitions | | diff | Line-level diff generation for result output |

Error Handling

| Situation | Behaviour | | -------------------------------------------------------------------- | ------------------------------------------------------ | | patch used together with path/oldText/newText/multi | Throws immediately — parameters are mutually exclusive | | Incomplete top-level edit (e.g. path + oldText but no newText) | Throws listing the missing fields | | multi item missing path and no top-level path set | Throws identifying which item is affected | | oldText not found in file | Preflight throws; no files are modified | | patch context line not found | Preflight throws; no files are modified | | File does not exist or is not writable | Throws before any mutations | | Patch *** Move to: operation | Throws — not supported |

Performance vs Base Edit

Measured across 38 real pi sessions (81 JSONL files) using npm run bench -- --from-session --all. Sessions are auto-classified: base = only single path/oldText/newText calls; multi-edit = uses multi or patch at least once.

Headline Numbers

| Metric | Base | Multi-Edit | Delta | | ------------------- | ----: | ---------: | ----------: | | Sessions | 22 | 16 | | | Tool calls | 92 | 137 | | | Logical edits | 92 | 247 | | | Edits / tool call | 1.00 | 1.80 | +0.80 | | Failure rate | 6.5% | 6.6% | +0.0 pp | | P50 duration | 7 ms | 11 ms | | | P95 duration | 31 ms | 25 ms | | | Cost / logical edit | $0.29 | $0.17 | -41.8% | | Calls saved vs base | — | 110 | 44.5% fewer |

What the data says

Wins:

  • Cost per edit drops 42%. Batching N edits into one tool call avoids N-1 round-trips of assistant→tool→assistant, each of which carries the full conversation context as input tokens. At $0.17 vs $0.29 per logical edit, multi-edit pays for itself on any batch ≥ 2.
  • 44.5% fewer tool calls. 110 hypothetical round-trips eliminated. This is time the model spends re-reading its own context, waiting for tool dispatch, and generating boilerplate tool-call framing — all wasted.
  • P95 latency is lower (25 ms vs 31 ms). The preflight + read cache avoids wasted disk I/O on doomed edits, and the cache deduplicates reads when multiple edits touch the same file.

Neutral / watch items:

  • P50 latency is slightly higher (11 ms vs 7 ms). Expected: multi-edit does a full preflight pass before the real write. The delta is negligible per-edit (~2 ms extra for the safety guarantee).

Mode Breakdown (pre-v1.5.1)

| Mode | Calls | % | Failure rate | | ------ | ----: | --: | -----------: | | single | 61 | 45% | 1.6% | | patch | 41 | 30% | 9.8% | | multi | 35 | 25% | 11.4% |

Root-cause analysis of the 8 multi/patch failures showed: 5 were trailing-whitespace mismatches in oldText, 2 were batch-poisoned (1 bad edit killed 5 good siblings), and 1 was a legitimate ENOENT. Three fixes shipped in v1.5.1 to address this:

  1. trimEnd matching for classic editsfindActualString now tries a per-line trimEnd normalization pass on both oldText and file content when exact and curly-quote passes miss. Catches the dominant failure class (model generates trailing spaces the file doesn't have, or vice versa).
  2. Partial success for multi batches — when one edit in a batch can't be found, the remaining edits are still applied. Failures are reported individually instead of aborting the entire batch. A 6-edit call with 1 bad edit now produces 5 successes + 1 failure instead of 0 + 6.
  3. trimEnd fallback for patch hunks — the patch applier now falls back to per-line trimEnd matching when exact indexOf misses, using the same strategy for both oldBlock and contextPrefix anchors.

Projected impact: multi mode failure rate ~11.4% → ~3%, patch mode ~9.8% → ~2.5%. The batch-poisoning fix alone eliminates the inflated failure count — previously a single bad edit in a 6-edit batch counted as 1 failed tool call; now it counts as 1 failed edit + 5 successes.

Multi-edit sessions still use single mode 45% of the time — room to push batch adoption via prompt guidelines.

Running the Benchmark & Analysis

The benchmark-edits tool provides two modes: a synthetic benchmark that measures engine latency on controlled scenarios, and a session analysis mode that parses historical pi session JSONL logs to compute cost, token, failure, and throughput metrics.

# Synthetic benchmark — built-in scenarios
npm run bench

# Custom scenario file (JSON array — see header comment in benchmark-edits.ts)
npm run bench -- scenarios.json

# Session analysis — all pi sessions
npm run bench -- --from-session --all

# Specific session files or directories
npm run bench -- --from-session ~/.pi/agent/sessions/<project-dir>/
npm run bench -- --from-session session1.jsonl session2.jsonl