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

@lucascardozo/pi-edit-guard

v0.13.0

Published

Pi extension that wraps the native edit tool with indentation-drift recovery, uniqueness enforcement, and batch-aware error reporting. Fixes the most common LLM failure mode in code editing.

Readme

pi-edit-guard

Pi extension that wraps the native edit tool with silent auto-fix, uniqueness enforcement, and batch-aware error reporting.

Fixes the most common LLM failure mode in code editing: the model counts spaces wrong, sends an oldText that doesn't match the file's actual indentation, and the edit fails with a generic "Could not find" error. The model then has to either re-read the file (cost) or give up.

pi-edit-guard silently corrects these failures for the most common case (uniform leading-space shift) by mutating event.input in place and letting native edit run with corrected oldText/newText. When the failure is more complex (ambiguous match, character difference, no match), it surfaces a consolidated report so the model can fix everything in one pass instead of N.

Install

pi install npm:@lucascardozo/pi-edit-guard

Quick start

After pi install, the extension is active by default with zero config. The most common failure mode (model writes oldText with wrong leading indentation) is handled silently — pi-edit-guard mutates the edit in place so native edit succeeds.

Verify it works

The debug logger is off by default — no files are written to /tmp unless you opt in. To capture a session for triage, set the env vars before launching Pi:

PI_EDIT_GUARD_DEBUG=1 pi   # capture one NDJSON line per edit
PI_EDIT_GUARD_DEBUG=1 PI_EDIT_GUARD_LOG_FULL=1 PI_EDIT_GUARD_LOG_SNAPSHOTS=1 pi  # full content + file snapshots

Once enabled, the log goes to /tmp/pi-edit-guard-<pid>.log (override with PI_EDIT_GUARD_LOG_PATH=...). Run a model turn that touches a file and inspect:

# in one terminal
tail -f /tmp/pi-edit-guard-$(pgrep -f "pi-coding-agent" | head -1).log
# in another: ask the model to edit any file in your project

If the extension is intercepting correctly, you'll see one JSON line per edit with fields like result: "autofixed" (silent correction), result: "blocked" (atomic block with consolidated report), or result: "formatter-rewritten" (formatter applied after a successful native edit).

Optional: configure a formatter

If you want the extension to defer to an external formatter (prettier, biome, deno fmt, etc.) instead of mutating newText itself, create a config file. See Auto-format (opt-in, v0.12.0+) for the schema and Common formatter recipes for copy-paste configs.

The simplest setup — use prettier on everything:

mkdir -p .pi/extensions/pi-edit-guard
cat > .pi/extensions/pi-edit-guard/config.json <<'JSON'
{
  "commands": { "prettier": ["npx", "prettier", "--write"] },
  "filetypes": { "*": "prettier" }
}
JSON

Restart Pi. From now on, any edit the model makes will run through prettier and the model will see the atomic final state (original → formatted), never the intermediate drift.

Disabling temporarily

To verify whether pi-edit-guard is causing a problem in a specific session, set the env var before launching Pi:

# Bypass autofix but keep cascade (lets an external formatter handle drift):
PI_EDIT_GUARD_TRUST_FORMATTER=1 pi

# Capture a debug session (default is OFF — no files written unless opted in):
PI_EDIT_GUARD_DEBUG=1 pi
# Full content + file snapshots for richer triage:
PI_EDIT_GUARD_DEBUG=1 PI_EDIT_GUARD_LOG_FULL=1 PI_EDIT_GUARD_LOG_SNAPSHOTS=1 pi

There's no kill switch that disables the extension entirely — the guard's design assumes it's better to have it on with a noisy error message than to silently let edits fail with Pi's generic "Could not find the exact text" message. If you really want to disable it, pi uninstall @lucascardozo/pi-edit-guard.

What it does

Layer 1 — tool_call (before native edit runs)

For each oldText in the batch:

  1. Counts line-anchored literal occurrences (native edit semantics: match must start at the beginning of a line)
    • 1 unique → pass through
    • 0 → continue to step 2
    • 1 → block with examples

  2. Whitespace-normalized exact match (strip leading spaces per line)
    • 1 unique → auto-fix path (silent, see below) or block if auto-fix can't apply
    • 1 → block with examples

    • 0 → continue to step 3
  3. Char-level Levenshtein per line, averaged across the block
    • 1 candidate above threshold (default 0.90) → block with similarity
    • 1 above threshold → block with examples

    • 0 above threshold → block with best-match hint

Auto-fix (silent success) — when the cascade resolves to unique-drift with a uniform leading-space shift, pi-edit-guard mutates the edit in place:

  • oldText is replaced with the file's verbatim block (the cascade already guarantees this matches).
  • newText is shifted by the same delta, per non-blank line.
  • For the cases auto-fix declines (non-uniform drift, tabs, large delta), the model gets a clear verdict + the file's verbatim block. To opt out of autofix entirely (e.g. when running pi-autoformat alongside), set PI_EDIT_GUARD_TRUST_FORMATTER=1; the guard only validates, then passes newText verbatim to native edit (see Configuration table).

No-op detection — when oldText === newText, the edit would make no change. Pi's native edit rejects this with a misleading "No changes made... special characters or text not existing" message. pi-edit-guard catches it before the cascade and returns a clear, actionable verdict so the model knows it sent a no-op.

  • The model receives a normal edit-success result. No error message, no retry cost.

Auto-fix declines (and falls back to the block path) with a specific reason when:

  • missing-textoldText or newText is empty/missing.
  • line-count-mismatcholdText has different line count than the matched file block.
  • tab-in-oldtextoldText uses tabs (spaces-only file assumption).
  • tab-in-newtext — any line of newText has a leading tab (would write mixed-indent).
  • tab-in-file-block — the matched file block uses tabs.
  • non-uniform-delta — different non-blank lines need different shifts (not a clean shift mistake).
  • zero-deltaoldText already matches (cascade would have returned ok-literal anyway).
  • delta-too-large — defensive cap of ±50 spaces exceeded.

The decline reason attaches to the EditEvaluation and is surfaced as a specific hint in the consolidated report so the model can correct on the next try instead of looping.

Layer 2 — tool_result (when native edit fails atomically)

Re-runs the cascade against the file's current state and mutates the error message in-place to surface the most probable target.

Batch semantics

Both layers iterate over input.edits[]. The atomicity rule: when the cascade resolves to auto-fixable for some edits but unfixable for others, the entire batch is blocked with a consolidated report so the model can fix everything in one pass:

Edit guard: 1 of 2 edits have issues.

Edit 2: Found 6 similar blocks (similarity ≥ 0.90).
First 3 examples:
- Lines 3-3:

});

- Lines 7-7:

});

- Lines 11-11:

});


Re-read the file and provide a more specific oldText that uniquely identifies the target block.

Fix the issues above and re-submit the entire batch. Edits already passing will be re-evaluated with the new file state.

The consolidated report is the model's single source of truth: it knows exactly which edits failed and why, and can fix all of them in one go instead of N trial-and-error rounds.

Mutates events in-place

  • tool_call: mutates event.input.edits[i].oldText and .newText in place when auto-fix applies. The Pi runtime applies mutations across handlers; native edit then runs with the corrected arguments.
  • tool_result: mutates event.content in place so custom renderers (e.g. gentle-pi's quiet-tools) pick up the enriched message. isError is set to false so renderers like quiet-tools collapse long output via their COLLAPSED_TAIL_LINE_LIMIT.

Output format

Single edit: fuzzy-match (character difference)

When auto-fix doesn't apply and the cascade surfaces a fuzzy match:

Error: Edit failed. Your oldText had a small difference from the file.

Lines 12-12. Use this block verbatim as your new oldText:
return 11;
(similarity 0.92)

Single edit: drift recovery (unfixable case)

When auto-fix declines (e.g. the file uses tabs, or the delta is non-uniform), the consolidated report includes the specific decline reason so the model knows what to fix:

Error: Edit failed. Indentation in your oldText didn't match the file.

Lines 12-16. Use these lines verbatim as your new oldText (including leading whitespace):
if (item % 2 === 0) {
  return acc + item * 2;
} else {
  return acc + item;
}
(Hint: autofix declined — tab detected in newText line 3. Replace the tab with spaces to match the file's indent.)

The block is the file's actual lines, byte-exact. The model copies them as-is — no transformation needed. The hint narrows the retry to a specific cause.

Single edit: ambiguous (with examples)

Found 13 similar blocks (literal).
First 3 examples:
- Lines 12-14:

});

- Lines 47-49:

});

- Lines 81-83:

});


Re-read the file and provide a more specific oldText that uniquely identifies the target block.

Single edit: no match (with best similarity + hint)

No sufficiently similar block found.
Best match: similarity 0.62 at line 47 (below threshold 0.90).

Closest block (lines 47-49):
  await triggerOrderStatusChanged(...)

Re-read the file to see its current contents before retrying.

When the best similarity is below the hint minimum (default 0.50), the closest block is omitted to avoid misleading the model.

Auto-format (opt-in, v0.12.0+)

When a formatter is configured for a file, pi-edit-guard skips its own auto-fix layer and lets the external formatter rewrite the file post-edit. The model sees the atomic final state (original → formatted), never the intermediate drift between newText and the formatter's output.

Config file

Create one of these (or both — project overrides global for matching keys):

  • Global: ~/.pi/agent/extensions/pi-edit-guard/config.json
  • Project: .pi/extensions/pi-edit-guard/config.json

Schema (same as pi-code-formatter):

{
  "commands": {
    "prettier": ["npx", "prettier", "--write"],
    "eslint": ["npx", "eslint", "--fix"]
  },
  "filetypes": {
    "*.ts": "prettier",
    "*.md": "prettier",
    "*.{js,jsx}": "eslint",
    "*": "prettier"
  }
}

Common formatter recipes

Copy-paste configs for the most common stacks. Drop into ~/.pi/agent/extensions/pi-edit-guard/config.json (global) or .pi/extensions/pi-edit-guard/config.json (project overrides global).

Prettier only (TypeScript / JavaScript / CSS / Markdown / HTML):

{
  "commands": {
    "prettier": ["npx", "prettier", "--write", "--ignore-unknown"]
  },
  "filetypes": {
    "*.{ts,tsx,js,jsx,mjs,cjs}": "prettier",
    "*.{css,scss,less}": "prettier",
    "*.{md,mdx}": "prettier",
    "*.{html,json,yaml,yml}": "prettier",
    "*": "prettier"
  }
}

Prettier + ESLint (format + lint-fix on JS/TS, prettier-only elsewhere):

{
  "commands": {
    "prettier": ["npx", "prettier", "--write"],
    "eslint": ["npx", "eslint", "--fix"]
  },
  "filetypes": {
    "*.{ts,tsx,js,jsx}": "eslint",
    "*.{md,css,html,json,yaml,yml}": "prettier",
    "*": "prettier"
  }
}

Biome (modern replacement for ESLint + Prettier in one binary):

{
  "commands": {
    "biome": ["npx", "@biomejs/biome", "format", "--write"]
  },
  "filetypes": {
    "*.{ts,tsx,js,jsx,json}": "biome",
    "*": "biome"
  }
}

Deno fmt (Deno projects):

{
  "commands": {
    "denofmt": ["deno", "fmt", "--quiet"]
  },
  "filetypes": {
    "*.{ts,tsx,js,jsx,json,md}": "denofmt",
    "*": "denofmt"
  }
}

Python (Ruff) — fast linter + formatter:

{
  "commands": {
    "ruff": ["ruff", "format"]
  },
  "filetypes": {
    "*.py": "ruff",
    "*": "ruff"
  }
}

Python (Black):

{
  "commands": {
    "black": ["black", "--quiet"]
  },
  "filetypes": {
    "*.py": "black",
    "*": "black"
  }
}

Go (goimports) — runs gofmt internally plus import sorting::

{
  "commands": {
    "goimports": ["goimports", "-w"]
  },
  "filetypes": {
    "*.go": "goimports"
  }
}

Mixed polyglot project (different tools per file type, wildcard for anything else):

{
  "commands": {
    "prettier": ["npx", "prettier", "--write"],
    "biome": ["npx", "@biomejs/biome", "format", "--write"],
    "ruff": ["ruff", "format"],
    "gofmt": ["gofmt", "-w"]
  },
  "filetypes": {
    "*.{ts,tsx,js,jsx}": "biome",
    "*.py": "ruff",
    "*.go": "gofmt",
    "*": "prettier"
  }
}

Important: every command must accept the file path as its last argument (after --). The extension invokes formatters as <command[0]> <command[1..n]> -- <absolute-file-path> with a 5-second timeout. If your formatter has a different flag convention, adapt the command accordingly — e.g., ["black", "-"] won't work because black expects stdin, not a path argument.

Pattern rules:

  • *.ext — glob, matches files ending in .ext (specific, wins over *)
  • /regex/ — explicit regex, the body between slashes
  • literal — literal suffix match
  • * — wildcard fallback (only matches if no specific pattern matched)

The first matching pattern wins. More-specific patterns are tried before the wildcard. Unknown command names are skipped with a console warning.

Behavior

When a formatter matches the file being edited:

  1. In tool_call: autofix is skipped (matchedFormatter propagates into processEditInput). The guard still validates that there IS a match — ambiguous/fuzzy/no-match still block with the consolidated report. The model's newText passes through verbatim.
  2. In tool_result (success only): the formatter runs via pi.exec (5-second timeout, -- separator + absolute path). If the formatter changes the file, details.{patch, diff, firstChangedLine} are rewritten so the model sees original → formatted. If the formatter fails, exits non-zero, or makes no change, the original details are kept (best-effort, never throws).

The result the model sees is the atomic final state — never the drift intermediate. This closes the "surrender" loop where the model re-reads the file to "fix" indentation it never actually broke.

Backward compatibility

If no config file exists, behavior is identical to v0.11.0: autofix runs for unique drift, blocks for everything else. The formatter integration is purely opt-in.

The pre-existing PI_EDIT_GUARD_TRUST_FORMATTER=1 flag (and --trust-formatter CLI flag) still works for projects that prefer an external formatter without the in-process rewrite — it's now redundant when a config file is present, but kept for users who already rely on it.

Debug logging (production triage)

The debug logger is off by default — no files are written to /tmp unless you opt in. To capture a session for triage, set the env vars before launching Pi:

PI_EDIT_GUARD_DEBUG=1 \                      # enable the NDJSON log (sha + length + 200-char preview)
PI_EDIT_GUARD_LOG_FULL=1 \                   # log full oldText/newText content
PI_EDIT_GUARD_LOG_SNAPSHOTS=1 \              # save verbatim file snapshots
pi

When enabled, the log goes to /tmp/pi-edit-guard-<pid>.log with one NDJSON line per cascade invocation. With LOG_SNAPSHOTS=1, snapshots go to /tmp/pi-edit-guard-<pid>/snapshots/<sha>.orig.

Fields per log entry:

| Field | Meaning | |---|---| | source | 'tool_call' | 'tool_result' — which hook fired. Pair the two to see what we intercepted vs what native returned. | | path | File path the edit targets. | | fileBytes / fileSha / filePreview | Length, sha256 (12 hex), and the file content (full when LOG_FULL is on; first 200 chars + [+N chars] when redacted). | | fileLeadingNewlines / fileTrailingNewlines | Helpful to spot BOM or trailing-newline mismatches. | | edits[i].oldTextBytes / oldTextSha / oldTextPreview / oldTextLeadingSpaces | What the model sent (full content by default; redacted when LOG_FULL=0). | | edits[i].newTextBytes / newTextSha / newTextPreview / newTextLeadingSpaces | What the model wants to write. | | edits[i].evaluationKind | ok-literal, unique-drift, fuzzy-match, ambiguous-*, no-match. | | edits[i].autofixOutcome | ok (with autofixDelta) | declined (with declineReason) | n/a. | | result | autofixed | blocked (with blockReasonBytes) | pass | pass-oversized | pass-unreadable | formatter-rewritten | formatter-noop | formatter-failed. | | formatterMatched | True when a formatter was configured and matched this file's path. Orthogonal to result — when present with result: 'autofixed', the autofix ran in trust mode (corrected oldText only, left newText verbatim for the formatter). | | autofixedCount | Number of edits silently corrected (only present when result: 'autofixed'). | | snapshotPath | Absolute path to the saved file snapshot (always present by default; absent when LOG_SNAPSHOTS=0). | | nativeError | What the native edit tool returned (only on tool_result events with isError). Shows what the model actually saw. | | formatterCommand | The resolved formatter command (only on tool_result formatter events). | | formatterApplied | true when the formatter actually changed the file content. | | formatterReason | Brief reason when formatter was skipped, failed, or no-op (exit-code-N, no-change, exec-threw). | | formatterStderr | Truncated stderr (only when formatter exited non-zero). | | formatterDurationMs | Wall-clock duration of the formatter run. |

The log rotates at 5 MB. Snapshots are capped at 200 files / 100MB total (oldest by mtime get pruned).

Custom log path

PI_EDIT_GUARD_LOG_PATH=./edit-guard.log pi

Snapshots go to ./edit-guard/snapshots/ (i.e. <dirname(log-path)>/<basename-without-ext>/snapshots/). The grouping is per log file so that all artifacts from one session live together — rm -rf /tmp/pi-edit-guard-* is enough to clean up both the log and snapshots.

Triage workflow

  1. Reproduce the issue with debug logging enabled: PI_EDIT_GUARD_DEBUG=1 pi (or +LOG_FULL=1+LOG_SNAPSHOTS=1 for richer triage).
  2. cat <log-path> | jq . (or use any NDJSON viewer).
  3. Filter by source to see what the extension intercepted (tool_call) vs what came back from native (tool_result). On tool_result with nativeError, you'll see exactly what the model saw.
  4. If snapshotPath is set, cat <snapshotPath> shows the file at edit time.
  5. Compare oldTextLeadingSpaces vs the snapshot's leading whitespace per line — this is the smoking gun for any "I copied verbatim but it doesn't match" mystery.

Configuration

| Env var | Default | Opt-out | Effect | |---|---|---|---| | PI_EDIT_GUARD_THRESHOLD | 0.90 | n/a | Similarity threshold for fuzzy matches (Level 3). Lower = more permissive. | | PI_EDIT_GUARD_EXAMPLES | 3 | n/a | Max number of example blocks shown for ambiguous cases. | | PI_EDIT_GUARD_HINT_MIN | 0.50 | n/a | Min similarity to show the closest block as hint in no-match messages. | | PI_EDIT_GUARD_DEBUG | OFF | =1 | NDJSON debug log written per cascade invocation. Off by default — no /tmp files unless opted in. | | PI_EDIT_GUARD_LOG_PATH | /tmp/pi-edit-guard-<pid>.log | n/a | Where the NDJSON log goes when enabled. Snapshots go to <dirname>/<basename-without-ext>/snapshots/ (e.g. /tmp/pi-edit-guard-<pid>/snapshots/). Only consulted when PI_EDIT_GUARD_DEBUG=1. | | PI_EDIT_GUARD_LOG_SNAPSHOTS | OFF | =1 | Save a verbatim copy of the file at edit time to <log-dir>/snapshots/<sha>.orig (e.g. /tmp/pi-edit-guard-<pid>/snapshots/<sha>.orig). Dedupe by sha, capped at 200 files / 100MB. Off by default — no snapshot directory is created unless opted in. | | PI_EDIT_GUARD_TRUST_FORMATTER | unset | =0 | Opt-in trust mode: skip autofix, pass newText verbatim to native edit. Designed for projects that run an external formatter (pi-autoformat, biome, prettier, etc.) alongside this extension. The cascade still validates; ambiguous/fuzzy/no-match still block. Also registered as --trust-formatter CLI flag for discoverability. Redundant when an auto-format config file is present (v0.12.0+); the config file is the preferred opt-in path. |

All three log flags default to OFF — no /tmp files are created unless you opt in. Set the env var to 1, true, or yes to enable that flag. 0 / false / no is also accepted (no-op with the new defaults, but explicit).

Set before launching Pi:

PI_EDIT_GUARD_THRESHOLD=0.85 pi
PI_EDIT_GUARD_EXAMPLES=5 PI_EDIT_GUARD_HINT_MIN=0.6 pi
PI_EDIT_GUARD_DEBUG=1 pi   # capture session for triage

What it does NOT do

  • It does not silently fix content differences, only indentation drift with a uniform leading-spaces shift.
  • It does not suppress errors for tabs or non-uniform drift — those fall through to the existing block+report path, now with a specific decline hint.
  • It does not change the bash, write, read, or any other tool — only edit.

Testing

A test suite is included in tests/. It is split by module so each test runs independently and is fast:

tests/
├── _framework.ts              # dependency-free assertion library
├── run.ts                     # test runner (imports all test modules)
├── autofix.test.ts            # src/autofix.ts (delta + shift logic + decline reasons)
├── whitespace.test.ts         # src/whitespace.ts
├── block.test.ts              # src/block.ts
├── debug.test.ts              # src/debug.ts (NDJSON logger)
├── matchers/
│   ├── literal.test.ts        # src/matchers/literal.ts
│   ├── normalized.test.ts     # src/matchers/normalized.ts
│   └── fuzzy.test.ts          # src/matchers/fuzzy.ts
├── evaluate.test.ts           # src/evaluate.ts
├── format.test.ts             # src/format/* (all output formats)
├── format/
│   ├── runner.test.ts         # src/format/runner.ts (fake pi.exec)
│   └── tool-result-rewriter.test.ts  # src/format/tool-result-rewriter.ts
└── extension.test.ts          # src/extension.ts (e2e via jiti, formatter flow)

Coverage:

  • Unit tests for every pure module (whitespace, block, autofix, each matcher, evaluate, format, debug)
  • E2E test that loads the extension via jiti (same loader Pi uses), registers hooks, and fires events with real files
  • Regression cases from v0.5.0/v0.6.0 (drift, fuzzy, ok, ambiguous)
  • Autofix happy path (uniform shift) + decline paths (tabs, non-uniform, MAX_SANE_DELTA, line-count, missing)
  • Decline-reason assertions per AutofixDeclineReason variant
  • Batch semantics: atomic block when one edit can't be auto-fixed
  • CRLF normalization, edge cases, max-examples limit

Run with:

pnpm test          # one-shot
pnpm test:watch    # watch mode
pnpm run typecheck

(uses Node 22+ --experimental-strip-types, no build step required)

Project structure

pi-edit-guard/
├── index.ts                 # entry point (re-export from src/extension.ts)
├── src/
│   ├── extension.ts         # composition root: default export with tool_call/tool_result hooks
│   ├── evaluate.ts          # evaluateEdit, evaluateBatch (pure cascade)
│   ├── autofix.ts           # tryAutofix — pure leading-spaces delta + shift logic + decline reasons
│   ├── mutate.ts            # in-place mutation of tool result events
│   ├── config.ts            # env var readers and constants
│   ├── debug.ts             # opt-in NDJSON debug logger
│   ├── types.ts             # shared EditEvaluation and CandidateKind types
│   ├── block.ts             # BlockExcerpt type and toBlockExcerpt adapter
│   ├── whitespace.ts        # stripLeadingWhitespace, normalizeText (spaces-only)
│   ├── formatter-config.ts  # auto-format config: loadConfig, resolveFormatters, findFormatter
│   ├── format/
│   │   ├── index.ts         # barrel re-export
│   │   ├── candidate.ts     # formatCandidate: fuzzy-match + unfixable drift
│   │   ├── ambiguous.ts     # formatAmbiguousMessage + formatExamples
│   │   ├── consolidated.ts  # formatConsolidatedReport (atomic block output)
│   │   └── no-match.ts      # formatNoMatchMessage (best-similarity hint)
│   │   ├── runner.ts        # runFormatter via pi.exec (formatter subprocess)
│   │   └── tool-result-rewriter.ts  # generateRewriteResult: original → formatted patch/diff
│   └── matchers/
│       ├── index.ts         # barrel re-export
│       ├── literal.ts       # countLineAnchoredMatches, findLineAnchoredMatches
│       ├── normalized.ts    # findNormalizedMatches
│       └── fuzzy.ts         # findFuzzyMatches, lineCharSimilarity, levenshteinDistance
├── tests/                   # (see Testing section)
├── README.md
├── LICENSE
└── package.json

Each module has one responsibility. To add a new matcher (e.g. AST-based), create a file in src/matchers/ and add it to the barrel. To add a new output format, add a function under src/format/. The cascade in src/evaluate.ts is the only place that knows about the order of matchers.

Compatibility

  • Pi: 0.84+
  • Node: 22+ (uses native --experimental-strip-types for tests, node:fs/promises for runtime)
  • TypeScript: source-only (Pi loads via jiti, no build step required)

Tested alongside

  • gentle-pi v2.1.2 with quiet-tools.ts enabled — works correctly, custom renderer respects the mutation

Credits

The auto-format integration (v0.12.0+) is adapted from pi-code-formatter by losnappas, under the MIT License. The pattern compilation, config schema, tool_result rewriting, and pi.exec formatter runner pattern were ported from that extension. See the header comments in src/formatter-config.ts and src/format/tool-result-rewriter.ts for attribution.

License

MIT