mnjev-cli
v0.2.0
Published
Unofficial CLI for TypeSafe's Jev decision model. Ask a typed question, get a decision with calibrated probabilities.
Maintainers
Readme
mnjev-cli
Ask TypeSafe's Jev a question from your terminal and get a decision back, with the probability of every option.
Unofficial. Not affiliated with, endorsed by, or supported by TypeSafe AI. "TypeSafe" and "Jev" are their names, not mine. The package name is prefixed to keep it clearly out of their namespace. You bring your own API key.
The package is mnjev-cli; the command it installs is mnjev.
Jev is a System One model. It does not write text. You give it some state and a question whose possible answers are fixed in advance, and it returns one of those answers with a calibrated probability. So this is not a chatbot, it is a decision tool.
› !git --version
$ git --version · 35 chars
› is this about version control?
● yes 99% sure
yes ████████████████████ 99%
no ░░░░░░░░░░░░░░░░░░░░ 1%
1941ms
╭──────────────────────────────────────────────────────────────────────────╮
│ ›
╰──────────────────────────────────────────────────────────────────────────╯
$ git --version · 35 chars · tab completesInstall
Needs Node 22.18 or newer. No dependencies and no build step.
node --version # must be v22.18+
npm install -g mnjev-cli
mnjev --helpDeveloped and tested on macOS, against Node 22.18 and 24.19. Nothing in it is macOS-specific and the Windows code paths are handled, but it has not been run on Windows or Linux.
From a checkout
git clone https://github.com/mahavirn/mnjev-cli.git
cd mnjev-cli
npm link # puts `mnjev` on your PATH, running src/ directly
npm unlink -g mnjev-cli # to remove it laterOr skip linking entirely: node bin/mnjev.mjs --help.
Log in
Get a key from console.typesafe.ai, then:
mnjev loginThat writes ~/.mnjev/config.json with mode 0600, outside your project so it cannot be
committed by accident. Windows ignores file permissions, so mnjev login warns there
and you should restrict the file yourself or use TYPESAFE_API_KEY instead.
TYPESAFE_API_KEY overrides the file, which is what you want in CI.
Use it
Run mnjev with no arguments for the interactive interface, or pipe state in for one shot.
Just ask
Jev decides what kind of question you asked. You never name a type:
is this a database problem? -> yes/no
which team should own this? billing, support, or infra -> pick one
which fruit is yellow? A. Apple B. Banana C. Grape -> pick one
email vs chat -> pick one
how urgent is this? low|medium|critical -> a level
how angry is this customer? -> a levelJev finds your options too. Code does not decide where they are. It shotguns every plausible span of your sentence, deliberately far too many, and Jev picks which ones are real. This is the pattern TypeSafe documents: "a regex finds the candidate values in the text. Tune it to over-find. TypeSafe picks which candidate the question is asking for."
That matters on real sentences. Rules that try to find options precisely score 4/10 on longer inputs. Over-finding and letting Jev choose scores 12/12:
coffee or tea in the morning -> ["coffee", "tea"]
...should we use email or chat -> ["email", "chat"]
is this a bug in our code or a problem with the upstream API
-> ["a bug in our code",
"a problem with the upstream API"]The one thing Jev cannot do is invent levels. A rating with no levels named, like
how angry is this customer?, gets a generic not at all → extremely scale.
To override what Jev decided, state it outright. A | list, or a leading choice or
score, is taken as given:
choice which team? billing, technical
score how urgent? low|medium|criticalLoad state
State is what Jev reads. Load it when the question is about your own code or data.
| Command | What it does |
|---|---|
| /set <text> | type the state in directly |
| /load <file> | use a file as state |
| /run <command> | use a command's output as state |
| !<command> | shorthand for /run |
| @<file> | pull a file into one question, without changing the state |
| /state | show what is loaded |
| /clear | drop the state |
| /cost | requests, tokens, and dollars for the session |
| /help /exit | |
/run keeps the output of a command that fails, so /run npm test on a broken suite
loads the failure for you to ask about.
State sticks until you change it. Once loaded it is sent with every question after it, not just the next one. Jev gets less accurate as unrelated text grows, so stale state is its worst failure mode. Two things guard against forgetting: the line under the input box always names what is loaded, and every answer ends with what it was judged against.
› can penguins swim?
● yes 97% sure
464ms · judged against $ git log --oneline -10 <- nothing to do with penguinsRun /clear when you see that. Use @<file> instead of /load when you only want the
file for one question.
Pasting several lines makes them the state. That is almost always what you wanted,
and it saves you typing /set first. End a line with \ to continue it onto the next.
The interface
Tab completes commands and file paths. Typing / filters the command list under the
box as you go. ctrl-c cancels whatever is running, whether that is a question in
flight or a /run command, and ctrl-c on an empty line twice exits. History is kept
in ~/.mnjev/history and survives restarts. /set lines and pasted blocks are left out
of that file, since both exist to carry content that may not belong on disk.
A command is confirmed before it runs:
╭──────────────────────────────────────────────────────────────────────────╮
│ Run a shell command? │
│ │
│ npm test │
│ │
│ 1 yes │
│ 2 yes, and stop asking for npm │
│ 3 no │
╰──────────────────────────────────────────────────────────────────────────╯
1-3, enter for yesOption 2 remembers the program for the rest of the session only. Nothing is written to disk, so a new session asks again.
A command containing shell syntax is always confirmed, whatever you have allowed.
;, &&, |, backticks and $(...) all run more than one program, so allowing git
must not wave through git log; curl example.com | sh. Option 2 is not offered for
those, and the box says so.
Option 2 trusts the program, not the arguments. Allowing git allows anything git
itself can be made to do, and plenty of tools can be made to run other programs through
their own flags: git -c alias.x=!cmd, env X=1 sh -c ..., find -exec, awk 'BEGIN{system(...)}'.
mnjev does not try to detect those. Every one of those lists is incomplete, and a check
that catches four of them while missing forty is worse than an honest limit, because it
reads like protection. The confirmation box exists so a command does not run by accident,
not to sandbox a command you typed on purpose. Nothing in mnjev writes commands for you:
Jev returns no text, so every command run here is one you typed.
Ask several questions at once
Jev answers questions in parallel, so separate them with ; and pay for one call:
› is this a bug fix?; how risky is it? low|medium|high
is this a bug fix?
no 57% sure
yes █████████░░░░░░░░░░░ 43%
no ███████████░░░░░░░░░ 57%
unsure - 50% means it has no idea
how risky is it? low|medium|high
medium 0.63 of 0-2, confidence 33%
low ████████░░░░░░░░░░░░ 41%
medium ███████████░░░░░░░░░ 55%
high █░░░░░░░░░░░░░░░░░░░ 4%
unsure - the levels overlap, try fewer or clearer ones
1206msTypeSafe measured batching at "12.2x cheaper and 10.0x faster with no change in answers", so five questions cost roughly what one costs.
One shot
State comes from stdin, so it pipes like any other unix tool. Output stays one plain greppable line when piped:
cat err.log | mnjev noul "is this a database problem?"
cat ticket.txt | mnjev choice "which team?" "billing=payments,technical=bugs"
cat review.txt | mnjev score "how angry is this person?" "calm|annoyed|furious"Add --json for the raw API response. mnjev --help exits 0; an unknown command exits 1,
so scripts can tell a typo from an answer.
Classify a list
map runs one question over every line of stdin and prints a tab-separated row per item:
git diff --name-only | mnjev map "does this file need a test?"src/auth/login.ts yes 0.76
README.md no 0.84
src/payments/charge.ts yes 0.70
.gitignore no 0.82The columns are the item, the answer, and the confidence, so cut, awk, and grep
all work on it. --json gives one JSON object per line instead.
It takes the same three question types as the one-shot form:
git log --format=%s -50 | mnjev map choice "what kind of change?" "fix,feature,chore"
cat reviews.txt | mnjev map score "how angry?" "calm|annoyed|furious"Each line is its own state, so each line is its own call. Jev's batching makes many
questions about one state cheap, not one question about many states, so map cannot
make 50 items cost less than 50 calls. What it does is run them at the same time:
| 16 items | time | |---|---| | one at a time | 6.8s | | 8 in flight (default) | 1.7s |
JEV_CONCURRENCY sets how many run at once.
Batching does apply when you ask several questions about each item. Separate them with
; and each item still costs one call:
git log --format=%s -50 | mnjev map "is this a bug fix?; is this risky?"Exit codes for scripts
The confidence is the point of Jev, so --exit-code puts it where a script can act on it
instead of making you parse the text:
| code | meaning |
|---|---|
| 0 | yes, above the confidence floor |
| 2 | no, above the confidence floor |
| 3 | below the floor, or none of your options fit |
| 1 | the command itself failed |
if cat err.log | mnjev noul "is this a database problem?" --exit-code; then
page-the-dba
fiWith --exit-code, map exits 3 if any row came back below the floor, so a batch that
needs a human look can fail a CI step. Without the flag it exits 0, so a plain map in
a pipeline never fails a script by surprise. An item that errored exits 1 either way,
because a network or auth failure is not low confidence.
Say what you are comparing on
A bare comparison names no criterion, so there is nothing for Jev to decide:
› email or chat
chat confidence 4%
chat ██████████░░░░░░░░░░ 52%
email ██████████░░░░░░░░░░ 48%
say what you are comparing on: which is better for X? email or chat
› which leaves a written record that is easy to search later? email or chat
email confidence 98%
› which is better for reaching someone within a minute? email or chat
chat confidence 100%Same two options, same model, opposite answers. The criterion is the question; without
one there is nothing to decide. Putting the question before the ? also keeps shared
context out of your options.
Jev has no clock and cannot see your machine
It judges the state you give it:
› /run date
› is this year 2027?
no 100% sureIt does know things, though, so plenty of questions need no state at all:
can penguins swim? yes 97%
can penguins fly? no 97%
is 7 a prime number? yes 98%
is my build passing? no 52% <- unsure, and rightly so
is it raining right now? no 73% <- unsure, and rightly soEverything you type goes to Jev. Nothing is intercepted and nothing is refused. Questions it cannot answer land near 50% and are flagged, which is a better guard than declining to ask.
The three question types
| Type | Question | Returns |
|---|---|---|
| noul | is this true? | one number, 0 to 1 |
| choice | which of these? | one of your options, plus confidence and full probabilities |
| score | which level? | a position on your scale, which can land between two levels |
For choice, a,b,c is enough. Use a=what a means,b=what b means when the names alone
are not clear.
Read the confidence
This is the part that matters.
Jev must return one of the options it was given. When the real answer is not among them, you get a wrong answer with low confidence. The confidence is the only signal:
Sydney confidence 0.41 <- correct answer was missing from the list
Canberra confidence 1.00 <- correct answer presentSame question, same model. Only the confidence told the truth.
Every probability Jev returns is shown, on every call:
coffee confidence 96%
coffee ████████████████████ 98%
tea ░░░░░░░░░░░░░░░░░░░░ 2%The confidence is not the winner's own probability. For a choice it tracks how far ahead the winner is, so 98% against 2% is a confidence of 96%, and a near tie is near zero:
email confidence 2%
email ██████████░░░░░░░░░░ 51%
chat ██████████░░░░░░░░░░ 49%
unsure - try clearer options, or more contextEmail "won" and the answer is worth nothing. The bar says which option came first; the confidence says whether that was a decision. Read the confidence.
A choice is ranked by probability. A score keeps its scale order, because for a score the
order is the meaning. Answers below 0.7 turn yellow and are marked unsure; confident
ones are green and stay out of your way. Colour is off automatically when output is piped,
when NO_COLOR is set, or on a dumb terminal.
A noul is measured outward from 0.5, since 0.5 means no idea and both 0.99 and
0.01 are confident. At the default floor a noul needs to be above 0.85 or below 0.15.
Letting Jev decline
Your options go to Jev exactly as typed. JEV_ESCAPE=1 adds a none of these option so
it can reject the whole list. Asking "which city is the capital of Australia?":
| options given | without escape hatch | with it |
|---|---|---|
| Sydney, Melbourne, Canberra | Canberra 1.00 (right) | Canberra 1.00 (right) |
| Sydney, Melbourne, Perth | Sydney 0.41 (wrong) | none of these 0.99 (right) |
On a clear-cut question it is free, and it rescues the case where the real answer is
missing. On an ambiguous question it can steal the answer: asked which of three
fruits is red, with a defensible answer present, one measured pair went from 0.71 right
to 0.27 on none of these.
So it is off by default. Turn it on when your option lists may genuinely not cover the case, such as routing against a fixed set of teams.
Speed and cost
$0.042 per million input tokens, output free. TypeSafe quotes 70 to 500 ms of model
time. Measured end to end from a terminal, including network:
| | first question of a session | after that |
|---|---|---|
| a line naming no alternatives (can penguins swim?) | ~1200ms | ~330ms |
| a line naming alternatives (email vs chat) | ~1900ms | ~730ms |
A line that names alternatives costs two calls, because the options have to be found before they can define the question that follows them.
The first question of a session pays a TCP and TLS handshake, about 600ms. After that the
connection is held open, so idle time between questions costs nothing. Node's own fetch
drops its socket after four seconds of idle, which is shorter than the time it takes to
type a question, so this tool uses a keep-alive agent instead. Without it every question
paid the handshake again.
Settings
Read once at startup. A value that does not parse prints a warning and falls back, so a typo cannot silently switch a safety feature off.
| variable | default | meaning |
|---|---|---|
| TYPESAFE_API_KEY | from ~/.mnjev/config.json | your API key |
| JEV_CONFIDENCE | 0.7 | below this an answer is flagged unsure; must be 0 to 1 |
| JEV_ESCAPE | 0 | add a "none of these" option; 1/true/yes/on to enable |
| JEV_TIMEOUT_MS | 30000 | give up on a request after this long; 1000 to 600000 |
| JEV_RETRIES | 3 | retries after a rate limit or gateway error; 0 to 10 |
| JEV_RETRY_BASE_MS | 500 | first backoff delay; each retry doubles it |
| JEV_CONCURRENCY | 8 | items map keeps in flight at once; 1 to 32 |
| JEV_PASTE_MS | 12 | lines arriving within this window count as one paste; 1 to 500 |
| NO_COLOR | unset | any value turns colour off |
A 429, 529, or 502/503/504 is retried with doubling backoff and jitter,
honouring the server's Retry-After header. A 401 is not retried, because a wrong key
stays wrong. A timeout is not retried either, because waiting longer for something already
slow rarely helps.
What Jev is bad at
From TypeSafe's own known issues:
- Counting and maths. It cannot count reliably. Compute it in code and pass the result.
- Dates. It reads dates as text, not as ordered values. Compare dates yourself.
- Literal reading. It answers the question you wrote, not the one you meant.
- Big irrelevant state. Accuracy falls as unrelated content grows. This is why
/runexists: send the 20 lines that matter, not the whole log. State above 24,000 characters is refused rather than sent, on every path including pipes. - Untrusted input. It does not treat state as hostile. Text inside your state can steer the answer, so do not pipe in content you would not trust.
Develop
npm test # plain assert, no framework, no fixtures
npm run build # strips types from src/ into lib/bin/mnjev.mjs entry point; checks the Node version, then loads the CLI
src/config.ts every tunable and env var, with validating parsers
src/jev.ts transport, retries, question building, the confidence gate
src/parse.ts generates candidate spans; decides nothing
src/classify.ts asks Jev to find the options and pick the question type
src/repl.ts the interactive interface
src/ui.ts colour, bars, spinner
src/cli.ts one-shot flags and loginNode will not strip types from files under node_modules, so the published package ships
plain JavaScript in lib/. npm run build produces it with Node's own type stripper,
which is why there are still no dependencies. It runs automatically on npm pack and
npm publish.
Not built yet
- MCP support. An MCP tool's arguments are open sets and Jev needs closed ones. The option finder here is the piece that was missing, so this is now mostly plumbing.
- OpenRouter key support. Jev is not in OpenRouter's public model index and its request shape is not OpenAI-compatible, so going through it would mean a text model imitating a calibrated probability, which defeats the point.
- Pinned model version. Requests use
jev-latest, so behaviour can change when TypeSafe ships a new model.
Licence
MIT. See LICENSE.
