{"slug": "show-hn-jev-cli-cli-wrapper-for-jev-typesafe-ai-model", "title": "Show HN: Jev-CLI – CLI wrapper for JEV typesafe AI model", "summary": "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.", "body_md": "Analyze JSON, NDJSON, and JSONC system artifacts with [TypeSafe AI](https://docs.typesafe.ai/introduction)'s\nJev decision model, and get **typed, calibrated answers** back instead of prose.\n\n``` bash\n$ uv run jcli analyze app.ndjson --pack logs.triage --window 2 --mark-uncertain 0.6 -o table\nwhere           question     type    answer                conf\napp.ndjson:1-2  is_incident  noul    0.11 (no)            0.78*\napp.ndjson:1-2  severity     score   0.2 (Informational)   0.71\napp.ndjson:1-2  category     choice  application           0.42\napp.ndjson:3-4  is_incident  noul    0.93 (yes)           0.86*\napp.ndjson:3-4  severity     score   1.87 (Outage)         0.71\napp.ndjson:3-4  category     choice  dependency            0.88\n* noul answers report derived certainty |p-0.5|*2, not an API confidence score.\n3 call(s)  1290 in / 36 out tokens\n```\n\nEvery 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.\n\n```\n./scripts/install.sh               # puts `jcli` on your PATH via uv\nexport TYPESAFE_API_KEY=...        # or --api-key-file, or ~/.config/jcli/config.toml\njcli --help\n```\n\n| Flag |  | \n|---|---|\n| *(none)* | install from this checkout | \n| `--dev` | editable — source edits take effect without reinstalling | \n| `--dry-run` | print what would happen, change nothing | \n| `--uninstall` | remove it | \n\nIt needs [uv](https://docs.astral.sh/uv/getting-started/installation/) and prints the install\ncommand if uv is missing. Nothing goes into the system Python; nothing needs sudo. After\ninstalling it verifies the binary runs, reports its version, and checks that the built-in packs\nand transforms actually shipped — a wheel that builds but omits its YAML data is the failure\nworth catching at install time, not on first use.\n\nTo work in the repo without installing, `uv sync` and prefix commands with `uv run`.\n\nJev takes a **state** plus a map of **typed questions** and evaluates all of them *in one\ncall, in parallel*. So asking eight questions costs about what asking one costs. `jcli`\nleans on that: selecting two packs does not make two requests, it makes one request carrying\nboth packs' questions.\n\n``` bash\n$ jcli analyze auth.ndjson -p logs.triage -p security.audit --dry-run | jq '. | length'\n1                                   # one window -> one call\n$ jcli analyze auth.ndjson -p logs.triage -p security.audit --dry-run \\\n    | jq '.[0].request.questions | keys | length'\n8                                   # ...carrying all eight questions\n```\n\nThree question types, straight from the API:\n\n| Type | Asks | Returns | \n|---|---|---|\n| `noul` | Is this statement true? | probability 0–1 | \n| `choice` | Which of these labels? | label + full distribution + confidence | \n| `score` | Where on this rubric? | position (may fall between levels) + confidence | \n\n```\njcli analyze session.log                    # text log; transform auto-detected\njcli analyze app.ndjson                     # NDJSON, streamed line by line\njcli analyze cluster.json                   # JSON object, or array of records\njcli analyze config.jsonc                   # comments and trailing commas welcome\ncat events.ndjson | jcli analyze -          # stdin\njcli analyze a.ndjson b.ndjson              # several sources; windows never straddle files\n```\n\nThe dialect is auto-detected; `--format json|ndjson|jsonc` overrides.\n\nOne flag, `--window`:\n\n| Value | Behaviour | \n|---|---|\n| `--window 25`*(default)* | N records per call. Neighbouring lines give each other context. | \n| `--window record` | One call per record. Per-item triage you intend to filter on. Identical to `--window 1` . | \n| `--window whole` | The entire input as one state. One verdict over the corpus. | \n\nEvery mode is additionally capped by `--max-state-bytes`. The API's true ceiling is not\npublished; `jcli probe` measures it against the live service so you can set the flag from\nmeasurement rather than superstition.\n\n```\njcli analyze session.log --filter agent=abc123           # just one agent's records\njcli analyze audit.ndjson --filter 'level=error' --filter 'status>=500'   # ANDed\njcli analyze session.log --filter 'tool!=Bash' --filter 'desc~cargo'\njcli analyze k8s-events.json -s reason -s message        # project down to what matters\njcli analyze audit.ndjson --redact-preset secrets        # scrub before anything leaves the process\n```\n\n`--filter FIELD OP VALUE` uses the same operators as `--fail-on`:\n`=` `!=` `~` (contains, case-insensitive) `!~` `>=` `>` `<=` `<`. Dotted paths reach nested\nfields (`user.id=u1`). Repeating the flag ANDs the terms; for an \"or\", run the two separately.\n\nIt is record-aware, which `grep` is not: on a log whose fields contain embedded newlines,\n`grep agent=X` silently truncates every multi-line record. Filtering also runs *before*\n`--select` and `--redact`, so you can filter on an identifier and then scrub it.\n\nA 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.\n\nRedaction runs strictly before the first byte reaches the SDK. Matched values become\n`[redacted]` rather than disappearing, so \"did this request carry credentials\" is still\nanswerable.\n\nMost system artifacts are not JSON. A **transform** declares the shape of a text log\nonce, in YAML, and everything downstream — chunking, redaction, packs, confidence\ngating — works unchanged.\n\n```\njcli transforms list\njcli transforms show syslog --sample /var/log/auth.log   # see what it makes of a real file\njcli analyze /var/log/auth.log -p security.audit         # transform auto-detected\njcli analyze weird.log -t ./my-transform.yaml            # or point at your own\n```\n\n`-t` is an override, not a requirement. When the input is not JSON, every transform is scored\nby parse rate over a sample; a single clear winner is used and announced on stderr. If nothing\nmatches, or two match equally, the run **stops and lists the candidates with their scores** —\na wrong transform yields plausible records with the wrong fields, so guessing is worse than\nasking.\n\n```\nname: bracketed-kv\ndescription: A bracketed-timestamp key=value log whose last field is quoted free text.\n\n# A record begins here. Lines that do not match belong to the record above, which\n# is what lets a field contain embedded newlines.\nrecord_start: '^\\[\\d{4}-\\d{2}-\\d{2}T'\n\npattern: |-\n  (?s)^\\[(?P<ts>[^\\]]*)\\]\\s+\n  agent=(?P<agent>\\S+)\\s+ tool=(?P<tool>\\S+)\\s+\n  desc=\"(?P<desc>.*)\"\\s*$\nflags: [verbose]\n\nnull_values: [\"-\"]        # a bare dash becomes null\ncoerce: { ts: timestamp } # int | float | bool | timestamp | string\n```\n\n`syslog` is the only transform that ships. Everything else is a file you write and pass with\n`-t ./my-transform.yaml`, or drop into `~/.config/jcli/transforms/` to have it discovered\nand auto-detected like a built-in. A worked example lives in `tests/fixtures/bracketed-kv.yaml`.\n\nNamed groups become JSON keys. Two details matter in practice, both learned from real emitter output rather than assumed:\n\n- **Records are not lines.**`record_start` frames them; continuation lines are joined.\n- **Quoted fields are not shell-quoted.** Emitters write command lines into a`desc=\"...\"` field without escaping the quotes inside, so logfmt, csv, and shlex\nparsers all truncate at the first inner quote. A greedy group running to the\nrecord's final quote is the fix.\n\nNothing is dropped silently. A record the pattern rejects is kept verbatim under\n`_unparsed` and counted in a warning — a log whose format drifted mid-file is itself\na finding, and an empty result set is a bad way to discover it.\n\nTransforms layer like packs: built-ins, then `~/.config/jcli/transforms/*.yaml`,\nthen `-t ./my-transform.yaml`.\n\nFour ship built in:\n\n| Pack | Questions | \n|---|---|\n| `logs.triage` | `is_incident` ,`severity` ,`category` ,`actionable` | \n| `logs.anomaly` | `anomalous` ,`novelty` ,`recurring_pattern` | \n| `security.audit` | `suspicious` ,`threat_class` ,`privilege_escalation` ,`blast_radius` | \n| `security.finding` | `exploitable` ,`false_positive_likelihood` ,`remediation_urgency` | \n| `agent.session` | `activity` ,`progress` ,`read_before_write` ,`repetition` | \n\n```\njcli packs list\njcli packs show security.audit --resolved    # exactly what would be sent\njcli packs init mine --from logs.triage      # scaffold your own\njcli packs validate ./mine.yaml\n```\n\n`--ask` / `-a` defines a question on the command line, no file needed. Two forms:\n\n```\n# shorthand: a noul. The '=' comes first, so a colon in the question is fine.\njcli analyze session.log -a 'stuck=Is the agent repeating itself?'\n\n# full form: the same YAML a pack file uses, so any type works\njcli analyze session.log -a 'focus: {type: score, instructions: \"How concentrated?\",\n                                   criteria: [scattered, clustered, focused]}'\n```\n\nIt needs no `--pack`, merges with one when given, and can reword a built-in question by\nnaming its id. Quote any value containing `:`, `,` or `?` — YAML flow mappings require it.\n\nPacks layer, lowest to highest:\n\n1. built-ins\n2. `~/.config/jcli/questions/*.yaml` — a file whose`name:` matches a built-in replaces it\n3. `--questions FILE` — deep-merged overlays, repeatable\n4. `--set QID.path=value` — repeatable, JSON-valued when parseable\n5. `--drop QID`\n\n```\njcli analyze app.ndjson -p logs.triage \\\n  --set 'category.criteria.application.what=A bug in our own service code' \\\n  --drop actionable\n```\n\nA `--set` only replaces the leaf it names; sibling keys survive. Overrides are re-validated\nafterwards, because a `--set` can break a rubric as easily as fix it.\n\n`instructions` and `criteria` accept nested structure, not just strings\n([Advanced: structure](https://docs.typesafe.ai/primitives/advanced)):\n\n```\nname: logs.triage\nversion: 1\nquestions:\n  severity:\n    type: score\n    instructions: How severe is the worst condition in these records?\n    criteria:                          # ordered, lowest first\n      - summary: Informational\n        signals: [\"debug/info only\", \"no error fields\"]\n      - summary: Degraded\n        signals: [\"elevated latency\", \"partial retries\"]\n      - summary: Outage\n        signals: [\"sustained 5xx\", \"dependency down\", \"data loss\"]\n\n  category:\n    type: choice\n    instructions: Which subsystem is the most likely origin?\n    criteria:                          # at least two options\n      infra:\n        what: The host, network, disk, or scheduler beneath the service.\n        examples: [\"OOMKilled\", \"disk full\", \"node NotReady\"]\n      security:\n        what: Authentication, authorization, or tampering signal.\n        not_for: Ordinary 401s from routinely expired tokens.\n\n  is_incident:\n    type: noul\n    instructions: Is this a live production incident?\n    criteria:\n      \"true\": User-visible failure or SLO breach in progress.\n      \"false\": Routine errors, retries, or known-benign noise.\n```\n\n`choice` and `score` answers carry a `confidence` the API derives from the shape of the\nprobability distribution. **`noul` answers do not** — a noul is a single probability and\nnothing else.\n\n`jcli` does not invent one. It reports a separate *certainty* for nouls, computed as\n`|p − 0.5| × 2` and shown under its own column with a `*`. A noul of 0.5 is maximally\nunsure; a noul of 0.05 is a confident *no*. Conflating that with the API's calibrated\nconfidence would make `--confident` mean two different things in one table.\n\nNouls *do* carry a `probabilities` map — `{\"true\": p, \"false\": 1 - p}` — so every answer type\nhas the same shape and one consumer handles all three:\n\n```\n{\"question\": \"is_incident\", \"type\": \"noul\", \"value\": 0.93, \"certainty\": 0.86,\n \"probabilities\": {\"true\": 0.93, \"false\": 0.07}}\n```\n\nThat is a different claim from a confidence. For a two-outcome question the returned number fixes the complement exactly, so writing it out adds no information and invents none.\n\nIn the table, a noul renders as `0.93 (yes)` / `0.11 (no)` — the same `value (meaning)` shape\n`score` has always used, so a confident *no* is never misread as a weak *yes*. The label is\nmechanical (which side of 0.5), inventing no threshold; how strongly it leans is the adjacent\ncertainty column.\n\n`--probabilities` / `-P` adds a distribution column to `table` and `summary`:\n\n``` bash\n$ jcli analyze app.ndjson -p logs.triage -o table --probabilities\nwhere           question     type    answer                conf   distribution\napp.ndjson:1-2  is_incident  noul    0.11 (no)            0.78*   false 89% · true 11%\napp.ndjson:1-2  severity     score   0.2 (Informational)   0.71   Outage 85% · Degraded 10% · ...\napp.ndjson:1-2  category     choice  application           0.42   dependency 55% · infra 24% · ...\n```\n\nIt earns its keep on `choice` and `score`, where the runner-up is real information — an even\nfour-way split is a different situation from a clear winner at the same confidence. On a noul\nthe two halves restate one number; they are shown anyway so every row reads the same way.\nJSON, NDJSON, and CSV output are unaffected by the flag.\n\n```\njcli analyze app.ndjson -p logs.triage --mark-uncertain 0.7   # keep weak answers, flagged\njcli analyze app.ndjson -p logs.triage --confident 0.7        # drop them\n```\n\nThe threshold is part of the flag, so there is no way to ask for dropping without saying how confident is confident enough.\n\nTypeSafe's [guidance](https://docs.typesafe.ai/confidence): act automatically above ~0.9 for\nhigh-stakes decisions, never act below 0.5.\n\n`--output json|ndjson|table|csv|summary`. Defaults to `table` on a TTY, `json` in a pipe.\n\n```\n{\n  \"source\": \"auth.ndjson\",\n  \"span\": {\"source\": \"auth.ndjson\", \"indices\": [0, 4], \"count\": 5, \"lines\": [1, 5]},\n  \"pack\": \"security.audit\",\n  \"question\": \"threat_class\",\n  \"type\": \"choice\",\n  \"value\": \"privilege_escalation\",\n  \"confidence\": 0.93,\n  \"probabilities\": {\"privilege_escalation\": 0.93, \"recon\": 0.05, \"benign\": 0.02},\n  \"below_threshold\": false\n}\njcli analyze audit.ndjson -p security.audit \\\n   --fail-on 'suspicious>=0.8 and blast_radius>=1.5'\necho $?    # 6\n```\n\nGrammar is `QID OP VALUE` joined by `and` / `or`. A term matches if *any* window satisfies it.\n\n| Exit | Meaning | \n|---|---|\n| 0 | success | \n| 1 | unexpected error | \n| 2 | usage or pack validation error | \n| 3 | auth failure (401/403) | \n| 4 | rate limited past the retry budget | \n| 5 | partial failure — some windows failed, results still emitted | \n| 6 | `--fail-on` matched | \n\nExit 5 is deliberate: a 500 on window 37 of 200 does not throw away 199 good answers. A 401\n*does* abort immediately, because it would fail identically on all 200.\n\nResolved in order: `--api-key` → `--api-key-file` → `$TYPESAFE_API_KEY` →\n`~/.config/jcli/config.toml` → keyring (with the `keyring` extra).\n\nThe token is wrapped so it cannot be printed by accident — `repr`, f-strings, and dataclass\nreprs all render `***`. A config or key file that is group- or world-readable is **refused**,\nnot warned about. `jcli config show` names which source supplied the key without revealing it.\n\n``` bash\n$ jcli config show\nconfig file    ~/.config/jcli/config.toml\nbase_url       https://api.typesafe.ai\nmodel          jev-latest\napi_key        found via $TYPESAFE_API_KEY\n```\n\nRepeating the same pack and transform on every run gets old. `~/.config/jcli/config.toml`\nabsorbs it:\n\n```\napi_key = \"...\"\n\n[defaults]\npack = [\"agent.session\"]\ntransform = \"syslog\"    # explicit -t still wins; auto-detect is the fallback\nwindow = 25\noutput = \"table\"\n```\n\nAn explicit flag always beats a config default, which always beats the built-in default.\n`jcli config` prints what resolved, and names any key it ignored.\n\n```\nuv run pytest            # 119 tests, no network\nuv run pytest -m live    # smoke tests against the real API (needs TYPESAFE_API_KEY, costs money)\nuv run ruff check .\nuv run mypy\n```\n\n`--dry-run` prints the exact request bodies without a key and without touching the network —\nthe fastest way to see what a flag combination actually does.\n\n```\njcli analyze audit.ndjson -p security.audit --dry-run | jq '.[0].request.state'\n```\n\n", "url": "https://wpnews.pro/news/show-hn-jev-cli-cli-wrapper-for-jev-typesafe-ai-model", "canonical_source": "https://github.com/joshLong145/jev-cli", "published_at": "2026-09-21 12:58:15+00:00", "updated_at": "2026-09-21 13:25:42.361906+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "large-language-models", "ai-products"], "entities": ["Jev-CLI", "TypeSafe AI", "Jev", "uv"], "alternates": {"html": "https://wpnews.pro/news/show-hn-jev-cli-cli-wrapper-for-jev-typesafe-ai-model", "markdown": "https://wpnews.pro/news/show-hn-jev-cli-cli-wrapper-for-jev-typesafe-ai-model.md", "text": "https://wpnews.pro/news/show-hn-jev-cli-cli-wrapper-for-jev-typesafe-ai-model.txt", "jsonld": "https://wpnews.pro/news/show-hn-jev-cli-cli-wrapper-for-jev-typesafe-ai-model.jsonld"}}