README.md

September 19, 2026 · View on GitHub

   ██ ██████ ██  ██ ██ ▄█▀ ██ ██████ 
   ██ ██▄▄   ██▄▄██ ████   ██   ██   
████▀ ██▄▄▄▄  ▀██▀  ██ ▀█▄ ██   ██   

Typed decisions from the command line.

CI License

TypeSafe’s Jev returns typed decisions instead of prose. jevkit is a CLI for it: ask Jev questions about a passage, get back probabilities, labels, and scores. jev lint validates a question set offline, before you spend anything on a call.

Install

curl -fsSL https://raw.githubusercontent.com/ariel-frischer/jevkit/main/install.sh | sh

More ways to install: see Installation details.

Usage

$ jev ask -q severity.yaml "The deploy script drops the production database with no confirmation."
{
  "risky": 0.96,
  "severity": "high",
  "urgency": 2.82
}

No file, either: a question set passed inline is a one-liner.

$ jev ask --question-set '{"risky":{"type":"noul","instructions":"Is this risky?"}}' "jumping into a volcano"
{
  "risky": 0.97
}

Built for AI agents too. jev ships with an agent skill that teaches a coding agent to drive the CLI; agents can also consume it via npx skills add ariel-frischer/jevkit.

Features

  • 🎯 Typed questions: noul (probability), choice (label), score (level), sent in terse YAML or JSON, by file, stdin, or --question-set inline
  • 🦀 Offline linting: 13 rules that catch billed-but-useless questions before a call; documented exit codes for CI
  • 🔩 Machine-first I/O: JSON on stdout (compact when piped, pretty on a TTY; --compact/--pretty to force), diagnostics on stderr, 0/1/2 exit codes (ok / usage·API / lint-rejected)
  • 📊 Usage ledger: jev ask --log appends one JSON line per call (request, response, tokens, cost) to ~/.local/state/jev/usage.jsonl
  • 🛠 Config: user defaults in ~/.config/jev/config.toml (provider, model, log), layered flags > env > config > defaults
  • 🔐 Keyring auth: keys stored in the OS credential store, never argv or shell history; fingerprint display rather than the key itself
  • One call, all answers: additional questions are answered in parallel by the API, so batching costs nothing in latency

Why this exists

Calling the decisions API is not hard: one POST, three question types. You can do it with curl. Three things a wrapper can do that curl cannot.

Less to write per call. Every call otherwise re-emits the endpoint, auth header, model string, and the typed envelope: 308 raw chars vs 198 in terse YAML, 36% less. The terse form expands back into the same JSON before sending, so the wire payload and the API cost are identical. The saving is in what you write, not in what you are billed for.

Validation before you are billed. The API accepts and charges for questions that cannot inform you, all returning HTTP 200:

QuestionWhat comes backWhy it is useless
score with one levelscore: 0, confidence: 1The answer was forced
choice with one optionthat option at probability 1.0The answer was forced
Criteria that restate the labela plausible-looking labelSee below

The last case is the dangerous one, because the result looks fine. On identical input, with only the descriptions altered, the verdict inverted: bare "analogy" labels scored analogy (0.75), while written descriptions scored popular_opinion (0.76). Criteria text is the prompt, not documentation. No schema catches any of this; jev lint does, offline, before you spend anything.

Fast where speed is actually available. The API is ~273 ms per call; jev lint is ~0.5 ms with no network call at all, fast enough for a pre-commit hook over a large question set. Nothing makes ask faster than the network, and this tool does not claim otherwise.

Installation details

Build from source

git clone https://github.com/ariel-frischer/jevkit.git
cd jevkit
make install-global      # builds and installs ~/.local/bin/jev
jev init                 # provider, custom endpoint, key in the OS keyring
# or non-interactive: `jev init --no-key --provider openrouter` then `jev auth login`

Installer internals

It downloads the matching prebuilt binary from GitHub Releases (linux x86_64/aarch64, macOS x86_64/aarch64), verifies the SHA-256 checksum, backs up an existing install, and warns if ~/.local/bin is not on your PATH. Override the version with JEV_VERSION=v0.1.0.

Compatibility

linux x86_64/aarch64 and macOS x86_64/aarch64 (glibc-based linux, e.g. Ubuntu 20.04+, Debian 11+, glibc ≥ 2.31; macOS 11+). Windows needs WSL.

The agent skill

.skills/jevkit/ is an agent skill that teaches a coding agent to drive the CLI, including the failure modes that are easy to hit.

Or install the bundled agent skill as a one-liner:

npx skills add ariel-frischer/jevkit
npx skills ls -g | grep jevkit   # confirm registration

One registration covers Claude Code, Codex, Gemini CLI, OpenCode, and Zed, since they all read ~/.agents/skills.

When contributing, install the skill by symlink so repository edits stay live.

Commands

jev ask

Send a question set and print one value per question.

jev ask -q questions.yaml --file passage.txt   # state from a file
echo "some text" | jev ask -q questions.yaml   # state from stdin
jev ask -q questions.yaml "some text"          # state as an argument

jev ask -q questions.yaml --dry-run            # print the payload, send nothing
jev ask -q questions.yaml --raw                # full response with usage and cost
`jev lint --question-set` works the same way.

Linting runs automatically. Errors block the call, warnings print to stderr and
proceed, so piping stdout to `jq` stays safe. Exit codes: `0` success, `1`
usage/credential/API failure, `2` lint errors -- nothing sent, nothing billed.
JSON is compact when piped, pretty on a TTY; `--compact`/`--pretty` override.

### `jev lint`

Validate a question set without touching the network.

```bash
jev lint questions.yaml
jev lint --json questions.yaml          # machine-readable findings
jev lint --strict questions.yaml        # warnings fail CI (alias: --deny-warnings)
jev lint --quiet questions.yaml         # rule ids only, no help blocks

Exit codes follow a documented contract, for use in scripts and CI:

  • 0 the question set is clean
  • 1 errors were found (the API would reject these), or warnings with --strict
  • 2 warnings only, which are heuristics rather than rejections

--json prints machine-readable findings (rule, severity, message, path, help), compact when piped, with --pretty/--compact to override. Text mode prints findings to stdout so piping to jq stays safe. --quiet drops the help blocks and prints rule ids only, roughly cutting text output in half; useful when an agent or script consumes the ids. Shell completions and a man page are behind the hidden completions subcommand, generated at build time alongside the binary.

jev auth

First-time setup: jev init interactively picks a provider (or a custom decisions URL), stores the API key in the OS keyring, and writes the config defaults. Flags pre-answer it for scripts and agents: jev init --no-key --provider typesafe.

jev auth login      # prompts on a hidden TTY, stores in the OS keyring
jev auth status     # shows which credential would be used, never the value
jev auth logout

Keys resolve in this order: --api-key, then the provider's environment variable, then the OS keyring. auth login never accepts a key as an argument, because argv is readable by other processes and lands in shell history.

jev config

User defaults in ~/.config/jev/config.toml. Keys: provider, model, endpoint (override the decisions URL, e.g. a proxy or self-hosted install), and log, and lint_verbosity (full default, or quiet for findings with rule ids only; a --quiet flag on ask and lint overrides it).

jev config keys     # what is configurable
jev config set model typesafe/jev-1.13
jev config show

Settings layer highest first: flags > JEV_* env vars > config.toml > built-in defaults.

Usage ledger

jev ask --log appends one JSON line per call (request, response, tokens, cost) to ~/.local/state/jev/usage.jsonl. Opt-in only; failed calls are logged too. config set log <path-or-1> makes a bare --log use it.

Writing questions

YAML or JSON, anywhere a question set is accepted, by file or on stdin. Ready-made examples are in examples/, including a clean severity.yaml and a file that fails every lint rule (bad-questions.yaml). The format is detected automatically, with no flag: JSON is tried first, because every JSON document is also valid YAML but the JSON parser gives better errors.

jev ask -q questions.yaml "text"
jev ask -q questions.json "text"
cat questions.json | jev lint

Within either format there are two spellings, terse and canonical. The terse one exists because the API envelope is verbose: on a two-question payload, 59% of the JSON was structure ("type", "instructions", "criteria", nesting) and only 41% was content.

# Terse: the primitive is the key, its value is the instructions.
risky:
  noul: Does the passage describe an irreversible operation?

severity:
  choice: How severe is the worst outcome the passage describes?
  options:
    low: Cosmetic or easily reversed with no lasting effect
    high: Permanent data loss or an outage affecting users
    none: The passage does not describe an operation with consequences

urgency:
  score: How soon must someone act?
  levels:
    - Can wait for the next planning cycle
    - Needs attention today
    - Requires an immediate response
# Canonical: exactly what goes on the wire.
risky:
  type: noul
  instructions: Does the passage describe an irreversible operation?

The terse form drops the envelope, never the content. Criteria stay full sentences in both.

YAML scalar typing

YAML types an unquoted 1.5 as a number and true as a boolean, and the API requires criteria to be text. jev coerces those back to strings rather than making you quote them, since the text is the only thing they could have meant. A quoted "1.5" is untouched, and objects and arrays are left alone.

Most other YAML-to-JSON complaints do not apply: 12:30, 0755, and a 20-digit integer all stay strings under YAML 1.2, and a bare NO label stays "NO" rather than becoming false.

Two things are still fatal, and both report what to do about it:

  • An unquoted value containing ": ", which YAML reads as a nested mapping. This is the likeliest mistake in a criteria file, since descriptions are prose. Quote the whole value.
  • Tab indentation, which YAML forbids.

instructions also accepts a structured object, which the API supports and which lint reads through:

q:
  noul:
    question: Does the `message` ask the recipient to disclose a credential?
    focus: A request to send the credential, not to reset it.

Lint rules

RuleSeverityCatches
missing-criteriaerrorchoice/score without criteria, which the API rejects
non-string-criteria, non-string-levelerrorA non-string criteria value, for requests built in code; the CLI coerces these
empty-instructionserrorA question with nothing to judge
context-overflowerrorA payload that cannot fit the 32k-token limit under any tokenization
degenerate-criteriawarningCriteria that restate the label
single-optionwarningA choice with one option
single-levelwarningA score with one level
numeric-levelwarningLevels described as bare numerals
conditional-questionwarning"if applicable" phrasing, which returns ~0.5 regardless
compound-questionwarningA noul weighing two properties at once
no-escape-optionwarningA choice that cannot decline to answer
context-pressurewarningA payload that may exceed the token limit
terse-instructionswarningInstructions too short to state a condition

Errors are certain. Warnings are heuristics: they flag questions that are valid but probably not what you meant.

Performance

See Why this exists for the numbers. The short version: the network is ~273 ms and everything local is under 0.1% of that, so only lint, which makes no call, is meaningfully fast.

One thing that does move the needle: Jev answers every question in a request in parallel. Measured on the same passage, ten questions were no slower than one, for 39% more cost:

QuestionsMedian latencyCost
1266 ms$0.0000120
10230 ms$0.0000167

So batch questions into one request rather than looping. Ten thin calls would have cost ten round trips and ten times the base overhead.

Contributing

Contributions are welcome. See CONTRIBUTING.md for guidelines; CI runs fmt, clippy, offline tests, and the MSRV build.

License

jevkit is licensed under the MIT license. See the LICENSE file for more information.