← All posts
SEP 2026

jev CLI: Text Classification in Your Terminal, With Probabilities Instead of Prose

jev is an open-source CLI for TypeSafe AI's Jev model: ask any text a typed question, get a calibrated probability back, and branch on the exit code.

A terminal running jev noul on a support ticket and printing the probability 0.96
On this page

You have a support ticket, a commit message, a product review, or something a user typed into a form. You need a decision about it: is this angry, which team owns it, should this pipeline continue.

Today you either write brittle keyword rules, or you ask a large language model and then parse the sentence it hands back. The rules break on the first message that says “not happy”. The model works, until the day it words the answer differently and your parser quietly starts returning the wrong thing.

I have been building a third option into my terminal, and I have just released it: jev, an open-source CLI for TypeSafe AI’s Jev model.

One command, one number

jev noul "Is this customer angry?" --state "You charged me twice. Fix it now."
answer  noul  yes  0.96  ████████████████████░
model jev-1.13.0 · 280 input tokens · est. cost $0.000012 · 783 ms

That is the whole idea. Jev never writes text. You define the question and the possible answers, and the model returns a calibrated probability for each one. Your code decides what to do with it.

LLM promptjev
You get backA sentence to parseA number in a shape you defined
Same input next monthMay be worded differentlySame contract
Cost per short ticketCentsA fraction of a cent
Use in a shell scriptParse and hopeExit code 0 or 10

Install it and ask something

curl -fsSL https://raw.githubusercontent.com/shaharia-lab/jev-cli/main/install.sh | sh
# or: brew install shaharia-lab/tap/jev

The script never uses sudo, checks the download’s SHA-256 and its signature, and installs a single binary. Windows has a PowerShell one-liner, and cargo binstall jev-cli works anywhere Rust does.

Then give it a key from the TypeSafe console and ask your first question:

export TYPESAFE_API_KEY=...
jev noul "Is this message asking for a refund?" --state "Please close my account and refund me."

About a minute, and a fraction of a cent.

A semantic if statement

This is where it stops being a demo and starts being useful. A gating flag turns the answer into an exit code, so your shell already knows how to use it:

if jev noul "Is this ticket about billing?" --state-file ticket.txt --fail-under 0.7 --quiet; then
  echo "route to billing"
fi

Exit 0 means yes above your threshold. Exit 10 means no, and it is not an error: the answer is printed as usual and stderr stays empty. Every other code is a genuine failure, so a missing key or an outage can never be mistaken for a “no”. That distinction is the part most hand-rolled wrappers get wrong.

Three questions I actually run this way:

  • Does this commit message describe a user-facing change, so the pull request needs a changelog entry?
  • Is the text a user pasted into this form on topic, before it reaches anything expensive?
  • Did the model’s answer in this pipeline actually answer the question it was asked?

Three kinds of question

TypeAsk it whenYou get back
noulA property holds or it does notThe probability of yes, 0 to 1
choiceExactly one of up to 255 options must winThe winner, its confidence, and a probability per option
scoreA described scale of 2 to 10 levelsA position on your scale

You can ask all of them about the same text in one call, which is what a triage step usually needs: how urgent, which team, how upset, answered together and paid for once.

A probability near 0.5 means the model cannot tell, not “medium”. --abstain-band 0.4,0.6 turns that into its own exit code, so those cases go to a person instead of a coin flip.

Built for AI agents, not just for people

Agents were a first-class user from the first commit, not an afterthought:

  • jev spec prints every command, flag, default, exit code and example as one JSON document.
  • Every --help says when to use this command rather than its siblings.
  • jev schema prints JSON Schemas for everything jev reads and writes.
  • jev validate and --dry-run check a request offline, for free, so an agent can fix its own mistake before spending anything.

And it speaks MCP, so an agent can call it without shell quoting at all:

claude mcp add jev -- jev mcp serve

The server takes a per-call spend cap, and it touches no file unless you start it with --allow-dir.

Thousands of rows, resumable

jev batch run -f triage.yaml --input tickets.jsonl --state-field body \
  --id-field ticket_id --out results.jsonl --resume

Rows are streamed, so memory does not grow with the file. Failed rows become records rather than stopping the run, the pool backs off when the API rate limits, and --resume picks up from the output file after a crash or a Ctrl-C. There is no state file to babysit.

What it will not do

Being honest about this saves you a bad afternoon. Jev reads literally and quickly. It cannot count, do arithmetic, compare dates or look anything up, so keep that in your code and ask Jev only for the meaning. Answers are not bit-for-bit repeatable, so compare against thresholds rather than for equality, and pin a versioned model for anything that has to stay stable.

And the obvious one: jev is an independent community project. It is not affiliated with, endorsed by, or sponsored by TypeSafe AI, and you need a TypeSafe API key to use it.

Try it

curl -fsSL https://raw.githubusercontent.com/shaharia-lab/jev-cli/main/install.sh | sh
jev noul "Is this a good idea?" --state "Classify text from the command line with probabilities."

The documentation covers the question types, writing questions that work, scripting and CI, batch runs, and the MCP setup for Claude Code, Cursor and VS Code.

If the idea is useful to you, star the repository. It takes two seconds, and it is how the next person finds it. Bug reports and feature requests are welcome in the issue tracker.