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
Package details
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.
- Queries see your latest save. Every search passes
--refresh wait.zgdefaults tooffin direct mode andbackgroundin server mode, so without this the agent edits a file, searches for it, and gets stale hits - and has no way to know. - No CUDA roulette. Indexing passes
--device cpuon every platform except macOS (metalworks there), and exportsZVEC_GREP_DEVICE=cpufor the child process when nothing else pinned a device. On linux x64zg'sautoresolves to the CUDA execution provider; a faileddlopenpoisons every later ONNX session in that process (transformers.jscaches 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. - Index builds do not pass
--mode direct. If a daemon already owns the workspace, upstream rejects the build withDAEMON_LEASE_ACTIVE.autoworks with and without a daemon. - 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.
- Credentials never reach the transcript.
details.commandis what lands in the session file, so--api-keyvalues are redacted there. PreferZVEC_GREP_API_KEY. - Deleting an index needs a human. The tool refuses
drop: truewithoutconfirm: true;/zg:dropasks, and refuses to act when there is no UI to ask in. .zvec-grep/is gitignored before it is created. The check asks git itself (git check-ignore), so a globalcore.excludesFile,.git/info/excludeor a parent.gitignorecounts, 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-grepsays "not ignored",check-ignore -q .zvec-grep/says ignored).- A half-built index is reported as a failure. When every file fails to embed,
zgcan leave a 0%-coverage directory; the plugin says so instead of reporting success. - 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 waitfixes it. And queries run from your directory, sozgfinds the nearest index the same way it does on the command line; only.pi/zvec-grep.jsonis 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. - It stays quiet in directories that are not projects. The startup prompt and the footer
✗ no indexonly 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:indexworks, and it warns once when the scope looks accidental (no VCS, >25 subdirectories, no glob/type filters). - A blocked
postinstallis not a dead end. npm hosts that gate lifecycle scripts (this machine printsnpm 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.
zgmatches 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": falsefor rawzgoutput.
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:
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);node_modules/@zvec/zvec-grepnext to this package or in your project (works if you add it yourself);zgonPATH.
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) orzg auth grant(persistent); the plugin surfaces the difference between "not authorized" and "bad key", whichzgreports 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
.gitignoreline. - Managed
--rggoes throughzg'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、生效配置(每个值都标来源)。
三条与墙内环境直接相关的实测结论,都是这个包存在的主要理由:
- 模型下载:
zg默认从 huggingface.co 拉模型,直连不可达时逐文件失败并留下 0% 的半成品索引。 本包 postinstall 从魔搭镜像预热 potion(31MB,实测 2.5 秒),并按字节校验(镜像对缺失文件返回 15 字节占位正文而不是 404),失败不阻断安装。 - CPU 后端:
zg的auto在 linux x64 上解析成 CUDA EP。无 GPU 时 dlopen 失败会把同进程后续 所有 ONNX session 一起毒死(transformers.js 缓存了 rejected promise),表现为按文件无限重试、 每次在堆外漏约 13MB,最终被内核 OOM 杀掉。本包的 index 一律带--device cpu(macOS 保留 auto, metal 能用),并为子进程注入ZVEC_GREP_DEVICE=cpu。 - 模型选型看语言,不看维度:代码用
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.