Show HN: Jev-CLI – CLI wrapper for JEV typesafe AI model A developer released Jev-CLI, an open-source command-line wrapper for TypeSafe AI's Jev decision model that returns typed, calibrated answers instead of prose when analyzing JSON, NDJSON, and JSONC system artifacts. The tool anchors every answer to the source lines it came from and attaches a numeric certainty value, evaluating all typed questions from multiple packs in a single parallel API call — the example output shows 8 questions carried in 1 request using 1290 input and 36 output tokens. Jev-CLI installs via uv without sudo or system Python changes, supports noul, choice, and score question types, and offers window modes of 25 records (default), one record, or the whole input, capped by --max-state-bytes. Analyze JSON, NDJSON, and JSONC system artifacts with TypeSafe AI https://docs.typesafe.ai/introduction 's Jev decision model, and get typed, calibrated answers back instead of prose. bash $ uv run jcli analyze app.ndjson --pack logs.triage --window 2 --mark-uncertain 0.6 -o table where question type answer conf app.ndjson:1-2 is incident noul 0.11 no 0.78 app.ndjson:1-2 severity score 0.2 Informational 0.71 app.ndjson:1-2 category choice application 0.42 app.ndjson:3-4 is incident noul 0.93 yes 0.86 app.ndjson:3-4 severity score 1.87 Outage 0.71 app.ndjson:3-4 category choice dependency 0.88 noul answers report derived certainty |p-0.5| 2, not an API confidence score. 3 call s 1290 in / 36 out tokens Every answer is anchored to the lines it came from, and every answer carries a number saying how sure the model is — so you can gate on it. ./scripts/install.sh puts jcli on your PATH via uv export TYPESAFE API KEY=... or --api-key-file, or ~/.config/jcli/config.toml jcli --help | Flag | | |---|---| | none | install from this checkout | | --dev | editable — source edits take effect without reinstalling | | --dry-run | print what would happen, change nothing | | --uninstall | remove it | It needs uv https://docs.astral.sh/uv/getting-started/installation/ and prints the install command if uv is missing. Nothing goes into the system Python; nothing needs sudo. After installing it verifies the binary runs, reports its version, and checks that the built-in packs and transforms actually shipped — a wheel that builds but omits its YAML data is the failure worth catching at install time, not on first use. To work in the repo without installing, uv sync and prefix commands with uv run . Jev takes a state plus a map of typed questions and evaluates all of them in one call, in parallel . So asking eight questions costs about what asking one costs. jcli leans on that: selecting two packs does not make two requests, it makes one request carrying both packs' questions. bash $ jcli analyze auth.ndjson -p logs.triage -p security.audit --dry-run | jq '. | length' 1 one window - one call $ jcli analyze auth.ndjson -p logs.triage -p security.audit --dry-run \ | jq '. 0 .request.questions | keys | length' 8 ...carrying all eight questions Three question types, straight from the API: | Type | Asks | Returns | |---|---|---| | noul | Is this statement true? | probability 0–1 | | choice | Which of these labels? | label + full distribution + confidence | | score | Where on this rubric? | position may fall between levels + confidence | jcli analyze session.log text log; transform auto-detected jcli analyze app.ndjson NDJSON, streamed line by line jcli analyze cluster.json JSON object, or array of records jcli analyze config.jsonc comments and trailing commas welcome cat events.ndjson | jcli analyze - stdin jcli analyze a.ndjson b.ndjson several sources; windows never straddle files The dialect is auto-detected; --format json|ndjson|jsonc overrides. One flag, --window : | Value | Behaviour | |---|---| | --window 25 default | N records per call. Neighbouring lines give each other context. | | --window record | One call per record. Per-item triage you intend to filter on. Identical to --window 1 . | | --window whole | The entire input as one state. One verdict over the corpus. | Every mode is additionally capped by --max-state-bytes . The API's true ceiling is not published; jcli probe measures it against the live service so you can set the flag from measurement rather than superstition. jcli analyze session.log --filter agent=abc123 just one agent's records jcli analyze audit.ndjson --filter 'level=error' --filter 'status =500' ANDed jcli analyze session.log --filter 'tool =Bash' --filter 'desc~cargo' jcli analyze k8s-events.json -s reason -s message project down to what matters jcli analyze audit.ndjson --redact-preset secrets scrub before anything leaves the process --filter FIELD OP VALUE uses the same operators as --fail-on : = = ~ contains, case-insensitive ~ = <= < . Dotted paths reach nested fields user.id=u1 . Repeating the flag ANDs the terms; for an "or", run the two separately. It is record-aware, which grep is not: on a log whose fields contain embedded newlines, grep agent=X silently truncates every multi-line record. Filtering also runs before --select and --redact , so you can filter on an identifier and then scrub it. A filter that matches nothing is an error listing the values the field actually holds — an empty table is a poor way to discover a typo. Redaction runs strictly before the first byte reaches the SDK. Matched values become redacted rather than disappearing, so "did this request carry credentials" is still answerable. Most system artifacts are not JSON. A transform declares the shape of a text log once, in YAML, and everything downstream — chunking, redaction, packs, confidence gating — works unchanged. jcli transforms list jcli transforms show syslog --sample /var/log/auth.log see what it makes of a real file jcli analyze /var/log/auth.log -p security.audit transform auto-detected jcli analyze weird.log -t ./my-transform.yaml or point at your own -t is an override, not a requirement. When the input is not JSON, every transform is scored by parse rate over a sample; a single clear winner is used and announced on stderr. If nothing matches, or two match equally, the run stops and lists the candidates with their scores — a wrong transform yields plausible records with the wrong fields, so guessing is worse than asking. name: bracketed-kv description: A bracketed-timestamp key=value log whose last field is quoted free text. A record begins here. Lines that do not match belong to the record above, which is what lets a field contain embedded newlines. record start: '^\ \d{4}-\d{2}-\d{2}T' pattern: |- ?s ^\ ?P