@danielmarbach/pi-tgrep
tgrep-backed code search for pi: auto-indexes the repo and routes all grep traffic through the trigram index
Package details
Install @danielmarbach/pi-tgrep from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@danielmarbach/pi-tgrep- Package
@danielmarbach/pi-tgrep- Version
0.2.3- Published
- Sep 18, 2026
- Downloads
- 494/mo · 494/wk
- Author
- danielmarbach
- License
- MIT
- Types
- extension
- Size
- 92.9 KB
- Dependencies
- 0 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./extensions"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-tgrep
tgrep for pi: auto-indexes your repo
with a trigram index and makes sure every search the LLM performs, whether through the grep
tool or through shell commands, runs on tgrep instead of scanning every file.
Why
grep and ripgrep re-scan the whole tree on every query (O(total bytes) each time). On large
repos that is the slowest thing an agent does all day. tgrep keeps a trigram index with a
persistent, file-watching server and answers the same queries orders of magnitude faster,
while staying ripgrep-compatible (same flags, same --json events, same ignore rules).
pi's grep tool is the code-search path of least resistance for a model. pi-tgrep turns that
path into the fast one and closes the side doors.
What it does
- Auto-index: on
session_startit finds the enclosing git repo and ensures atgrep servedaemon is running for it (spawned detached, builds the index if missing, and the file watcher keeps it fresh). The server is shared across sessions and with other tgrep clients..tgrep/is added to.git/info/excludesogit statusstays clean. - Owns the
greptool: registers a tool namedgrepwith the identical schema (pattern,path,glob,ignoreCase,literal,context,limit) and identical output contract. The model callsgreplike always, but the results come from the trigram index. Every result carriesdetails.enginefor provenance, plusdetails.fallback(why ripgrep served the query: index still building, no binary, or error) anddetails.truncatedwhen the match limit kicked in. When the repo index exists, searches under the repo root automatically pass--index-path <index dir>, because tgrep only looks for the index next to the searched path, so a subdirectory search would otherwise silently fall back to scanning every file. - Guards the shell: a
tool_callhook inspectsbashand MCP shell-executing tools (ctx_execute,ctx_execute_file,ctx_batch_execute, bare or namespaced likemcp__context-mode__ctx_execute, extend viaPI_TGREP_WATCH_TOOLS), pipeline-aware:- commands are split on top-level
|; each segment is judged on its primary binary; - grep/
rgscan segments (pattern + path present) are transparently translated totgrep search(the CLI'ssearchsubcommand — it answers from an existing index or scans directly, while the bare default query mode can hang when no server is running) with quote-preserving re-emission ('foo|bar'stays quoted, translated globs are always quoted,sudo/envprefixes survive); - translated bash commands are stamped onto the tool result (
details.engine: "tgrep"plus the rewritten and original commands), so session logs andnpm run analyzeattribute shell tgrep usage directly; - the index directory is resolved for the directory the command actually runs in — the
leading
cdtarget when there is one, otherwise the session cwd — socd /other/repo && grep …searches that repo's index, not the session repo's; when a valid index directory exists, translated commands whose positional paths are all relative get--index-path '<index dir>'injected (any absolute positional, or an explicit--index-path, skips the injection); without an index thesearchscans the files directly (tgrep prints its own notice) — a bare query-modetgrep, which would rebuild an index beside the searched path or hang, is never emitted;-e/--regexp/-f/--filepattern arguments are tracked so a pattern never masquerades as a path; - grep/
rgstdin post-filters (… | grep -v x) are left verbatim: no tree scan, no block; ctx_execute/ctx_execute_fileshell code is checked line by line; heredoc bodies are never rewritten, and inside heredoc-containing blocks family command lines block instead;- non-shell
ctx_execute/ctx_execute_filecode (e.g. JavaScript) is scanned forchild_processexec/spawn calls (exec,execSync,execFile,execFileSync,spawn,spawnSync): an embedded shell grep inside a backtick template or double-quoted string is translated in place, while single-quoted, escaped, interpolated, or statically unextractable commands are blocked, soexecSync('grep -rn …')can no longer bypass the index; ctx_batch_executeentries are checked individually (the entry's label appears in block reasons);- output-side redirects are preserved with fd numbers still glued to their redirect
(
2>/dev/null,2>>file,2>&1), so the fd digit never leaks into tgrep as a search path; ;,&&,||, background&, backticks,$(), and stdin redirects (<) are blocked;- unknown flags are blocked with a reason that redirects the model to the
greptool; BRE-only patterns (\(,\1, …) andag/ack/pt(flag semantics diverge from rg/tgrep) run the original command untranslated, exact but slow; underPI_TGREP_BASH_POLICY=blockboth are still blocked; zgrep,git log --grep, and filenames containing "grep" are never touched.
- commands are split on top-level
- Degrades gracefully: no tgrep binary means it is fully dormant (built-in grep untouched,
with a one-time offer to
brew install tgrep). Index still building falls back to ripgrep so results are never silently incomplete. Server dead falls back to tgrep's own on-disk index.
Install
# from a local checkout
pi install /path/to/pi-tgrep
# from git
pi install git:github.com/danielmarbach/pi-tgrep
# from npm
pi install npm:@danielmarbach/pi-tgrep
Requires the tgrep binary (brew install tgrep, or cargo from the tgrep repo). If it is
missing, pi-tgrep offers to install it via Homebrew once (see PI_TGREP_AUTO_INSTALL).
For development, skip the install and load it directly:
pi -e /path/to/pi-tgrep/extensions
Configuration
All configuration is via environment variables.
| Variable | Default | Meaning |
|---|---|---|
PI_TGREP_DISABLED |
– | 1 disables the extension entirely |
PI_TGREP_AUTO_INSTALL |
ask |
ask | never | always: brew install when tgrep is missing; ask prompts once and remembers (~/.cache/pi-tgrep/auto-install.json) |
PI_TGREP_BASH_POLICY |
translate |
translate (translate scan segments in pipelines, block the rest) | block (block all grep-family shell use) | warn (allow, notify) | off |
PI_TGREP_WATCH_TOOLS |
– | additional tool names to watch, comma-separated, additive to the default set (bash, ctx_execute, ctx_execute_file, ctx_batch_execute); names match bare or namespace__name |
PI_TGREP_SERVE_ARGS |
– | extra args passed to tgrep serve (e.g. "--exclude vendor --no-watch"); split on whitespace, quotes are not parsed, so individual args cannot contain spaces |
PI_TGREP_INDEX_PATH |
tgrep default (<repo>/.tgrep) |
index directory override, honored everywhere: serve, status/stop/reindex, the grep tool, and shell injection; relative values resolve against the repo root |
PI_TGREP_SCOPE |
repo |
repo keeps the server after the session ends; session stops servers this session started on shutdown |
Commands
| Command | Action |
|---|---|
/tgrep-status |
show server/index status for the current repo |
/tgrep-reindex |
stop the server, rebuild the index, restart |
/tgrep-stop |
stop the server for this repo |
The footer shows tgrep: N files (or tgrep: indexing…) while active.
Architecture
extensions/index.ts factory: config → binary check → session lifecycle → hooks → commands
src/config.ts env-var parsing
src/tgrep-client.ts binary discovery, status parsing (both output shapes), serve.json
discovery + PID liveness, stop (SIGTERM→SIGKILL; tgrep has no stop cmd)
src/server-manager.ts repo-root detection, detached `tgrep serve` spawn, .git/info/exclude
maintenance, readiness monitor, per-scope shutdown, reindex
src/grep-tool.ts the grep override: TypeBox schema identical to the built-in, tgrep
--json streaming (ripgrep event schema, parsed 1:1), built-in truncation
rules via pi's own helpers, abort handling, rg fallback while indexing
src/bash-policy.ts shell detection (quote-aware), pipeline segmentation, rg→tgrep
pass-through, grep→tgrep flag translator with quote-preserving
re-emission, verbatim fallback or guided blocks for the rest
Output parity
The override keeps pi's grep output contract byte-for-byte where it matters: path:line: text
matches, path-line- context lines, No matches found, the [N matches limit reached…] and
truncation notices, and details fields (matchLimitReached, truncation, linesTruncated).
The only additions are metadata: details.engine: "tgrep" | "rg-fallback",
details.fallback: { reason: "indexing" | "no-binary" | "error", message? }, and
details.truncated.
Safety notes
- GNU grep's
-ris recursive, but tgrep's-ris--replace, so the translator drops-r/-Rinstead of passing them through (tgrep walks recursively by default). - Plain
greppatterns using BRE-only syntax (\(,\1, …) are not translated. tgrep's default engine is a Rust regex (ERE-like), so a translation would silently match differently; the original command runs instead.-E/egrepneed no flag;-F/fgrepmaps to-F;-P/--perl-regexpmaps to tgrep's-P(PCRE2). - During a from-scratch index build the tgrep server answers from an empty index, so searches
temporarily use ripgrep until
Indexing: complete, favoring correctness over speed.
Validation
npm run typecheck # npx tsc --noEmit -p tsconfig.json
npm test # node --test over harness.mjs + the *.test.mjs suites
The test suites shell out to the real tgrep binary (brew install tgrep), so they need it on
PATH locally and in CI (see .github/workflows/ci.yml, which runs on macos-latest for that
reason).
node scripts/analyze-pi-sessions.mjs (or npm run analyze) mines ~/.pi/agent/sessions/
for the real numbers: indexed vs fallback vs builtin grep traffic, zero-result rate, latency
p50/p95, search→read conversion, and how often the shell policy translates or blocks.
Translated shell greps are identified by the details.engine: "tgrep" stamp the extension
adds to the bash tool result; sessions recorded before the stamp existed count translated
greps as plain grep/rg.
See test/e2e.md for headless pi -p scripts and test/tgrep-cli-notes.md for the verified
tgrep CLI ground truth this implementation relies on.
License
MIT