jevgrep

CI Latest release

grep lines by what they mean.

jevgrep scores every line with a language model instead of matching it against a pattern, so you can search an English log in Chinese, find “a line that reports an error to the user” without guessing how it was worded, and pipe the result into anything that already reads grep.

It is not static analysis. There are no rules, no AST, no language support to wait for — if you want to match code structure, reach for semgrep; jevgrep reads a line the way you do when you skim a log.

Every non-blank line you search is sent to api.typesafe.ai to be scored. jevgrep never sends .git/, credential-shaped files or binaries — see Privacy and cost.

$ jevgrep -n "a disk or filesystem error" examples/app.log
4:2026-09-18 03:12:19 ERROR write /var/lib/pg/base/16384: input/output error
6:2026-09-18 03:13:44 WARN  smartd: 1 uncorrectable sector on /dev/nvme0n1
9:2026-09-18 03:16:33 ERROR no space left on device while flushing WAL

grep ERROR finds three lines in that file. It prints the one about a declined payment, which is not a disk error, and it misses line 6 — a WARN about a disk about to fail — which is. And the meaning does not have to be written in the same language as the lines:

$ jevgrep -n "a problem signing in or with authentication" examples/tickets.txt
3:#814 パスワードを再設定するメールが届かない
5:#816 二段階認証のコードが毎回エラーになる

Both commands run as they are written, against files in this repository.

Install

curl -fsSL https://sijiaoh.github.io/jevgrep/install.sh | sh

It picks the archive for your platform, checks it against the release’s checksums.txt, installs nothing if that check fails, and tells you where the binary went. Two environment variables change what it does:

With Go:

go install github.com/sijiaoh/jevgrep/cmd/jevgrep@latest

Or download an archive for Linux, macOS or Windows from Releases and put the binary on your PATH — this is the way to install it on Windows.

Then, whichever way you installed it:

jevgrep --version

If that says the command is not found, the directory it was installed into is not on your PATH. install.sh says so when it happens, and prints the line to add and the file to add it to.

Releases also carry a GitHub build provenance attestation, which says the archive came out of this repository’s release workflow — something checksums.txt, published alongside the archives, cannot tell you. The command to check it is in SECURITY.md.

Quick start

The commands below search examples/app.log and examples/tickets.txt, two small files kept in this repository so that the output shown here is reproducible. Clone it to follow along, or point them at a log of your own — nothing depends on the fixture except the exact lines that come back.

1. Get a key. jevgrep talks to the TypeSafe API, and needs a key of your own.

$ jevgrep --login
jevgrep: paste a key from https://console.typesafe.ai/keys (it will not be echoed)
TypeSafe API key: 
jevgrep: key saved to /home/you/.config/jevgrep/api_key

export TYPESAFE_API_KEY=… works too, and takes precedence over the stored file.

2. Price it before you spend it. --dry-run sends nothing at all, and does not even need a key:

$ jevgrep --dry-run "a disk or filesystem error" examples/app.log
dry run: nothing was sent
  files                 1
  lines read           10
  blank                 0  never sent
  questions            10  1 meaning per line
  cached                0  already scored
  to send              10
  batches              ~1
  input tokens       ~743
  cost         ~$0.000031  at $0.042 per 1M input tokens

On a cache that already has these lines, cached and to send swap over and the cost is $0 — that is the point of the cache, not a bug in the estimate.

3. Search a log. -p prints the probability the model gave each line, which is the number -t is compared against:

$ jevgrep -np "a disk or filesystem error" examples/app.log
4:0.96:2026-09-18 03:12:19 ERROR write /var/lib/pg/base/16384: input/output error
6:0.80:2026-09-18 03:13:44 WARN  smartd: 1 uncorrectable sector on /dev/nvme0n1
9:0.90:2026-09-18 03:16:33 ERROR no space left on device while flushing WAL

Scores come from a model that gets updated. The lines a search selects are stable; the exact probabilities may drift by a few hundredths.

4. Search in another language. The meaning and the lines never have to agree on one:

$ jevgrep -n "a problem signing in or with authentication" examples/tickets.txt
3:#814 パスワードを再設定するメールが届かない
5:#816 二段階認証のコードが毎回エラーになる

5. Search a tree. -r obeys .gitignore, and skips hidden files, binaries and credential-shaped files. Price it first — a tree is a great many more lines than a log file:

$ jevgrep --dry-run -rn "code that retries a failed request" .
$ jevgrep -rn "code that retries a failed request" .

6. Read a pipe. Pass - as the PATH and jevgrep reads standard input:

$ head -3 examples/app.log | jevgrep "a request that succeeded" -
2026-09-18 03:11:07 INFO  GET /healthz 200 3ms

7. Run it again for free. Scores are cached on disk, so the second run of the same search sends nothing. --stats says what a run actually cost:

$ jevgrep --stats -n "a disk or filesystem error" examples/app.log
4:2026-09-18 03:12:19 ERROR write /var/lib/pg/base/16384: input/output error
6:2026-09-18 03:13:44 WARN  smartd: 1 uncorrectable sector on /dev/nvme0n1
9:2026-09-18 03:16:33 ERROR no space left on device while flushing WAL
jevgrep: stats
  files                1
  lines read          10
  blank                0  never sent
  questions           10
  cached              10  100% of the questions
  sent                 0
  requests             0  0 batches, 0 retried
  failed               0
  input tokens         0
  cost                $0  at $0.042 per 1M input tokens
  elapsed           0.0s
  cache        /home/you/.cache/jevgrep  (delete it to clear)

Privacy and cost

What leaves your machine

Every non-blank line of every file you search, and the meaning you searched for. A line is cleaned up before it is sent — bytes that are not UTF-8 are replaced, and a line longer than 4 KB is truncated — but none of that reaches the output: what jevgrep prints is the line exactly as it was read. Blank lines are never sent, and never match.

What never leaves it

A walk never opens .git/, .ssh/, .env, .env.*, *.pem, *.key, *.p12, *.pfx, or an extensionless id_* file such as id_rsa. No option reopens them-g is a filter, not a key, and neither --hidden nor --no-ignore is consulted before those rules have had their say. Naming such a file on the command line yourself is the only way to search one, which is a thing you can only do on purpose. Binary content is skipped even when the path was named explicitly.

The cache

The cache holds a hash of each (line, meaning), the probability that came back, and the day it arrived — no lines, no meanings, no file names. --stats prints the directory it is in so that you can delete it; --no-cache skips it entirely for a run. A cached answer is believed for 30 days, then asked again — the model behind jev-latest moves, and nothing else would ever wash a stale answer out, so “the second run is free” stops being true on the 31st day.

Cost

You are billed per line searched. --dry-run prices a search without sending any of it, and --stats reports what a run really cost, however it ended — including after Ctrl-C.

As of 2026-09-20, --stats reported $0.000036 for the 10-line log file above, and --dry-run priced one meaning against this repository’s own tree — 16,315 non-blank lines across 100 files — at ~$0.0310. The two numbers for one search do not have to agree: --dry-run is jevgrep’s own estimate and marks every figure it works out with a ~, while --stats prefers what the API said it charged. When they differ by a few percent, the reported one is the one that is right.

Options

Option Description
-e, --meaning MEANING Add a meaning; repeat to match any of them
--and MEANING Also require this meaning on the same line
--not MEANING Reject the lines that have this meaning
-v, --invert-match Select the lines that do not match
-t, --threshold NUM Match at a score of NUM or above (default 0.5)
-r, --recursive Search the files under each directory
-g, --glob GLOB Search only the paths matching GLOB, or skip !GLOB
--hidden Also search hidden files and directories
--no-ignore Do not obey .gitignore and .ignore files
-n, --line-number Prefix each output line with its line number
-H, --with-filename Print the file name with each output line
-h, --no-filename Never print the file name
-l, --files-with-matches Print only the name of each file that matched
-L, --files-without-match Print only the name of each file that did not match
-c, --count Print only the number of matching lines per file
-q, --quiet Print nothing; stop at the first match
-m, --max-count NUM Stop after NUM matching lines per file
-A, --after-context NUM Print NUM lines after each matching line
-B, --before-context NUM Print NUM lines before each matching line
-C, --context NUM Print NUM lines before and after each matching line
-p, --score Print each line’s score before its text
--json Print one JSON object per line of output
-Z, --null Terminate each file name with a NUL byte
--color[=WHEN] Color output: always, never, auto (default “auto”)
--dry-run Estimate what would be sent and what it costs
--stats Report lines, requests, cost and time on stderr
--no-cache Do not read or write the local score cache
--model NAME Model that scores the lines (default “jev-latest”)
--login Store an API key for later runs, then exit
-V, --version Print the version and exit
--help Print this help and exit

-h is grep’s --no-filename, not help. Ask for help with --help.

With no PATH, jevgrep reads standard input, or searches . under -r. With PATH as -, it reads standard input. Exit status is 0 if a line matched, 1 if none did, 2 on error — so if jevgrep -q …; then works the way it does with grep.

-e may be repeated to match any of several meanings; --and requires another meaning on the same line, and --not rejects one. Every meaning is scored and compared against -t separately, but -p has room for one number: it prints the highest of the -e meanings’ scores, not the --and and --not ones, which qualify a match rather than being what you asked to see. -v does not change it either — inverting happens to the verdict, not to what the model measured. --json reports every meaning’s score.

Changing -t costs nothing: the threshold is compared locally, against scores that are already cached, so tightening or loosening a search you have already run sends no requests at all.

Combining with other tools

Print the names of the files that matched, NUL-separated, and act on them:

jevgrep -rlZ "code that writes to the database directly" . | xargs -0 ls -l

--json prints one object per line of output — type, file, line, text, score, scores, selected — so that a program can read what a terminal would have shown. score is absent on a line that has none, and selected is false on a context line:

jevgrep -r --json "a TODO that admits something is broken" . \
  | jq -r 'select(.selected) | "\(.file):\(.line) \(.text)"'

Pick a result interactively, and open it:

jevgrep -rn "the place this program parses its command line" . \
  | fzf --delimiter=: --preview 'sed -n "{2}p" {1}'

Give an AI coding agent a way to find code by description instead of by name: point it at jevgrep -r --json MEANING . as a retrieval tool, and it gets file, line, text and score per hit without having to guess identifiers.

How it works

Every line becomes a yes/no question — does this line mean X? — and the model answers with a probability. From there it is arithmetic and plumbing:

  1. Lines are batched and sent; blank lines never are.
  2. Each line comes back with a probability between 0 and 1.
  3. -t compares it locally, so changing your mind about the threshold is free.
  4. Batches finish out of order; jevgrep prints every line in input order regardless.
  5. Every score is written to the on-disk cache, so the next run asks for less.

This is why searching across languages works at all: the model is reading the meaning, not the bytes, and a Japanese support ticket and an English meaning are the same question to it.

The default threshold of 0.5 and the number of lines per request are measurements, not preferences — they were picked from a labelled corpus and re-derived by make calibrate. The corpus, and the reasoning about why a balanced corpus alone cannot pick a threshold for unbalanced real input, is in internal/calibration.

Limits

Contributing, security, license