grep for what code does, not what it's called.
Gate PRs on it. Run only the tests a diff can affect.
jgrep "catches an error and silently ignores it" src/
No index. No embeddings. No LLM round-trips. A whole src/ tree in ~2 s for about a cent.
On five OSS repos, --tests picked 12% of the test files and still caught over 90% of the
tests each commit's author changed, for $0.11 across 60 commits (bench).
Not @dzhng/jevgrep (jg), a separate code-discovery CLI.
This project is jgrep, published on npm as jevgrep.
| you want to find⦠| grep / rg |
embeddings | an LLM | jgrep |
|---|---|---|---|---|
| an exact name or string | β instant | meh | π’ $$ | use grep |
| "code that swallows errors" | β | β fuzzy | β slow | β 2 s |
| "endpoint with no auth check" in my diff | β | β | β $$ | β Β’ |
| needs an index / vector DB | no | yes | no | no |
jgrep runs on Jev, through TypeSafe or
OpenRouter, a System One model: it never
generates text, it answers typed yes/no questions with calibrated
probabilities, in parallel, at $0.042 per million input tokens with output free.
jgrep packs 16 code chunks and 16 questions into one request and turns the
probabilities into file:line hits.
npm i -g jevgrep # installs the `jgrep` command
jgrep init # paste a TypeSafe or OpenRouter key, pick where to keep it, donejgrep init verifies the key against the API, stores it with chmod 600,
and optionally teaches Claude Code / Codex to use jgrep. Get a key at
console.typesafe.ai, or use an OpenRouter key
(openrouter.ai/keys); see Other endpoints.
No credits yet? An OpenRouter key works the same (Other endpoints),
and jgrep --estimate ... shows what a run would cost before spending anything.
Prefer not to run init?
export TYPESAFE_API_KEY=... # env
echo 'TYPESAFE_API_KEY=...' >> .env # per project
mkdir -p ~/.config/jgrep && echo 'TYPESAFE_API_KEY=...' > ~/.config/jgrep/env # global
export OPENROUTER_API_KEY=sk-or-... # or an OpenRouter keyGot Jev through OpenRouter instead of TypeSafe? Set OPENROUTER_API_KEY
(sk-or-... β jgrep init detects it) instead of TYPESAFE_API_KEY; jgrep
sends requests to OpenRouter's System One endpoint with the matching model
id. Answers are cached by logical model name, so results are shared with the
TypeSafe endpoint. Requests to openrouter.ai also carry OpenRouter's
app-attribution headers (HTTP-Referer, X-OpenRouter-Title and its older alias
X-Title, both jgrep, X-OpenRouter-Categories: cli-agent) so jgrep appears in its app rankings;
no other endpoint gets them.
export OPENROUTER_API_KEY=sk-or-...
jgrep "reads a CSV file" src/JGREP_ENDPOINT overrides the URL for either key β point it at a local stub
for tests or CI, without touching typesafe.ai or openrouter.ai:
export TYPESAFE_API_KEY=test-key JGREP_ENDPOINT=http://127.0.0.1:8765
jgrep "reads a CSV file" src/JGREP_MODEL overrides the model id, so any server that speaks the System One
protocol works, including local ones. A loopback JGREP_ENDPOINT needs no key:
export JGREP_ENDPOINT=http://127.0.0.1:11434/v1/systemone JGREP_MODEL=nimble # e.g. Ollama
jgrep --tests HEAD~1Answers are cached per model id, so switching models never reuses another
model's answers. jev-latest is an alias TypeSafe moves to each new release, and
cached answers stay with the alias: delete ~/.cache/jgrep after a move, or pin a
version with JGREP_MODEL=jev-1.14.0 (on OpenRouter, typesafe/jev-1.14).
jgrep "reads user input without validating it" app/
jgrep -C "parses a JWT or decodes a base64 token payload" src/ # -C prints the chunk
jgrep -t 0.9 "builds an SQL string by concatenation" . # stricter
jgrep -a -t 0 "is dead code nothing calls" lib/ | head # everything, best firstjgrep --diff --staged "leaves debug output such as console.log"
jgrep --diff origin/main "adds an HTTP endpoint that has no auth check"
jgrep --diff origin/main "changes billing logic without touching a test"Exit status is grep's: 0 matched, 1 nothing matched, 2 jgrep could not run
(bad key, API down, malformed response). In CI keep the three apart: a plain !
would turn an outage or an expired secret into a passing check.
- run: npm i -g jevgrep
- name: no unauthenticated endpoints
env: { TYPESAFE_API_KEY: "${{ secrets.TYPESAFE_API_KEY }}" } # or OPENROUTER_API_KEY
run: |
set +e
jgrep --diff "origin/${{ github.base_ref }}" "adds an HTTP endpoint that has no auth check"
case $? in
0) echo "::error::jgrep found a match"; exit 1 ;;
1) ;; # clean
*) echo "::error::jgrep failed to run"; exit 1 ;;
esacThe same gate as a one-liner. For test selection use mode: tests instead
(see jgrep-action).
- uses: actions/checkout@v5
with: { fetch-depth: 0 }
- uses: kyu1204/jgrep-action@v1
with: { mode: diff, rule: "adds an HTTP endpoint that has no auth check", api-key: "${{ secrets.TYPESAFE_API_KEY }}" }jgrep --tests origin/main | xargs bun test # or vitest / pytest / go test
jgrep --tests --staged -a # every test file with its probabilityThree layers, cheapest first: tests named after a changed file (foo.ts β foo.test.ts)
and tests that import a changed module (or the package root, when the package entry file (src/index.*, index.* or __init__.py) changed) are selected in code; the rest are asked of Jev
with the compacted diff (source files only, changed lines only) and each test file's
imports and test names, one Noul per file. Default threshold is 0.5 here because a
missed test costs more than an extra one. Run the full suite afterwards; this is for the
fast first signal.
Measured on a 142-file TypeScript suite, 3 commits touching 12 source files (2026-09-21):
| files | test cases | wall time | |
|---|---|---|---|
| full suite | 142 | 1,420 | 18.9 s |
jgrep --tests HEAD~3 |
47 (12 by name, 27 by import, 8 by Jev) | 536 | 12.6 s |
Selection itself: 7 requests, 56k tokens, $0.0024, 1.0 s. The suite above is fast, so runner startup dominates; the ratio matters more on suites that take minutes.
On five OSS repos (hono, zod, fastify, flask, requests; 60 commits that changed both source
and tests), --tests selected 12% of test files and caught 93% of the tests each commit's
author had touched, versus 45% from name, import and package-root matching alone; $0.11 total. Method and
per-commit rows: bench/tests.
Every row becomes one state. One description works like grep; a JSON file of Jev questions (noul, choice, score) adds one answer column per question.
jgrep --rows creators.csv "beauty is the main content of this account"
jgrep --rows creators.csv --questions beauty.json --out scored.csv{
"beauty": { "type": "noul", "instructions": "Is beauty the main content of this account?" },
"category": { "type": "choice", "instructions": "Dominant sub-category?",
"criteria": { "skincare": "skin care", "makeup": "cosmetics", "other": "not beauty" } },
"fit": { "type": "score", "instructions": "Fit for a Korean skincare seeding campaign?",
"criteria": ["no fit", "weak", "moderate", "strong", "ideal"] }
}Question objects are passed to the API verbatim, so anything Jev accepts works.
Output columns: beauty (probability), category + category_p, fit + fit_conf.
Eight creators and five questions is one request, 3k tokens, well under a cent;
see examples/. This is the "AI map-reduce" shape: scrape N
things, ask k typed questions each, filter in a spreadsheet.
Agents burn most of their tokens looking for code. jgrep hands them a short list of ranges instead of whole files. On a 115 KB module the agent read 6 KB of matching chunks instead of everything.
jgrep init # tick "Claude Code" / "Codex" to install the skill
jgrep --json "spawns a child process" src/ | jq '.[].file'Or install the skill as a Claude Code plugin (needs npm i -g jevgrep too):
/plugin marketplace add kyu1204/jgrep
/plugin install jgrep@jgrep
The skill also has the agent run a few --diff --staged rules on its own
change before committing: a second model checking the first one's work, for
a fraction of a cent.
jgrep init interactive setup
jgrep [options] "<description>" [path ...]
jgrep [options] --diff [ref] "<description>"
jgrep [options] --rows <file.csv|.jsonl> "<description>"
jgrep [options] --rows <file> --questions <q.json> [--out scored.csv]
jgrep [options] --tests [ref] [--staged] [path ...]
-t, --threshold <p> print chunks with probability >= p (default 0.7)
-C, --show print the matching chunk body under each hit
-a, --all print every chunk with its probability, best first
--json machine-readable output
--diff [ref] grep git diff hunks (working tree, or against <ref>)
--staged with --diff / --tests: staged changes only
--tests [ref] predictive test selection: test files a diff plausibly affects
--rows <file> grep rows of a CSV / JSONL file instead of code
--questions <f> with --rows: JSON of Jev questions asked of every row
--out <file> with --questions: write the CSV here instead of stdout
-b, --batch <n> chunks per request (default 16)
-c, --concurrency <n> parallel requests (default 16)
--timeout <s> per-batch deadline, retries included (default 15)
--request-timeout <s> per-attempt HTTP timeout (default 30)
--retries <n> failed attempts tolerated per batch (default 4)
--rate <req/s> global request pacing (token bucket); 0 = unlimited
--fail-fast abort on the first fatal error instead of isolating it
--estimate print requests, input tokens and cost a run would need, then
exit 0 without calling the API (no key needed)
--no-cache ignore and do not write ~/.cache/jgrep
Failures are isolated by default: a failed batch marks only its chunks errored (rows, when scoring), the rest of the run continues, and answers already paid for are kept in the cache.
- Retries: statuses 408/429/5xx and transport errors (timeouts,
ECONNRESET/ETIMEDOUT/ECONNREFUSED) retry with full-jitter exponential backoff (500 ms base, 30 s cap),--retriestimes (default 4, so 5 attempts). A providerRetry-Afteris honored, capped at 5 minutes. - Deadlines:
--request-timeout(30 s) bounds one HTTP attempt;--timeout(15 s) is the deadline for a whole batch including its retries β an expired batch is recorded as errored and the run moves on. - Pacing:
--rate REQ/SECspaces all requests with a token bucket (0 = unlimited). - Circuit breaker: 3 consecutive fatal failures β out of credits, bad key,
model gone, host unreachable, TLS β stop new dispatches instead of hammering
on; chunks never attempted are reported as
circuit_breaker_open.--fail-fastrestores abort-on-the-first-fatal instead of isolating.
Exit status: 0 hits, 1 none, 2 on error or when any chunk/row errored β
partial failures still report their hits, the summary line carries the error
breakdown ( Β· 4 errored (3 timeout, 1 rate_limited)), and up to 5 examples
go to stderr. Every failure carries a typed kind:
| kind | meaning |
|---|---|
insufficient_credits |
out of credits β top up with the provider |
invalid_api_key |
key rejected; the hint tells you if it worked earlier this run |
model_unavailable |
the provider does not serve this model |
rate_limited |
throttled β pace with --rate |
bad_request |
request-shape problem; the provider's body is quoted |
malformed_response |
unexpected 200 body β the API surface may have changed |
server_unreachable |
network or provider down after retries |
tls_error |
certificate problem, message quoted verbatim |
timeout |
attempt or batch deadline exceeded |
circuit_breaker_open |
never attempted β the breaker stopped new dispatches |
--json is unchanged (same contract as 0.3.0): the bare array β code mode
[{file,start,end,p,text}], rows questions mode the scored table rows (your row
fields plus the answer columns), rows single-description mode the shown hits
[{row, p, ...row fields}] with row the CSV line number. Questions mode
(--questions) includes every row β an errored row keeps its place with the
question columns empty (its flattened answer is null); single-description mode
emits only the shown hits, so errored rows never enter that array. Errors
surface through the stderr summary and exit 2.
- Files come from
git ls-files(untracked included, ignored excluded), or a directory walk. Binaries and files over 1 MB are skipped. - Chunks: each file is split at column-0 line starts into 5 to 60 line
pieces. With
--diff, each hunk is a chunk and keeps its+/-markers. - One request, 16 chunks, 16 questions:
state.chunks[]plus a Noul question per chunk, "look only at chunk c3, does it match: β¦". - Threshold: probabilities at or above
-tare printed in file order. Answers are cached by(model, question, chunk)in~/.cache/jgrep/, so the same query again is free and instant.
| repo | chunks | time | cost |
|---|---|---|---|
TypeScript CLI, src/ |
896 | 1.8 s | $0.010 |
| same query again (cache) | 896 | 0.0 s | $0 |
one module, app/lib/ |
521 | 1.6 s | $0.006 |
- Write the description in English and describe the code, not the feature: "decides whether to alert based on OCR confidence" beats "alert feature". Jev's accuracy is lower on non-English text.
- One behavior per query. Split compound questions and combine in your head
(or in a script with
--json). - Chunks are judged in isolation, so cross-file flow ("does this eventually hit the DB") will not match. Ask about the local code.
p >= 0.9is reliable,0.7-0.9is worth a look.
bun test src/ # unit tests, no network
bun run build # dist/jgrep.js, plain node, deps bundledIf jgrep saved you a file-hunting session, a β on GitHub is the best thanks.
MIT