@sunilaleti/hx 0.3.0-beta.1 → 1.0.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,55 @@ the project follows [semantic versioning](https://semver.org/).
28
28
 
29
29
  ---
30
30
 
31
+ ## [1.0.0] - 2026-10-31
32
+
33
+ ### Changed
34
+
35
+ - The TypeScript build is the release: install it with `npm install --global @sunilaleti/hx`,
36
+ and run the same command again to update.
37
+ - With nothing signed in, `hx` opens straight into the TUI, under the HX logo, and says to type
38
+ `/login`, instead of asking for an OpenRouter key in the shell before anything is on screen.
39
+ A model whose route is not signed in says to `/login` or pick another with `/model`, and the
40
+ first message after signing in goes through.
41
+
42
+ ### Removed
43
+
44
+ - `hx upgrade`. Update by running the install command again.
45
+ - The beta channel (`@sunilaleti/hx@next`). Every version is a stable release on `latest`.
46
+ - The Python build. `hx-cli` on PyPI stops at 0.2.20 and gets no more releases. To move over,
47
+ run `uv tool uninstall hx-cli` and install from npm: `~/.hx` carries over as it is.
48
+ `install.sh` now checks for Node.js 22.15 or newer and installs from npm.
49
+ - The `live` tests, which drove the Python build's internals against real APIs.
50
+
51
+ ### Security
52
+
53
+ - A server in a project's `.hx/mcp.json` no longer starts by itself: a cloned repository could
54
+ run any command the moment `hx` opened in it. HX names each waiting server and what it runs;
55
+ `hx mcp approve <server>` starts it, and `hx mcp add` approves what it adds. An approval is for
56
+ that command, so a server whose command changes waits again. Existing project servers need
57
+ approving once.
58
+ - Read, Grep, Glob, Symbols, Write and Edit ask before touching `~/.ssh`, `~/.aws`, `~/.gnupg`,
59
+ `~/.kube`, `~/.config/gh`, `~/.docker/config.json`, `~/.netrc`, `~/.npmrc`, `~/.pypirc` and
60
+ HX's `auth.json`, in every mode and through symlinks. The sandbox kept the shell out of them,
61
+ but these tools run outside it. An allow rule naming the file opens it.
62
+ - A command hidden inside another no longer passes as read-only: `cat <(rm -rf src)`, `>(...)`,
63
+ zsh's `=(...)` and glob qualifiers, an `awk` program that pipes to or reads from a command,
64
+ `@include`, and `find -fprint0` all ask now.
65
+ - A deny rule holds however the command is spelled: behind `NAME=value` assignments, by full
66
+ path, or behind a wrapper such as `nohup`, `timeout`, `sudo` or `xargs`. An allow rule no
67
+ longer covers a command with assignments it does not name.
68
+ - `acceptEdits` covers only the project: a write outside it, through a symlink that leads out
69
+ of it, or into its `.git` or `.hx` folder asks.
70
+ - `$HX_HOME` (`~/.hx`), which holds every transcript, is made private to its owner (`0700`).
71
+
72
+ ### Fixed
73
+
74
+ - Reads that come before a write in one turn run concurrently, as reads elsewhere do.
75
+ - A login shell that does not speak POSIX sh - csh, tcsh, fish - no longer makes every command
76
+ wait out its timeout: commands run in bash.
77
+
78
+ ---
79
+
31
80
  ## [0.3.0-beta.1] - 2026-10-31
32
81
 
33
82
  ### Changed
@@ -1054,7 +1103,8 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
1054
1103
  - `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
1055
1104
  self-update, and `install.sh` bootstrapping uv with a pinned Python.
1056
1105
 
1057
- [Unreleased]: https://github.com/aletisunil/hx/compare/v0.3.0-beta.1...HEAD
1106
+ [Unreleased]: https://github.com/aletisunil/hx/compare/v1.0.0...HEAD
1107
+ [1.0.0]: https://github.com/aletisunil/hx/compare/v0.3.0-beta.1...v1.0.0
1058
1108
  [0.3.0-beta.1]: https://github.com/aletisunil/hx/compare/v0.2.20...v0.3.0-beta.1
1059
1109
  [0.2.20]: https://github.com/aletisunil/hx/compare/v0.2.19...v0.2.20
1060
1110
  [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,18 @@ 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
+ Run the same command again to update. Then run `hx`: the first time, it opens
20
+ with the HX logo and says to type `/login`.
21
+
22
+ Or let the install script check your Node.js version and run that command:
23
+
24
+ ```sh
25
+ curl -fsSL https://raw.githubusercontent.com/aletisunil/hx/main/install.sh | sh
26
+ ```
20
27
 
21
28
  ### `@sunilaleti/hx` on npm, `hx` in your terminal
22
29
 
@@ -37,9 +44,14 @@ to your `PATH`:
37
44
  export PATH="$(npm prefix --global)/bin:$PATH"
38
45
  ```
39
46
 
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`.
47
+ Coming from `hx-cli`, the Python build on PyPI? It is no longer released.
48
+ Remove it, then install from npm - sessions, sign-ins and settings in `~/.hx`
49
+ carry over:
50
+
51
+ ```sh
52
+ uv tool uninstall hx-cli
53
+ npm install --global @sunilaleti/hx
54
+ ```
43
55
 
44
56
  ### Credentials
45
57
 
@@ -329,6 +341,7 @@ hx --no-sandbox # disable OS sandboxing (rules still apply)
329
341
  hx --system-prompt @p.md # replace the system prompt for one run
330
342
  hx --append-system-prompt "Always run the tests" # add to it; repeatable
331
343
  hx mcp list|add|remove # manage MCP servers
344
+ hx mcp approve <server> # let a server from the project's .hx/mcp.json start
332
345
  hx mcp login <server> # sign in to a remote MCP server in the browser
333
346
  hx mcp logout <server> # forget a remote MCP server's sign-in
334
347
  hx auth [set|clear] # manage the OpenRouter key
@@ -336,7 +349,6 @@ hx auth login [provider] # sign in (openrouter, openai-codex, devin, claude-c
336
349
  hx auth logout <provider> # forget a stored credential
337
350
  hx docs [section] # the manual; no argument lists its sections
338
351
  hx changelog [version] # what shipped in each version
339
- hx upgrade # update to the latest release
340
352
  ```
341
353
 
342
354
  Print mode is the scriptable one: stdout carries only the assistant's text, so
@@ -373,7 +385,7 @@ allowlist it if you would rather not be asked:
373
385
 
374
386
  ### Keys
375
387
 
376
- Every key below is defined once, in `src/hx/keys.py`. `/help`, the hints line
388
+ Every key below is defined once, in `src/keys.ts`. `/help`, the hints line
377
389
  under the prompt and this table all read from that registry, and any of them can
378
390
  be rebound (see [Keybindings](#keybindings)).
379
391
 
@@ -474,7 +486,7 @@ says so; copy the file over and drag or paste its path instead.
474
486
  #### Keybindings
475
487
 
476
488
  Any key can be rebound in `~/.hx/keybindings.json`, keyed by the action ids in
477
- `src/hx/keys.py`:
489
+ `src/keys.ts`:
478
490
 
479
491
  ```json
480
492
  {
@@ -721,7 +733,7 @@ route at another OpenAI-compatible endpoint - a gateway, or a recording proxy.
721
733
  stderr so stdout stays pipeable. `/prompt` shows the same thing inside a running
722
734
  session.
723
735
 
724
- The built-in prompt is `SYSTEM_PROMPT` in `src/hx/core/context.py`. It is
736
+ The built-in prompt is `SYSTEM_PROMPT` in `src/core/context.ts`. It is
725
737
  replaced by the first of these that exists:
726
738
 
727
739
  | Source | Scope |
@@ -785,7 +797,7 @@ mapping semantic roles onto them. Drop one in `~/.hx/themes/mine.json` and
785
797
  ```
786
798
 
787
799
  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.
800
+ files load here unchanged. `src/tui/themes/dark.json` is the reference.
789
801
  A few roles pi has no concept of may be left out, and borrow another role:
790
802
  `toolArgs`, the colour of a tool call's paths and patterns, borrows `accent`;
791
803
  `panel` and `mdCodeBlockBg` borrow `surface`; `userText` borrows `text`; and
@@ -842,7 +854,19 @@ Two independent layers guard every tool call, and both must pass.
842
854
  holds even in bypass mode. Shell commands are decomposed into their real
843
855
  segments first, so an allow rule for `git status` does not carry
844
856
  `&& rm -rf /` along with it; a command that cannot be decomposed with
845
- confidence prompts rather than passing.
857
+ confidence prompts rather than passing. What runs inside a command counts as a
858
+ segment of its own - `$(...)`, `<(...)`, `>(...)` - so `cat <(rm -rf src)` is
859
+ not a read-only `cat`. A deny rule matches the command however it is spelled:
860
+ `deny: Bash(rm:*)` also stops `FOO=1 rm`, `/bin/rm` and `nohup rm`. An allow
861
+ rule covers only what it names, so `allow: Bash(ls)` does not cover
862
+ `LD_PRELOAD=x ls`.
863
+
864
+ **Credential files** - the same `~/.ssh`, `~/.aws` and `auth.json` the sandbox
865
+ guards - always ask before Read, Grep, Glob, Symbols, Write or Edit touch them,
866
+ in every mode, symlinks followed. Those tools run inside HX, outside the
867
+ sandbox. An allow rule that names the file, such as `allow: Read(~/.ssh/config)`,
868
+ opens that one file. A search that starts above them, from `$HOME`, leaves
869
+ them out.
846
870
 
847
871
  **An OS sandbox** wraps command execution: Seatbelt on macOS, bubblewrap on
848
872
  Linux. The filesystem is readable, writes are confined to the project and the
@@ -855,7 +879,13 @@ and the Tavily and Devin keys) are removed from the environment of every
855
879
  command it runs.
856
880
 
857
881
  Modes cycle with shift+tab: `plan` (read-only — mutating tools are not even
858
- offered to the model), `default`, `acceptEdits`, `bypass`.
882
+ offered to the model), `default`, `acceptEdits`, `bypass`. `acceptEdits` covers
883
+ the project's own files: a Write or Edit outside the project, through a symlink
884
+ that leads out of it, or into its `.git` or `.hx` folder still asks.
885
+
886
+ **Nothing that arrives with a clone runs by itself.** Hooks in a project's
887
+ shared `.hx/settings.json` are ignored, and a server in its `.hx/mcp.json`
888
+ starts only once you approve it - see [Extending it](#extending-it).
859
889
 
860
890
  ---
861
891
 
@@ -1083,7 +1113,16 @@ construction.
1083
1113
  ```
1084
1114
 
1085
1115
  Or `hx mcp add local python server.py`, or
1086
- `hx mcp add remote --url https://example.com/mcp`. Tools arrive namespaced
1116
+ `hx mcp add remote --url https://example.com/mcp`.
1117
+
1118
+ A server in the project's `.hx/mcp.json` runs a command on your machine, and
1119
+ the file arrives with a clone - so HX does not start it until you approve it.
1120
+ A session names each one waiting, with what it runs. `hx mcp approve local`
1121
+ starts it from the next session on, and `hx mcp add` approves what it adds.
1122
+ The approval is for that command: if a `git pull` changes it, the server waits
1123
+ again. Servers in your own `~/.hx/mcp.json` need no approval.
1124
+
1125
+ Tools arrive namespaced
1087
1126
  `mcp__<server>__<tool>` in a deterministic order. Servers connect concurrently
1088
1127
  with a per-server timeout; one that is broken or slow logs a warning and is
1089
1128
  dropped rather than taking the session with it. A server that changes its tool
@@ -1186,23 +1225,26 @@ bypassed by delegating through `Task`.
1186
1225
 
1187
1226
  ```sh
1188
1227
  git clone https://github.com/aletisunil/hx && cd hx
1189
- uv sync --extra dev
1228
+ npm ci
1190
1229
 
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
1230
+ npm run check # tsc, biome, and the source-literal check
1231
+ npm run build # dist/hx.js, the bundle npm ships
1232
+ node dist/hx.js # run from the checkout
1233
+ npm run e2e # build, then the whole end-to-end suite
1234
+ uv run ruff check . && uv run ruff format --check . # the suite's own Python
1195
1235
  ```
1196
1236
 
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
1237
+ The suite is end-to-end, written in Python with pytest, and run with
1238
+ [uv](https://docs.astral.sh/uv/). Each test in `tests/e2e/` starts the `hx`
1239
+ bundle - on a real pseudo-terminal for the TUI, or as a plain subprocess for
1199
1240
  print mode and the other commands - in a private `$HOME` and `$HX_HOME`, and
1200
1241
  points it at a local OpenRouter stand-in with `HX_OPENROUTER_BASE_URL`, and
1201
1242
  at a ChatGPT stand-in with `HX_CODEX_BASE_URL`.
1202
1243
  Everything between the keyboard and the socket is the shipped code; only the
1203
1244
  model's replies are scripted. The screen is read back through a VT emulator,
1204
1245
  so assertions are on the cells a terminal shows, not on the bytes HX meant to
1205
- write.
1246
+ write. The suite never imports HX: it tests `dist/hx.js` by default, and any
1247
+ other `hx` - an installed one, say - named by `HX_E2E_BIN`.
1206
1248
 
1207
1249
  Every run writes `e2e-report/`: `index.html` shows each test's outcome and
1208
1250
  every screen it checked, rendered in colour, and one text file per screen lets
@@ -1212,41 +1254,26 @@ two runs be compared with `diff -r`. CI uploads it from every test job as an
1212
1254
  Tests that need the OS sandbox are skipped where there is no backend
1213
1255
  (`sandbox-exec` on macOS, `bwrap` on Linux).
1214
1256
 
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
1257
  ### Layout
1234
1258
 
1235
1259
  ```
1236
- src/hx/
1237
- cli.py config.py paths.py frontmatter.py git.py
1260
+ src/
1261
+ cli.ts runtime.ts config.ts paths.ts frontmatter.ts git.ts trace.ts
1238
1262
  core/ loop, context assembly, compaction, late injection, sessions, usage
1239
1263
  auth/ credential store, OAuth flows, per-route resolution
1240
- providers/ OpenRouter, Codex, Devin, the model catalogue
1264
+ providers/ OpenRouter, Codex, Claude Code, Devin, the model catalogue
1241
1265
  tools/ Bash, Read, Write, Edit, Glob, Grep, Symbols, TodoWrite, Task,
1242
1266
  WebSearch, WebFetch, line anchors, output capping
1243
1267
  permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
1244
1268
  hooks/ the four events, the trust boundary, subprocess execution
1245
- skills/ agents/ mcp/
1246
- keys.py keybinding registry: ids, defaults, descriptions, user overrides
1269
+ skills/ agents/
1270
+ mcp/ client, OAuth, and approval for a project's servers
1271
+ keys.ts keybinding registry: ids, defaults, descriptions, user overrides
1247
1272
  term/ the renderer: raw mode, input decoding, the differ, the screen
1248
1273
  tui/ views, commands, per-tool renderers, themes
1249
1274
  themes/ the shipped palettes, as JSON
1275
+ py.ts pathlib.ts json.ts Python's string, path and JSON behaviour, which
1276
+ HX's output and files were written against
1250
1277
  ```
1251
1278
 
1252
1279
  The core is headless and emits events; the TUI and print mode are both just
@@ -1257,44 +1284,43 @@ imports `tui/`.
1257
1284
 
1258
1285
  ## Releasing
1259
1286
 
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.
1287
+ For maintainers. HX is published to npm as **`@sunilaleti/hx`**, on the
1288
+ `latest` tag, by CI. Every version is a stable release; there is no beta
1289
+ channel.
1270
1290
 
1271
- Each release:
1291
+ A push to `main` whose `package.json` version is not on npm yet is a release:
1292
+ once lint, the test suite and the install script pass, CI's `release` job
1293
+ publishes it to npm, tags it `vX.Y.Z` and makes the GitHub release, with the
1294
+ version's `CHANGELOG.md` section as its notes. A push that leaves the version
1295
+ alone publishes nothing.
1272
1296
 
1273
1297
  ```sh
1274
1298
  # 1. move CHANGELOG.md's [Unreleased] entries under the new version, dated,
1275
1299
  # 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
1300
+ # 2. bump "version" in package.json
1301
+ npm version X.Y.Z --no-git-tag-version
1302
+ # 3. the whole suite; the version must have a CHANGELOG section
1303
+ npm run e2e
1304
+ git commit -am "chore(release): X.Y.Z"
1305
+ git push origin main
1280
1306
  ```
1281
1307
 
1282
1308
  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.
1309
+ whole suite when the version has no `CHANGELOG.md` section, and the `release`
1310
+ job checks again before it publishes. The file ships inside the bundle and
1311
+ `hx changelog` prints it, so a version with no section there would reach users
1312
+ as a blank release.
1287
1313
 
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.
1314
+ One-time setup: CI publishes with npm's
1315
+ [trusted publishing](https://docs.npmjs.com/trusted-publishers), so no npm
1316
+ token is stored anywhere. On npmjs.com, open `@sunilaleti/hx` → Settings →
1317
+ Trusted Publisher, choose GitHub Actions, and enter owner `aletisunil`,
1318
+ repository `hx` and workflow `ci.yml`, with no environment.
1293
1319
 
1294
- To check a build before tagging:
1320
+ To see what npm would ship before releasing:
1295
1321
 
1296
1322
  ```sh
1297
- uv build && ls dist/
1323
+ npm pack --dry-run
1298
1324
  ```
1299
1325
 
1300
1326
  ---
@@ -1306,13 +1332,11 @@ streaming with prefix caching and accurate cost accounting, session persistence
1306
1332
  and resume, the tool suite, the permission engine and OS sandbox, late
1307
1333
  injection, compaction, output capping, skills, subagents, MCP, and the TUI.
1308
1334
 
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.
1335
+ Four routes to a model: an OpenRouter API key, a ChatGPT Plus/Pro
1336
+ subscription over OAuth, a Claude Pro/Max subscription through Claude Code, or
1337
+ a Devin subscription over OAuth. The model id decides which.
1312
1338
 
1313
- Published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/), released
1314
- from CI on a tag.
1339
+ Published to npm as [`@sunilaleti/hx`](https://www.npmjs.com/package/@sunilaleti/hx).
1340
+ The Python build that came before it, `hx-cli` on PyPI, stopped at 0.2.20.
1315
1341
 
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.
1342
+ No test reaches a real model: every route is proven against a local stand-in.