@sunilaleti/hx 0.3.0-beta.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -28,6 +28,75 @@ the project follows [semantic versioning](https://semver.org/).
28
28
 
29
29
  ---
30
30
 
31
+ ## [1.1.0] - 2026-10-31
32
+
33
+ ### Added
34
+
35
+ - HX updates itself, as Claude Code does. About once a day a session checks npm for a newer
36
+ release, installs it in the background with `npm install --global`, and says to restart.
37
+ `hx update` (or `hx upgrade`) updates now. Turn the background update off with
38
+ `"autoUpdates": false` in settings or `HX_DISABLE_AUTOUPDATER=1`. Only an npm install updates
39
+ itself; `hx -p` never does.
40
+ 1.0.0 has no updater: install 1.1.0 once with `npm install --global @sunilaleti/hx@latest`, and
41
+ it keeps itself current from there.
42
+
43
+ ### Changed
44
+
45
+ - CI runs the suite twice instead of four times - Linux and macOS, both on Node 24 - and
46
+ publishes with the npm that ships with Node 24. Node 22, the oldest HX supports, is checked
47
+ by installing the package on it and running `hx --version`.
48
+
49
+ ---
50
+
51
+ ## [1.0.0] - 2026-10-31
52
+
53
+ ### Changed
54
+
55
+ - The TypeScript build is the release: install it with `npm install --global @sunilaleti/hx`,
56
+ and run the same command again to update.
57
+ - With nothing signed in, `hx` opens straight into the TUI, under the HX logo, and says to type
58
+ `/login`, instead of asking for an OpenRouter key in the shell before anything is on screen.
59
+ A model whose route is not signed in says to `/login` or pick another with `/model`, and the
60
+ first message after signing in goes through.
61
+
62
+ ### Removed
63
+
64
+ - `hx upgrade`. Update by running the install command again.
65
+ - The beta channel (`@sunilaleti/hx@next`). Every version is a stable release on `latest`.
66
+ - The Python build. `hx-cli` on PyPI stops at 0.2.20 and gets no more releases. To move over,
67
+ run `uv tool uninstall hx-cli` and install from npm: `~/.hx` carries over as it is.
68
+ `install.sh` now checks for Node.js 22.15 or newer and installs from npm.
69
+ - The `live` tests, which drove the Python build's internals against real APIs.
70
+
71
+ ### Security
72
+
73
+ - A server in a project's `.hx/mcp.json` no longer starts by itself: a cloned repository could
74
+ run any command the moment `hx` opened in it. HX names each waiting server and what it runs;
75
+ `hx mcp approve <server>` starts it, and `hx mcp add` approves what it adds. An approval is for
76
+ that command, so a server whose command changes waits again. Existing project servers need
77
+ approving once.
78
+ - Read, Grep, Glob, Symbols, Write and Edit ask before touching `~/.ssh`, `~/.aws`, `~/.gnupg`,
79
+ `~/.kube`, `~/.config/gh`, `~/.docker/config.json`, `~/.netrc`, `~/.npmrc`, `~/.pypirc` and
80
+ HX's `auth.json`, in every mode and through symlinks. The sandbox kept the shell out of them,
81
+ but these tools run outside it. An allow rule naming the file opens it.
82
+ - A command hidden inside another no longer passes as read-only: `cat <(rm -rf src)`, `>(...)`,
83
+ zsh's `=(...)` and glob qualifiers, an `awk` program that pipes to or reads from a command,
84
+ `@include`, and `find -fprint0` all ask now.
85
+ - A deny rule holds however the command is spelled: behind `NAME=value` assignments, by full
86
+ path, or behind a wrapper such as `nohup`, `timeout`, `sudo` or `xargs`. An allow rule no
87
+ longer covers a command with assignments it does not name.
88
+ - `acceptEdits` covers only the project: a write outside it, through a symlink that leads out
89
+ of it, or into its `.git` or `.hx` folder asks.
90
+ - `$HX_HOME` (`~/.hx`), which holds every transcript, is made private to its owner (`0700`).
91
+
92
+ ### Fixed
93
+
94
+ - Reads that come before a write in one turn run concurrently, as reads elsewhere do.
95
+ - A login shell that does not speak POSIX sh - csh, tcsh, fish - no longer makes every command
96
+ wait out its timeout: commands run in bash.
97
+
98
+ ---
99
+
31
100
  ## [0.3.0-beta.1] - 2026-10-31
32
101
 
33
102
  ### Changed
@@ -1054,7 +1123,9 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
1054
1123
  - `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
1055
1124
  self-update, and `install.sh` bootstrapping uv with a pinned Python.
1056
1125
 
1057
- [Unreleased]: https://github.com/aletisunil/hx/compare/v0.3.0-beta.1...HEAD
1126
+ [Unreleased]: https://github.com/aletisunil/hx/compare/v1.1.0...HEAD
1127
+ [1.1.0]: https://github.com/aletisunil/hx/compare/v1.0.0...v1.1.0
1128
+ [1.0.0]: https://github.com/aletisunil/hx/compare/v0.3.0-beta.1...v1.0.0
1058
1129
  [0.3.0-beta.1]: https://github.com/aletisunil/hx/compare/v0.2.20...v0.3.0-beta.1
1059
1130
  [0.2.20]: https://github.com/aletisunil/hx/compare/v0.2.19...v0.2.20
1060
1131
  [0.2.19]: https://github.com/aletisunil/hx/compare/v0.2.18...v0.2.19
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # HX
2
2
 
3
- An agent harness for the terminal. Python core, its own terminal renderer,
4
- OpenRouter for models.
3
+ An agent harness for the terminal. TypeScript on Node.js, its own terminal
4
+ renderer, and models through OpenRouter or a ChatGPT, Claude or Devin
5
+ subscription.
5
6
 
6
7
  HX runs in your project directory, reads and edits your code, runs commands in
7
8
  a sandboxed shell, and shows you what every turn costs.
@@ -11,12 +12,23 @@ a sandboxed shell, and shows you what every turn costs.
11
12
  ## Install
12
13
 
13
14
  ```sh
14
- npm install --global @sunilaleti/hx@next
15
+ npm install --global @sunilaleti/hx
15
16
  ```
16
17
 
17
18
  HX runs on [Node.js](https://nodejs.org/) 22.15 or newer, on macOS and Linux.
18
- Update with `hx upgrade`. The `@next` tag is the beta channel while the
19
- TypeScript build is being tested; a beta upgrades to the next beta.
19
+ Then run `hx`: the first time, it opens with the HX logo and says to type
20
+ `/login`.
21
+
22
+ HX keeps itself up to date, as Claude Code does. About once a day, a session
23
+ checks npm for a newer release, installs it in the background, and says to
24
+ restart. Run `hx update` to update now. To turn the background update off, set
25
+ `"autoUpdates": false` in `~/.hx/settings.json`, or `HX_DISABLE_AUTOUPDATER=1`.
26
+
27
+ Or let the install script check your Node.js version and run that command:
28
+
29
+ ```sh
30
+ curl -fsSL https://raw.githubusercontent.com/aletisunil/hx/main/install.sh | sh
31
+ ```
20
32
 
21
33
  ### `@sunilaleti/hx` on npm, `hx` in your terminal
22
34
 
@@ -37,9 +49,14 @@ to your `PATH`:
37
49
  export PATH="$(npm prefix --global)/bin:$PATH"
38
50
  ```
39
51
 
40
- The Python build, `hx-cli` on PyPI, keeps working on the same `~/.hx`:
41
- sessions, sign-ins and settings carry over in both directions. Install one or
42
- the other - both put a command named `hx` on your `PATH`.
52
+ Coming from `hx-cli`, the Python build on PyPI? It is no longer released.
53
+ Remove it, then install from npm - sessions, sign-ins and settings in `~/.hx`
54
+ carry over:
55
+
56
+ ```sh
57
+ uv tool uninstall hx-cli
58
+ npm install --global @sunilaleti/hx
59
+ ```
43
60
 
44
61
  ### Credentials
45
62
 
@@ -329,6 +346,7 @@ hx --no-sandbox # disable OS sandboxing (rules still apply)
329
346
  hx --system-prompt @p.md # replace the system prompt for one run
330
347
  hx --append-system-prompt "Always run the tests" # add to it; repeatable
331
348
  hx mcp list|add|remove # manage MCP servers
349
+ hx mcp approve <server> # let a server from the project's .hx/mcp.json start
332
350
  hx mcp login <server> # sign in to a remote MCP server in the browser
333
351
  hx mcp logout <server> # forget a remote MCP server's sign-in
334
352
  hx auth [set|clear] # manage the OpenRouter key
@@ -336,7 +354,7 @@ hx auth login [provider] # sign in (openrouter, openai-codex, devin, claude-c
336
354
  hx auth logout <provider> # forget a stored credential
337
355
  hx docs [section] # the manual; no argument lists its sections
338
356
  hx changelog [version] # what shipped in each version
339
- hx upgrade # update to the latest release
357
+ hx update # install the newest release now (also: hx upgrade)
340
358
  ```
341
359
 
342
360
  Print mode is the scriptable one: stdout carries only the assistant's text, so
@@ -373,7 +391,7 @@ allowlist it if you would rather not be asked:
373
391
 
374
392
  ### Keys
375
393
 
376
- Every key below is defined once, in `src/hx/keys.py`. `/help`, the hints line
394
+ Every key below is defined once, in `src/keys.ts`. `/help`, the hints line
377
395
  under the prompt and this table all read from that registry, and any of them can
378
396
  be rebound (see [Keybindings](#keybindings)).
379
397
 
@@ -474,7 +492,7 @@ says so; copy the file over and drag or paste its path instead.
474
492
  #### Keybindings
475
493
 
476
494
  Any key can be rebound in `~/.hx/keybindings.json`, keyed by the action ids in
477
- `src/hx/keys.py`:
495
+ `src/keys.ts`:
478
496
 
479
497
  ```json
480
498
  {
@@ -651,6 +669,7 @@ along with the `.hx/.gitignore` it added to hide it.
651
669
  {
652
670
  "theme": "dark", // dark | light | ansi, or a file in ~/.hx/themes
653
671
  "quiet_startup": false, // skip the startup header
672
+ "autoUpdates": true, // install new releases in the background, once a day
654
673
 
655
674
  "models": {
656
675
  "model": "anthropic/claude-sonnet-4.5",
@@ -721,7 +740,7 @@ route at another OpenAI-compatible endpoint - a gateway, or a recording proxy.
721
740
  stderr so stdout stays pipeable. `/prompt` shows the same thing inside a running
722
741
  session.
723
742
 
724
- The built-in prompt is `SYSTEM_PROMPT` in `src/hx/core/context.py`. It is
743
+ The built-in prompt is `SYSTEM_PROMPT` in `src/core/context.ts`. It is
725
744
  replaced by the first of these that exists:
726
745
 
727
746
  | Source | Scope |
@@ -785,7 +804,7 @@ mapping semantic roles onto them. Drop one in `~/.hx/themes/mine.json` and
785
804
  ```
786
805
 
787
806
  The role names are pi's, so [pi](https://github.com/earendil-works/pi) theme
788
- files load here unchanged. `src/hx/tui/themes/dark.json` is the reference.
807
+ files load here unchanged. `src/tui/themes/dark.json` is the reference.
789
808
  A few roles pi has no concept of may be left out, and borrow another role:
790
809
  `toolArgs`, the colour of a tool call's paths and patterns, borrows `accent`;
791
810
  `panel` and `mdCodeBlockBg` borrow `surface`; `userText` borrows `text`; and
@@ -842,7 +861,19 @@ Two independent layers guard every tool call, and both must pass.
842
861
  holds even in bypass mode. Shell commands are decomposed into their real
843
862
  segments first, so an allow rule for `git status` does not carry
844
863
  `&& rm -rf /` along with it; a command that cannot be decomposed with
845
- confidence prompts rather than passing.
864
+ confidence prompts rather than passing. What runs inside a command counts as a
865
+ segment of its own - `$(...)`, `<(...)`, `>(...)` - so `cat <(rm -rf src)` is
866
+ not a read-only `cat`. A deny rule matches the command however it is spelled:
867
+ `deny: Bash(rm:*)` also stops `FOO=1 rm`, `/bin/rm` and `nohup rm`. An allow
868
+ rule covers only what it names, so `allow: Bash(ls)` does not cover
869
+ `LD_PRELOAD=x ls`.
870
+
871
+ **Credential files** - the same `~/.ssh`, `~/.aws` and `auth.json` the sandbox
872
+ guards - always ask before Read, Grep, Glob, Symbols, Write or Edit touch them,
873
+ in every mode, symlinks followed. Those tools run inside HX, outside the
874
+ sandbox. An allow rule that names the file, such as `allow: Read(~/.ssh/config)`,
875
+ opens that one file. A search that starts above them, from `$HOME`, leaves
876
+ them out.
846
877
 
847
878
  **An OS sandbox** wraps command execution: Seatbelt on macOS, bubblewrap on
848
879
  Linux. The filesystem is readable, writes are confined to the project and the
@@ -855,7 +886,13 @@ and the Tavily and Devin keys) are removed from the environment of every
855
886
  command it runs.
856
887
 
857
888
  Modes cycle with shift+tab: `plan` (read-only — mutating tools are not even
858
- offered to the model), `default`, `acceptEdits`, `bypass`.
889
+ offered to the model), `default`, `acceptEdits`, `bypass`. `acceptEdits` covers
890
+ the project's own files: a Write or Edit outside the project, through a symlink
891
+ that leads out of it, or into its `.git` or `.hx` folder still asks.
892
+
893
+ **Nothing that arrives with a clone runs by itself.** Hooks in a project's
894
+ shared `.hx/settings.json` are ignored, and a server in its `.hx/mcp.json`
895
+ starts only once you approve it - see [Extending it](#extending-it).
859
896
 
860
897
  ---
861
898
 
@@ -1083,7 +1120,16 @@ construction.
1083
1120
  ```
1084
1121
 
1085
1122
  Or `hx mcp add local python server.py`, or
1086
- `hx mcp add remote --url https://example.com/mcp`. Tools arrive namespaced
1123
+ `hx mcp add remote --url https://example.com/mcp`.
1124
+
1125
+ A server in the project's `.hx/mcp.json` runs a command on your machine, and
1126
+ the file arrives with a clone - so HX does not start it until you approve it.
1127
+ A session names each one waiting, with what it runs. `hx mcp approve local`
1128
+ starts it from the next session on, and `hx mcp add` approves what it adds.
1129
+ The approval is for that command: if a `git pull` changes it, the server waits
1130
+ again. Servers in your own `~/.hx/mcp.json` need no approval.
1131
+
1132
+ Tools arrive namespaced
1087
1133
  `mcp__<server>__<tool>` in a deterministic order. Servers connect concurrently
1088
1134
  with a per-server timeout; one that is broken or slow logs a warning and is
1089
1135
  dropped rather than taking the session with it. A server that changes its tool
@@ -1186,23 +1232,26 @@ bypassed by delegating through `Task`.
1186
1232
 
1187
1233
  ```sh
1188
1234
  git clone https://github.com/aletisunil/hx && cd hx
1189
- uv sync --extra dev
1235
+ npm ci
1190
1236
 
1191
- uv run pytest # the suite; live tests are deselected
1192
- uv run ruff check . && uv run ruff format --check .
1193
- uv run mypy # strict
1194
- uv run hx # run from the checkout
1237
+ npm run check # tsc, biome, and the source-literal check
1238
+ npm run build # dist/hx.js, the bundle npm ships
1239
+ node dist/hx.js # run from the checkout
1240
+ npm run e2e # build, then the whole end-to-end suite
1241
+ uv run ruff check . && uv run ruff format --check . # the suite's own Python
1195
1242
  ```
1196
1243
 
1197
- The suite is end-to-end. Each test in `tests/e2e/` starts the installed `hx`
1198
- binary - on a real pseudo-terminal for the TUI, or as a plain subprocess for
1244
+ The suite is end-to-end, written in Python with pytest, and run with
1245
+ [uv](https://docs.astral.sh/uv/). Each test in `tests/e2e/` starts the `hx`
1246
+ bundle - on a real pseudo-terminal for the TUI, or as a plain subprocess for
1199
1247
  print mode and the other commands - in a private `$HOME` and `$HX_HOME`, and
1200
1248
  points it at a local OpenRouter stand-in with `HX_OPENROUTER_BASE_URL`, and
1201
1249
  at a ChatGPT stand-in with `HX_CODEX_BASE_URL`.
1202
1250
  Everything between the keyboard and the socket is the shipped code; only the
1203
1251
  model's replies are scripted. The screen is read back through a VT emulator,
1204
1252
  so assertions are on the cells a terminal shows, not on the bytes HX meant to
1205
- write.
1253
+ write. The suite never imports HX: it tests `dist/hx.js` by default, and any
1254
+ other `hx` - an installed one, say - named by `HX_E2E_BIN`.
1206
1255
 
1207
1256
  Every run writes `e2e-report/`: `index.html` shows each test's outcome and
1208
1257
  every screen it checked, rendered in colour, and one text file per screen lets
@@ -1212,41 +1261,26 @@ two runs be compared with `diff -r`. CI uploads it from every test job as an
1212
1261
  Tests that need the OS sandbox are skipped where there is no backend
1213
1262
  (`sandbox-exec` on macOS, `bwrap` on Linux).
1214
1263
 
1215
- Tests marked `live` hit a real API. The OpenRouter ones cost money:
1216
-
1217
- ```sh
1218
- OPENROUTER_API_KEY=... uv run pytest -m live tests/test_live.py
1219
-
1220
- # the Codex route draws on your ChatGPT subscription instead
1221
- hx auth login openai-codex
1222
- uv run pytest -m live tests/test_live_codex.py
1223
-
1224
- # and the Devin route on your Devin subscription
1225
- hx auth login devin
1226
- uv run pytest -m live tests/test_live_devin.py
1227
- ```
1228
-
1229
- They are the only place the wire formats, streaming, tool use and a genuine
1230
- cache hit are proven against a real service; everything else runs against the
1231
- stand-in.
1232
-
1233
1264
  ### Layout
1234
1265
 
1235
1266
  ```
1236
- src/hx/
1237
- cli.py config.py paths.py frontmatter.py git.py
1267
+ src/
1268
+ cli.ts runtime.ts config.ts paths.ts frontmatter.ts git.ts trace.ts
1238
1269
  core/ loop, context assembly, compaction, late injection, sessions, usage
1239
1270
  auth/ credential store, OAuth flows, per-route resolution
1240
- providers/ OpenRouter, Codex, Devin, the model catalogue
1271
+ providers/ OpenRouter, Codex, Claude Code, Devin, the model catalogue
1241
1272
  tools/ Bash, Read, Write, Edit, Glob, Grep, Symbols, TodoWrite, Task,
1242
1273
  WebSearch, WebFetch, line anchors, output capping
1243
1274
  permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
1244
1275
  hooks/ the four events, the trust boundary, subprocess execution
1245
- skills/ agents/ mcp/
1246
- keys.py keybinding registry: ids, defaults, descriptions, user overrides
1276
+ skills/ agents/
1277
+ mcp/ client, OAuth, and approval for a project's servers
1278
+ keys.ts keybinding registry: ids, defaults, descriptions, user overrides
1247
1279
  term/ the renderer: raw mode, input decoding, the differ, the screen
1248
1280
  tui/ views, commands, per-tool renderers, themes
1249
1281
  themes/ the shipped palettes, as JSON
1282
+ py.ts pathlib.ts json.ts Python's string, path and JSON behaviour, which
1283
+ HX's output and files were written against
1250
1284
  ```
1251
1285
 
1252
1286
  The core is headless and emits events; the TUI and print mode are both just
@@ -1257,44 +1291,43 @@ imports `tui/`.
1257
1291
 
1258
1292
  ## Releasing
1259
1293
 
1260
- For maintainers. HX publishes to PyPI as **`hx-cli`** from CI, on a tag.
1261
-
1262
- One-time setup:
1263
-
1264
- 1. Push the repo to `github.com/<owner>/hx` and update the URLs in
1265
- `pyproject.toml`.
1266
- 2. Create a GitHub environment named `pypi`.
1267
- 3. On PyPI, add a [trusted publisher](https://docs.pypi.org/trusted-publishers/)
1268
- for the project: owner, repo `hx`, workflow `ci.yml`, environment `pypi`.
1269
- No API token is stored anywhere.
1294
+ For maintainers. HX is published to npm as **`@sunilaleti/hx`**, on the
1295
+ `latest` tag, by CI. Every version is a stable release; there is no beta
1296
+ channel.
1270
1297
 
1271
- Each release:
1298
+ A push to `main` whose `package.json` version is not on npm yet is a release:
1299
+ once lint, the test suite and the install script pass, CI's `release` job
1300
+ publishes it to npm, tags it `vX.Y.Z` and makes the GitHub release, with the
1301
+ version's `CHANGELOG.md` section as its notes. A push that leaves the version
1302
+ alone publishes nothing.
1272
1303
 
1273
1304
  ```sh
1274
1305
  # 1. move CHANGELOG.md's [Unreleased] entries under the new version, dated,
1275
1306
  # and leave a fresh empty [Unreleased] behind
1276
- # 2. bump __version__ in src/hx/__init__.py
1277
- # 3. uv run pytest tests/e2e/test_cli.py # the version must have a section
1278
- git commit -am "release: X.Y.Z"
1279
- git tag v$(uv run hx --version | cut -d' ' -f2) && git push origin main --tags
1307
+ # 2. bump "version" in package.json
1308
+ npm version X.Y.Z --no-git-tag-version
1309
+ # 3. the whole suite; the version must have a CHANGELOG section
1310
+ npm run e2e
1311
+ git commit -am "chore(release): X.Y.Z"
1312
+ git push origin main
1280
1313
  ```
1281
1314
 
1282
1315
  Step 1 is not optional and is not a convention: `tests/e2e/test_cli.py` fails the
1283
- whole suite when `__version__` has no `CHANGELOG.md` section, so a release that
1284
- skips it cannot get past CI to the `publish` job. The file ships inside the
1285
- wheel and `hx changelog` prints it, so a version with no section there reaches
1286
- users as a blank release.
1316
+ whole suite when the version has no `CHANGELOG.md` section, and the `release`
1317
+ job checks again before it publishes. The file ships inside the bundle and
1318
+ `hx changelog` prints it, so a version with no section there would reach users
1319
+ as a blank release.
1287
1320
 
1288
- The `publish` job runs only on `refs/tags/v*` and only after lint, the test
1289
- suite and an install-script run in a clean Debian container have passed. It builds with
1290
- `uv build`, checks that the wheel's version matches the tag and has a `CHANGELOG.md`
1291
- section, uploads via OIDC, and then creates the GitHub release with that section
1292
- as its notes.
1321
+ One-time setup: CI publishes with npm's
1322
+ [trusted publishing](https://docs.npmjs.com/trusted-publishers), so no npm
1323
+ token is stored anywhere. On npmjs.com, open `@sunilaleti/hx` → Settings →
1324
+ Trusted Publisher, choose GitHub Actions, and enter owner `aletisunil`,
1325
+ repository `hx` and workflow `ci.yml`, with no environment.
1293
1326
 
1294
- To check a build before tagging:
1327
+ To see what npm would ship before releasing:
1295
1328
 
1296
1329
  ```sh
1297
- uv build && ls dist/
1330
+ npm pack --dry-run
1298
1331
  ```
1299
1332
 
1300
1333
  ---
@@ -1306,13 +1339,11 @@ streaming with prefix caching and accurate cost accounting, session persistence
1306
1339
  and resume, the tool suite, the permission engine and OS sandbox, late
1307
1340
  injection, compaction, output capping, skills, subagents, MCP, and the TUI.
1308
1341
 
1309
- Three routes to a model: an OpenRouter API key, a ChatGPT Plus/Pro
1310
- subscription over OAuth, or a Devin subscription over OAuth. The model id
1311
- decides which.
1342
+ Four routes to a model: an OpenRouter API key, a ChatGPT Plus/Pro
1343
+ subscription over OAuth, a Claude Pro/Max subscription through Claude Code, or
1344
+ a Devin subscription over OAuth. The model id decides which.
1312
1345
 
1313
- Published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/), released
1314
- from CI on a tag.
1346
+ Published to npm as [`@sunilaleti/hx`](https://www.npmjs.com/package/@sunilaleti/hx).
1347
+ The Python build that came before it, `hx-cli` on PyPI, stopped at 0.2.20.
1315
1348
 
1316
- The one thing still unproven in CI is a live call on any route: the `live`
1317
- tests exist and cover all three wire formats, tool use, reasoning replay and a
1318
- genuine cache hit, but they need a credential and are deselected by default.
1349
+ No test reaches a real model: every route is proven against a local stand-in.