jev is a command-line tool that asks questions to the Jev API (TypeSafe's
System One, through OpenCode) about a text or a JSON payload.
Jev does not write answers: it classifies. You pass it a material (state)
and one or more questions, and it returns one answer per question with
probabilities. For example, you can ask whether a ticket is urgent, which team
should handle it, or how much anger it conveys.
jev ask --state "I was charged twice this month." \
--noul 'billing=Is this about billing?'model: jev-1.13
billing: noul=0.9
tokens: 283 in / 20 out, cost=1.189e-5
Besides the CLI, it includes a TUI, a local web console and an MCP server for an agent to use. They all share the same configuration and the same history.
- Installation
- First step: the API key
- Check that everything works:
jev demo - Ask questions:
jev ask - Repeat questions with templates
- Ask in batch:
jev batch - Browse the history
- Interactive mode:
jev tui - Local web console
- Use Jev from an agent (MCP)
- Analyze images
- Common problems
Requirements: Node.js 22.13+ (22 LTS, 24 LTS or 26 LTS) and an API key from OpenCode Console.
Try it without installing anything:
npx jev-client demoInstall it globally:
npm install -g jev-client
jev --versionUpdate or remove it:
npm install -g jev-client@latest
npm uninstall -g jev-clientNode 20 is not supported: the built-in
node:sqlitemodule is unflagged only from 22.13.0. On an older runtime the binary exits with an explicit message instead of a native-module stack trace.
If the global install fails with EACCES, see
Common problems. Working on the code? The checkout and build
instructions live in docs/development.md.
The fastest way is to pass it through an environment variable:
export JEV_API_KEY="sk-..."or store it so you don't have to repeat it:
jev config set api_key "sk-..."The key stored with
jev config setstays in clear text in your local database. If you don't want to persist it, useJEV_API_KEYor--key.
Check what is configured:
jev config list
jev config get model
jev config get api_key # prints it masked
jev config get api_key --reveal # prints it in fullChange the model or the timeout permanently:
jev config set model jev-1.13
jev config set timeout 30
jev config set retries 2
jev config unset model # goes back to the default value
jev config path # path of the database in useThe precedence is flag > environment variable > stored value > default
value. Full detail in docs/configuration.md.
jev demomodel: jev-1.13
frustration: score=1.99 ≈ "Very angry" (confidence=0.98)
2=0.99, 1=0.01, 0=0
is_urgent: noul=0.99
team: choice=infra (confidence=0.52)
infra=0.68, billing=0.32, sales=0
tokens: 449 in / 73 out, cost=1.886e-5
The exact probability varies slightly on each call: what matters is the chosen option and its confidence.
It is a built-in example with urgency, team and frustration. If you see answers and tokens, the key and the connection work.
--json prints the raw API response, useful to process it with another tool:
jev demo --json | jq '.answers'Jev supports three question types. All of them can be written directly on the command line.
Yes/no (noul) — returns a probability between 0 and 1:
jev ask --state "I was charged twice this month." \
--noul 'billing=Is this about billing?'model: jev-1.13
billing: noul=0.9
tokens: 283 in / 20 out, cost=1.189e-5
Pick an option (choice) — you describe the options with --option:
jev ask --state "The checkout has been down for 30 minutes." \
--choice 'team=Which team should handle the incident?' \
--option 'team.billing=Charges, invoices or payments' \
--option 'team.infra=Outages, 500 errors or platform problems' \
--option 'team.sales=Pricing or sales questions'model: jev-1.13
team: choice=infra (confidence=0.61)
infra=0.74, billing=0.26, sales=0
tokens: 354 in / 38 out, cost=1.487e-5
Score (score) — levels are added in order, from lowest to highest:
jev ask --state "We have had no reply for two days." \
--score 'urgency=How urgent is the incident?' \
--level 'urgency=It can wait' --level 'urgency=This week' \
--level 'urgency=Today'model: jev-1.13
urgency: score=1.29 ≈ "This week" (confidence=0.47)
1=0.65, 2=0.32, 0=0.03
tokens: 312 in / 18 out, cost=1.310e-5
score returns a weighted average and may come out as a decimal; when it falls
between two levels it is shown with ≈.
You can combine several questions in a single call: they are evaluated against
the same state and each one receives an answer.
jev ask --state "I was charged twice and I want a refund." \
--noul 'refund=Is a refund requested?' \
--choice 'team=Which team should handle it?' \
--option 'team.billing=Charges' --option 'team.legal=Claims' \
--score 'urgency=How urgent is it?' \
--level 'urgency=It can wait' --level 'urgency=Today'It can be plain text, a JSON object or an array:
jev ask --state "The customer is angry." --noul 'angry=Is the customer angry?'
jev ask --state '{"ticket":"I was charged twice","plan":"pro"}' \
--noul 'refund=Is a refund requested?'
jev ask --state '["error 500","checkout down"]' \
--noul 'infra=Is this a platform problem?'If the text starts with { or [, it is sent as JSON; otherwise, as a string.
echo "The checkout is down" | jev ask --state - \
--noul 'urgent=Is it urgent?'jev ask --state-file examples/state.json \
--questions-file examples/questions.jsonexamples/state.json is the material and examples/questions.json the
questions. Useful when the JSON is long or another program generates it.
If you already have the Jev request as is (model, state and questions):
jev ask --request-file examples/request.jsonIt also accepts inline JSON with --request '{"..."}'.
The model in the payload is only a fallback: the model saved with
jev config set model (or the TUI) wins, as do --model and JEV_MODEL.
jev ask --model jev-1.13-free \
--url https://opencode.ai/inference/systemone/v1/systemone \
--timeout 60 --retries 3 \
--state "hello" --noul 'q=Is it a greeting?'Useful flags in ask, demo and batch:
| Flag | What it is for |
|---|---|
--json |
prints the raw API response |
--no-save |
does not save this run in the history |
--db <path> |
uses another database |
--key <key> |
API key for this run only |
--model <model> |
model for this run only |
--url <endpoint> |
full endpoint for this run only |
--timeout <s> / --retries <n> |
timeout and retries |
A template is a Jev request with blanks ({{key}}) that are filled in with a
context. It saves you from repeating the same JSON every time.
The model in the body is a fallback: the model saved with
jev config set model (or the TUI) wins, as do --model and JEV_MODEL.
The file examples/template.json contains:
{
"model": "jev-1.13",
"state": {
"order": "{{order_state}}",
"customer": "{{customer.name}}",
"plan": "{{customer.plan}}"
},
"questions": {
"urgent": { "type": "noul", "instructions": "Is it urgent?" }
}
}Save it under a name:
jev template create order \
--description "Customer order with macros" \
--file examples/template.json \
--macro-desc 'order_state=What the customer is asking for, as free text' \
--macro-desc 'customer.name=Customer name' \
--macro-type 'customer.plan=string'template saved: order
Macros are detected from the body on their own. --macro-desc and
--macro-type are optional: they add a description and a type so the template
is easier to understand in the TUI, the web console and agents.
You can also pass the body inline or through stdin:
jev template create greeting --body '{"model":"jev-1.13","state":"{{text}}","questions":{"q":{"type":"noul","instructions":"Is it a greeting?"}}}'
cat body.json | jev template create greeting --body -jev templates is an alias of jev template.
jev template list
# order - Customer order with macros
jev template show ordershow prints the body and the list of macros with their description and schema.
jev template validate order
# template valid: ordervalidate checks the template without calling Jev. It also accepts a file:
jev template validate --file examples/template.jsonThe context is a JSON object with one key per macro. For example,
examples/context.json:
{
"order_state": "I was charged twice and I want a refund.",
"customer": { "name": "Ana", "plan": "pro" }
}jev template render order --context-file examples/context.json{
"model": "jev-1.13",
"state": {
"order": "I was charged twice and I want a refund.",
"customer": "Ana",
"plan": "pro"
},
"questions": {
"urgent": {
"type": "noul",
"instructions": "Is it urgent?"
}
}
}
render does not call Jev. To run the result:
jev template render order --context-file examples/context.json > /tmp/request.json
jev ask --request-file /tmp/request.jsonMacro rules:
{{key}}looks up the key in the context. Nested paths are supported ({{customer.name}}); if a literal key with dots exists, it takes precedence.- If the text is exactly a macro, the value is inserted with its native type (object, array, number, boolean). If it is embedded in a sentence, it is interpolated as text.
\{{...}}leaves the macro written without resolving it.- If any macro is missing, the command fails before calling the API and lists every missing key.
# Open the body in $EDITOR
jev template edit order
# Replace the body from a file
jev template edit order --file new-body.json
# Add, change or clear metadata without touching the body
jev template edit order --macro-desc 'customer.plan=Contracted plan'
jev template edit order --clear-macro-desc 'customer.name'
jev template edit order --clear-macro-schema 'customer.plan'
# Delete (asks for confirmation)
jev template delete order
jev template delete order --yesjev template add and jev template remove are quick synonyms of create and
delete without confirmation.
Runs a template once for each context of a JSON array. Every request ends up in
the history under the same batch_id.
[
{
"order_state": "I was charged twice and I want a refund.",
"customer": { "name": "Ana", "plan": "pro" }
},
{
"order_state": "The checkout hangs when I try to pay.",
"customer": { "name": "Luis", "plan": "free" }
},
{
"order_state": "I never received this month's invoice.",
"customer": { "name": "Marta", "plan": "pro" }
}
]jev batch order --contexts examples/contexts.jsonbatch: order — 3 requests (batch 4f3c…)
1: ok jev-1.13 306 in / 20 out cost=1.3e-5
2: ok jev-1.13 302 in / 20 out cost=1.3e-5
3: ok jev-1.13 302 in / 20 out cost=1.3e-5
summary: 3 ok, 0 error
tokens: 910 in / 60 out
cost: 3.8e-5
If a context does not fit the template, that row shows the error and the rest continues:
2: error missing macros in context: customer.plan
Options:
# Up to 3 requests at a time (maximum 10)
jev batch order --contexts examples/contexts.json --concurrency 3
# Stop at the first error (by default it continues with the rest)
jev batch order --contexts examples/contexts.json --fail-fast
# Read the contexts from stdin
cat contexts.json | jev batch order --contexts -With --continue-on-error (the default), an invalid context does not cancel the
rest; the process exits with code 1 if there were errors. Results are printed
in file order, even though they run in parallel.
Every run of ask, demo and batch is saved in a local SQLite database,
including the ones that fail.
jev history list # one line per request, most recent first
jev history show 12 # full request and response
jev history export > hist.jsonl # one request per line (JSONL)Filter:
jev history list --limit 20
jev history list --status 429
jev history list --model jev-1.13
jev history list --template order
jev history list --batch 4f3c…
jev history list --since 2026-10-01T00:00:00ZTo avoid saving a particular run, add --no-save to ask, demo or batch.
The database lives in ~/.jev-client/db/jev.db and can be changed with --db or
JEV_DB. Technical detail in docs/persistence.md.
To configure the provider and manage templates without memorizing commands:
jev tui
jev tui --db /path/jev.dbIt has two areas:
- Provider settings:
api_url,api_key,model,timeoutandretries, with validation while you type. The key is shown masked. - Templates: search, show, create, edit and delete. While editing, the JSON is validated live and the detected macros are shown.
| Key | Action |
|---|---|
↑ / ↓ |
move the selection |
Enter |
choose / confirm |
Esc |
cancel / go back |
q |
return to the menu (or quit) |
Ctrl-C |
quit (asks for confirmation if there are unsaved changes) |
n / e / d / / |
in templates: new, edit, delete, search |
Changes are saved in the same database used by jev config and jev template,
so they are visible from outside the TUI. Without an interactive terminal
jev tui exits with an error and suggests the equivalent commands.
A graphical interface with the same features, no account and no telemetry:
jev web-console
jev --web-console # alias
jev web-console --port 0 --no-openIt opens the URL it prints in the browser; this is the Overview screen:
Screens:
| Screen | What it is for |
|---|---|
| Overview | state, metrics and connection test |
| Evaluate | launch noul, choice and score questions and see probabilities |
| Templates | create, edit, validate and render, with a description and type per macro |
| Batches | paste an array of contexts, run it and follow the progress |
| History | filter, see the detail and export |
| Settings | edit the configuration and reveal the key only if you ask for it |
Evaluate runs the questions in the order of the form and shows one answer per question with its probabilities and the CLI equivalent:
History filters the same SQLite rows the CLI writes, shows the detail per tab and is exported as JSONL:
Every screen shows the equivalent CLI command. --port, --host,
--allow-remote, --open/--no-open, --dev and the session flags are
explained in docs/web-console.md, together with the
security model. --allow-remote exposes an administration console: use it only
on trusted networks.
The MCP server lets an agent discover templates, fill in contexts and run requests without you translating the CLI.
jev mcp # server over stdio
jev-mcp # alias
jev mcp --read-only # read-only
jev mcp --no-execute # without executing against Jev
jev mcp --allow-destructive # allows deleting templatesConfiguration in opencode.json (globally installed package):
Without installing it globally:
{
"mcp": {
"servers": {
"jev": { "type": "local", "command": ["npx", "-y", "jev-client", "mcp"] }
}
}
}Besides the fixed catalog (jev_ask, jev_batch, jev_template_*,
jev_history_*, etc.), every saved template is exposed as a tool with its
macros as parameters. Full catalog, resources and permissions in
docs/mcp.md.
The API accepts images in an assets field, referenced from the state. From
the CLI:
jev ask --state '{"photo":"cat"}' \
--asset cat=photo.jpg \
--choice 'animal=Which animal appears in `photo`?' \
--option 'animal.CAT=Domestic feline' \
--option 'animal.DOG=Domestic canine'--asset [<ref>=]<path>can be repeated; without<ref>a UUID is generated.--assets-file <path>reads a completeassetsmap in JSON.- Formats: static PNG/JPEG, up to 16 MiB, sides of 280–1568 px.
The image must be referenced in state, instructions or criteria, and the
API does not resize it nor apply the EXIF orientation. Limits, pitfalls and the
protocol detail in docs/images.md.
Unsupported Node version. jev uses the built-in node:sqlite module and
needs Node.js 22.13+. On an older runtime it exits with code 1 and:
jev requires Node.js >= 22.13; found <version>.
Upgrade to Node 22 LTS, 24 LTS or 26 LTS. There is no native binary to rebuild.
npm may refuse the install first with EBADENGINE; both cases are covered in
docs/troubleshooting.md.
EACCES on a global install. A system Node usually installs into a
directory your user cannot write to. Use a version manager (nvm, fnm, asdf,
volta) or point npm at a user prefix:
npm config set prefix ~/.npm-global
export PATH="$HOME/.npm-global/bin:$PATH"
npm install -g jev-clientThe database uses a newer schema. Another version of jev wrote into the
same database. Use a temporary database:
jev ask --db "$(mktemp -d)/jev.db" --state "hello" --noul 'q=Is it a greeting?'429 FreeUsageLimitError. jev-1.13-free is limited outside OpenCode or
once the quota runs out. Use jev-1.13 for tests.
401 / api_key_expired. Check the key with
jev config get api_key --reveal or export JEV_API_KEY again.
422. The question is malformed: choice uses an object in criteria and
score uses an ordered array.
The web console does not start. Check the port (--port 0 picks a free one).
If the installation looks incomplete, reinstall with
npm install -g jev-client@latest. More cases in
docs/troubleshooting.md.
| Code | Meaning |
|---|---|
0 |
Success |
1 |
API, network or configuration error (missing key, invalid JSON…) |
2 |
Wrong usage (invalid command or flags) |
Authentication errors (401 / api_key_expired), quota
(FreeUsageLimitError) and malformed questions (422) add a hint: line with
the recommended action.
The README is a usage guide. Implementation detail lives in
docs/: architecture, persistence, configuration, question
types, web console, MCP, images, use as a library and development.
For scripting by use case, see also the
skills/jev-cli/ skill.



{ "$schema": "https://opencode.ai/config.json", "mcp": { "servers": { "jev": { "type": "local", "command": ["jev", "mcp"] } } } }