pi-zvec-grep-ext

pi plugin for zvec-grep (zg): hybrid BM25 + embedding search and managed ripgrep in one tool, with freshness-guaranteed queries, CPU-pinned local embeddings, readiness commands, and a ModelScope mirror warm-up that works without access to huggingface.co

Packages

Package details

extension

Install pi-zvec-grep-ext from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-zvec-grep-ext
Package
pi-zvec-grep-ext
Version
0.2.7
Published
Sep 14, 2026
Downloads
497/mo · 53/wk
Author
angri450
License
Apache-2.0
Types
extension
Size
132.6 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-zvec-grep-ext

zvec-grep (zg) inside pi: hybrid BM25 + embedding retrieval and managed ripgrep, wrapped as one native tool (four actions behind a single schema), plus human commands (/zg:*) and a mirror-based model warm-up that works where huggingface.co is unreachable.

pi install npm:pi-zvec-grep-ext

Try it without installing (nothing is added to your settings):

pi -e npm:pi-zvec-grep-ext

Then start pi and run /zg:status. If zg itself is not on PATH, that command prints the one line that installs it. The tool appears as zg; ask something like "where is retry backoff handled?" and the agent will call it.

What you get

tool zg one tool, four actions: search (hybrid / fts / vector / fuse), rg (managed ripgrep, no index needed), index (build / rebuild / drop), status
/zg:status readiness ladder: node, resolved binary + version + where it came from, model cache, index coverage/embedding/dims, .gitignore state, effective config with the source of every value, and which directory config came from vs which one queries run in
/zg:index build or update: --rebuild, -g/-t filters, a bare model name, --device; streams progress
/zg:query / /zg:rg search straight from the prompt, no model turn
/zg:drop delete the index - confirms first, refuses without a UI
/zg:settings per-project knobs without editing JSON by hand
/zg:disable / /zg:enable opt a project out; "never for this project" from the startup dialog sticks

Behaviour that is guaranteed, not configured

These are the things that were wrong in the first version of this package and are now pinned by tests. Each one is a silent failure mode if dropped.

  1. Queries see your latest save. Every search passes --refresh wait. zg defaults to off in direct mode and background in server mode, so without this the agent edits a file, searches for it, and gets stale hits - and has no way to know.
  2. No CUDA roulette. Indexing passes --device cpu on every platform except macOS (metal works there), and exports ZVEC_GREP_DEVICE=cpu for the child process when nothing else pinned a device. On linux x64 zg's auto resolves to the CUDA execution provider; a failed dlopen poisons every later ONNX session in that process (transformers.js caches the rejected promise), which shows up as an endless per-file retry that leaks memory off the JS heap until the OOM killer arrives. See upstream zvec-grep#135 and transformers.js#1767.
  3. Index builds do not pass --mode direct. If a daemon already owns the workspace, upstream rejects the build with DAEMON_LEASE_ACTIVE. auto works with and without a daemon.
  4. Output is capped at pi's own tool limits (2000 lines / 50KB) at line boundaries, and the cap is announced to the model with the total line count plus how to narrow the query.
  5. Credentials never reach the transcript. details.command is what lands in the session file, so --api-key values are redacted there. Prefer ZVEC_GREP_API_KEY.
  6. Deleting an index needs a human. The tool refuses drop: true without confirm: true; /zg:drop asks, and refuses to act when there is no UI to ask in.
  7. .zvec-grep/ is gitignored before it is created. The check asks git itself (git check-ignore), so a global core.excludesFile, .git/info/exclude or a parent .gitignore counts, and no redundant line is written. Outside a git repository nothing is written at all. The probe path carries a trailing slash on purpose: a .zvec-grep/ rule is directory-only and git cannot classify a path that does not exist yet (measured: check-ignore -q .zvec-grep says "not ignored", check-ignore -q .zvec-grep/ says ignored).
  8. A half-built index is reported as a failure. When every file fails to embed, zg can leave a 0%-coverage directory; the plugin says so instead of reporting success.
  9. A stale index is not an error, and a subdirectory is not a different project. Readiness is "an index exists and can answer" (parsed from zg status), not --check-ready - a 88%-coverage index that merely needs a refresh is reported as present and the query's --refresh wait fixes it. And queries run from your directory, so zg finds the nearest index the same way it does on the command line; only .pi/zvec-grep.json is looked up at the repository root. Getting this backwards meant "no index" in exactly the layout used by this machine's literature matrices (index in .../apple-replant, git root one level up), so it has a regression test.
  10. It stays quiet in directories that are not projects. The startup prompt and the footer ✗ no index only appear where an index makes sense: a git repository, a directory with .pi/zvec-grep.json, or one that already has an index. In a plain container directory (the pi workspace here holds 97 repositories and 4.9GB) it says nothing instead of offering to index all of it. Building there is still possible - /zg:index works, and it warns once when the scope looks accidental (no VCS, >25 subdirectories, no glob/type filters).
  11. A blocked postinstall is not a dead end. npm hosts that gate lifecycle scripts (this machine prints npm warn allow-scripts ... pi-zvec-grep-ext@0.2.0 (postinstall: node scripts/warmup.js)) never run the warm-up, so the plugin re-checks the model cache before every index build: in a TUI it asks once before downloading ~32MB, and in -p/JSON/RPC it only prints the command. Nothing gets downloaded behind your back either way.

Search results come with a support verdict

zg fuses its lexical and vector routes with Reciprocal Rank Fusion and cuts at a positional limit. RRF is purely ordinal, so the displayed score carries no confidence: the authors of pi-zvec measured 0.0320 for an on-topic query against 0.0323 for "quantum chromodynamics lattice QCD gluon field tensor" in the same repository. What this package measured itself is the observable part of the same effect: a query whose terms occur nowhere in the workspace still comes back with a full page of line-cited hits (tests/live.test.ts asserts exactly that, so the claim is re-checked on every run rather than trusted from prose). matchedBy=fts+vector is not a signal either - both routes always return their top-k - and the raw cosine is dropped in zg's recall layer, so a plugin cannot read a real score. Instead the tool adds one line, computed locally from the actual cited text:

[zg-support weak] only 1/5 query terms appear in the hits; missing from hits: jitter, 429, upstream
  • ok - a real share of the query terms is present in the hits.
  • weak - terms exist but are thin in the hits, or the workspace scan hit its budget (absence is then never claimed).
  • none - the scan completed and none of the terms occur anywhere in the workspace.

Two deliberate deviations from the same idea elsewhere:

  • CJK queries are supported. A whitespace tokenizer turns a whole Chinese sentence into one giant token; terms here are character bigrams, so 重试退避 is checkable.
  • Nothing is ever suppressed. zg matches across languages - a Chinese query legitimately hits English text (measured: 生物炭改良土壤 → Biochar alleviates apple replant disease…). A lexical-absence rule that deleted hits would throw away exactly those cases, so the worst outcome here is a warning line above the results. Set "verify": false for raw zg output.

Cost: bounded in-process scan (5000 files / 12MB total / 2MB per file), cached 60s per cwd+terms. Retrieval itself stays local.

When the model keeps grepping and finding nothing

In an unfamiliar repository the agent guesses identifiers and greps: three, four, five empty rounds, each one costing a turn. After three consecutive search-shaped commands that returned nothing, the plugin nudges once - concept questions do not need the identifier first; try zg with action=search - and then stays quiet: at most twice per session, at least five minutes apart, never twice in a row, and only when the index is actually ready (advertising a tool that cannot run is worse than silence). A hit, or any use of zg, resets the streak.

ls | grep foo deliberately does not count: an empty result there means "no such entry", not "the concept is elsewhere". Bounded state machine, no suppression of anything (coach.ts, 10 tests).

What it costs (measured on a 3.6GB, GPU-less box)

Exact search and semantic search have completely different prices, and only one of them needs a model:

what you do embedding model daemon RSS
daemon running, nothing asked none 71 MB
zg with action=rg (exact, no index) no model is loaded 71 MB
action=search with local/potion-code-16m-v2 (code) loaded ~400 MB
action=search with local/multilingual-e5-small (Chinese) loaded ~600 MB
15 minutes after the last semantic query released automatically back to idle

So: semantic search is the only thing that costs RAM, and the model is the whole cost. The daemon drops it after its idle TTL (15 minutes, hardcoded - no env or flag changes it; 42 releases in this machine's log), and systemctl restart zvec-grep-mcp reclaims it immediately. Note that switching projects can briefly hold two models; the pool only trims on the next acquire.

Disk follows the file count, not the byte count: 248 indexed files here cost 21MB (~85KB/file), so a 4.9GB tree of 97 repositories would be hundreds of megabytes and a long build. Index the project you are working in, not the container directory - the plugin does this by default (see guarantee 10).

Turning semantic search off entirely

{ "semantic": false }

in .pi/zvec-grep.json (or ZG_SEMANTIC=0). Then the plugin never looks for an index, never prompts, leaves the footer empty (a setting you chose is not a status worth repeating every session - /zg:status reports it), and action=search / action=index refuse immediately with a pointer to action=rg - no zg subprocess is started, so no model can be loaded. Exact search, which is the part that needs no model anyway, keeps working. Use --rg semantics as the default and turn semantic on per project only where it earns its ~600MB.

Why the engine is not a dependency

Bundling @zvec/zvec-grep looks convenient but measured 1.3GB / 192 packages per install, of which node-llama-cpp is 703MB and is never loaded by local CPU search; onnxruntime-node adds 208MB and onnxruntime-web 92MB. And --omit=optional (440MB) breaks the install: the @zvec/zvec native bindings are an optional dependency, so the CLI dies with Failed to load prebuilt binary for linux-x64.

So zg is resolved at runtime instead, in this order:

  1. ZG_BIN, or "zgBin" in .pi/zvec-grep.json - if it points at nothing, that is an error and no fallback happens (a silently wrong binary is worse than an honest one);
  2. node_modules/@zvec/zvec-grep next to this package or in your project (works if you add it yourself);
  3. zg on PATH.

When nothing is found, /zg:status prints:

ONNXRUNTIME_NODE_INSTALL_CUDA=skip npm install -g @zvec/zvec-grep

That env var keeps the CUDA probe out of onnxruntime-node's postinstall, which is the usual reason that install dies; --ignore-scripts is not enough on its own (@vscode/ripgrep fetches its binary in a postinstall, and zg query --rg then fails at runtime).

First search on a machine that cannot reach huggingface.co

zg downloads potion-code-16m-v2 from huggingface.co on the first index. Where that is unreachable the build fails per file and leaves a 0% index. This package's postinstall warms that model from a ModelScope mirror (~32MB, 2.5s measured), validates the byte counts (mirrors return a 15-byte placeholder body rather than 404 for missing files), and never fails the install:

bash <install-dir>/fetch-model.sh              # potion-code-16m-v2, code search
bash <install-dir>/fetch-model.sh e5-small     # multilingual-e5-small q8, Chinese/multilingual
bash <install-dir>/fetch-model.sh all --verify # check the cache, download nothing
PI_ZVEC_GREP_SKIP_WARMUP=1 npm install ...     # skip the warm-up (offline/CI images)

If the host blocks lifecycle scripts the postinstall never runs; /zg:index then asks to do the same thing at the moment it matters (see guarantee 9).

Picking an embedding model

model good at measured cost
local/potion-code-16m-v2 code near-random ranking for Chinese queries (real matrix test: correct segment ranked #3, #2 was unrelated) 32MB, no GPU, ~1s/round
local/multilingual-e5-small Chinese + multilingual prose 4/4 top-1 on Chinese queries; reload leaks ~80-100MB off-heap per session, so ~500MB resident 118MB q8, CPU
qwen/text-embedding-v4 best of the three; Chinese query hits English documents 3/3 remote: quota + the query text leaves the machine

e5-small must be pinned to cpu in zg's own config, because the flag alone does not persist and daemon/MCP paths never see it:

zg config model set local/multilingual-e5-small --device cpu

Configuration

.pi/zvec-grep.json at the project root (shareable with your team); environment variables win over the file, the file wins over built-in defaults. Invalid values are dropped one by one with a note that /zg:status shows - one typo does not take the plugin down.

key default meaning
indexMode ask ask prompts once at session start, manual only reports, auto builds in the background
disabled false full opt-out for this project, including the prompt
mode auto index transport: prefer a live daemon, else direct
refresh wait wait answers from a current index; background/off are faster and can be stale
preview short cite the matched range so the model does not have to re-read the file
device cpu (macOS: auto) local embedding device
embedding zg default model used for new indexes; existing ones keep theirs
verify true the [zg-support …] line
semantic true false = exact search only: no index, no prompt, no footer line, no model ever loaded
footer true false = never write the footer status at all
gitignore true let the plugin keep .zvec-grep/ out of git
timeoutQueryMs / timeoutIndexMs 120s / 1h
zgBin unset explicit CLI path

Environment: ZG_BIN, ZG_INDEX_MODE, ZVEC_GREP_DEVICE, ZVEC_GREP_EMBEDDING, ZVEC_GREP_MODEL_CACHE, PI_ZVEC_GREP_SKIP_WARMUP.

Privacy and safety

  • Retrieval is local: local model, CPU device, no telemetry. Nothing is sent anywhere unless you ask for a remote embedding model.
  • Remote embeddings need allowRemote (per call) or zg auth grant (persistent); the plugin surfaces the difference between "not authorized" and "bad key", which zg reports identically.
  • The support scan reads file contents in-process, bounded, for the query terms only. Same bytes the index already holds; nothing leaves the machine.
  • The only file written inside your repository (besides the index) is one .gitignore line.
  • Managed --rg goes through zg's flag whitelist; no shell interpolation, no output-format rewrites.

Out of scope

Not a memory/recall system, not multi-workspace search, does not override pi's built-in grep or find, no MCP-client registration, no custom TUI rendering. Related capabilities belong in their own packages.

Development

npm run verify        # typecheck + the offline suite (what CI would run)
npm run test:live     # + builds real scratch indexes and feeds our argv to the real CLI
npm run test:unit     # everything, no network access

The live tier skips (not fails) without zg, because an unready machine is the exact case this extension has to handle. tests/fixtures.ts holds captured zg 0.2.1 output as the parser's compat tripwire; regenerate it when upstream changes shape and update buildArgs in the same commit.

中文说明

安装:pi install npm:pi-zvec-grep-ext(国内网络可走 pi install git:gitcode.com/Agricultural/pi-zvec-grep-ext)。 装完 /zg:status 看六层就绪:node、CLI、模型缓存、索引覆盖率、.gitignore、生效配置(每个值都标来源)。

三条与墙内环境直接相关的实测结论,都是这个包存在的主要理由:

  1. 模型下载zg 默认从 huggingface.co 拉模型,直连不可达时逐文件失败并留下 0% 的半成品索引。 本包 postinstall 从魔搭镜像预热 potion(31MB,实测 2.5 秒),并按字节校验(镜像对缺失文件返回 15 字节占位正文而不是 404),失败不阻断安装。
  2. CPU 后端zgauto 在 linux x64 上解析成 CUDA EP。无 GPU 时 dlopen 失败会把同进程后续 所有 ONNX session 一起毒死(transformers.js 缓存了 rejected promise),表现为按文件无限重试、 每次在堆外漏约 13MB,最终被内核 OOM 杀掉。本包的 index 一律带 --device cpu(macOS 保留 auto, metal 能用),并为子进程注入 ZVEC_GREP_DEVICE=cpu
  3. 模型选型看语言,不看维度:代码用 local/potion-code-16m-v2;中文/多语文档用 local/multilingual-e5-small(q8,实测中文 4/4,需 --device cpu),要更高质量且接受外发就用 qwen/text-embedding-v4(实测 3/3,且支持中文问命中英文文献)。potion 处理中文排序近乎随机, 别拿它当通用模型。

配套技能包(zg CLI 的完整配方、索引治理与案例): pi install git:gitcode.com/Agricultural/zvec-grep。两者不重复:插件管 pi 里的工具与生命周期命令, 技能包管 CLI 的用法细节。

License

Apache-2.0. zvec-grep itself is upstream's (@zvec/zvec-grep); this package only shells out to it.