hx-cli 0.1.0__tar.gz

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.
Files changed (103) hide show
  1. hx_cli-0.1.0/.github/workflows/ci.yml +63 -0
  2. hx_cli-0.1.0/.gitignore +14 -0
  3. hx_cli-0.1.0/PKG-INFO +430 -0
  4. hx_cli-0.1.0/README.md +400 -0
  5. hx_cli-0.1.0/install.sh +62 -0
  6. hx_cli-0.1.0/pyproject.toml +87 -0
  7. hx_cli-0.1.0/src/hx/__init__.py +5 -0
  8. hx_cli-0.1.0/src/hx/agents/__init__.py +1 -0
  9. hx_cli-0.1.0/src/hx/agents/definitions.py +106 -0
  10. hx_cli-0.1.0/src/hx/agents/subagent.py +190 -0
  11. hx_cli-0.1.0/src/hx/cli.py +667 -0
  12. hx_cli-0.1.0/src/hx/config.py +277 -0
  13. hx_cli-0.1.0/src/hx/core/__init__.py +1 -0
  14. hx_cli-0.1.0/src/hx/core/compaction.py +245 -0
  15. hx_cli-0.1.0/src/hx/core/context.py +271 -0
  16. hx_cli-0.1.0/src/hx/core/events.py +183 -0
  17. hx_cli-0.1.0/src/hx/core/lateinject.py +121 -0
  18. hx_cli-0.1.0/src/hx/core/loop.py +537 -0
  19. hx_cli-0.1.0/src/hx/core/messages.py +164 -0
  20. hx_cli-0.1.0/src/hx/core/session.py +208 -0
  21. hx_cli-0.1.0/src/hx/core/usage.py +129 -0
  22. hx_cli-0.1.0/src/hx/frontmatter.py +80 -0
  23. hx_cli-0.1.0/src/hx/mcp/__init__.py +1 -0
  24. hx_cli-0.1.0/src/hx/mcp/client.py +319 -0
  25. hx_cli-0.1.0/src/hx/mcp/manager.py +265 -0
  26. hx_cli-0.1.0/src/hx/paths.py +90 -0
  27. hx_cli-0.1.0/src/hx/permissions/__init__.py +7 -0
  28. hx_cli-0.1.0/src/hx/permissions/engine.py +406 -0
  29. hx_cli-0.1.0/src/hx/permissions/parser.py +306 -0
  30. hx_cli-0.1.0/src/hx/permissions/sandbox.py +227 -0
  31. hx_cli-0.1.0/src/hx/providers/__init__.py +1 -0
  32. hx_cli-0.1.0/src/hx/providers/base.py +77 -0
  33. hx_cli-0.1.0/src/hx/providers/fake.py +87 -0
  34. hx_cli-0.1.0/src/hx/providers/models.py +238 -0
  35. hx_cli-0.1.0/src/hx/providers/openrouter.py +468 -0
  36. hx_cli-0.1.0/src/hx/skills/__init__.py +1 -0
  37. hx_cli-0.1.0/src/hx/skills/loader.py +102 -0
  38. hx_cli-0.1.0/src/hx/skills/runtime.py +84 -0
  39. hx_cli-0.1.0/src/hx/tools/__init__.py +1 -0
  40. hx_cli-0.1.0/src/hx/tools/base.py +97 -0
  41. hx_cli-0.1.0/src/hx/tools/bash.py +544 -0
  42. hx_cli-0.1.0/src/hx/tools/edit.py +167 -0
  43. hx_cli-0.1.0/src/hx/tools/glob.py +75 -0
  44. hx_cli-0.1.0/src/hx/tools/grep.py +165 -0
  45. hx_cli-0.1.0/src/hx/tools/output.py +133 -0
  46. hx_cli-0.1.0/src/hx/tools/read.py +142 -0
  47. hx_cli-0.1.0/src/hx/tools/registry.py +149 -0
  48. hx_cli-0.1.0/src/hx/tools/task.py +76 -0
  49. hx_cli-0.1.0/src/hx/tools/todo.py +149 -0
  50. hx_cli-0.1.0/src/hx/tools/write.py +87 -0
  51. hx_cli-0.1.0/src/hx/tui/__init__.py +1 -0
  52. hx_cli-0.1.0/src/hx/tui/app.py +487 -0
  53. hx_cli-0.1.0/src/hx/tui/commands.py +399 -0
  54. hx_cli-0.1.0/src/hx/tui/hx.tcss +197 -0
  55. hx_cli-0.1.0/src/hx/tui/renderers.py +570 -0
  56. hx_cli-0.1.0/src/hx/tui/theme.py +322 -0
  57. hx_cli-0.1.0/src/hx/tui/widgets/__init__.py +1 -0
  58. hx_cli-0.1.0/src/hx/tui/widgets/configure.py +95 -0
  59. hx_cli-0.1.0/src/hx/tui/widgets/diff.py +25 -0
  60. hx_cli-0.1.0/src/hx/tui/widgets/input.py +145 -0
  61. hx_cli-0.1.0/src/hx/tui/widgets/palette.py +130 -0
  62. hx_cli-0.1.0/src/hx/tui/widgets/permission.py +97 -0
  63. hx_cli-0.1.0/src/hx/tui/widgets/statusbar.py +212 -0
  64. hx_cli-0.1.0/src/hx/tui/widgets/todos.py +116 -0
  65. hx_cli-0.1.0/src/hx/tui/widgets/transcript.py +316 -0
  66. hx_cli-0.1.0/src/hx/tui/widgets/working.py +78 -0
  67. hx_cli-0.1.0/tests/__init__.py +0 -0
  68. hx_cli-0.1.0/tests/agents/__init__.py +0 -0
  69. hx_cli-0.1.0/tests/agents/test_subagent.py +307 -0
  70. hx_cli-0.1.0/tests/conftest.py +49 -0
  71. hx_cli-0.1.0/tests/core/__init__.py +0 -0
  72. hx_cli-0.1.0/tests/core/test_compaction.py +137 -0
  73. hx_cli-0.1.0/tests/core/test_context.py +78 -0
  74. hx_cli-0.1.0/tests/core/test_lateinject.py +30 -0
  75. hx_cli-0.1.0/tests/core/test_loop.py +259 -0
  76. hx_cli-0.1.0/tests/core/test_usage.py +25 -0
  77. hx_cli-0.1.0/tests/mcp/__init__.py +0 -0
  78. hx_cli-0.1.0/tests/mcp/fixtures/echo_server.py +132 -0
  79. hx_cli-0.1.0/tests/mcp/test_manager.py +186 -0
  80. hx_cli-0.1.0/tests/permissions/__init__.py +0 -0
  81. hx_cli-0.1.0/tests/permissions/test_engine.py +143 -0
  82. hx_cli-0.1.0/tests/permissions/test_parser.py +31 -0
  83. hx_cli-0.1.0/tests/permissions/test_sandbox.py +152 -0
  84. hx_cli-0.1.0/tests/providers/__init__.py +0 -0
  85. hx_cli-0.1.0/tests/providers/test_openrouter.py +146 -0
  86. hx_cli-0.1.0/tests/skills/__init__.py +0 -0
  87. hx_cli-0.1.0/tests/skills/test_loader.py +157 -0
  88. hx_cli-0.1.0/tests/test_cli.py +133 -0
  89. hx_cli-0.1.0/tests/test_integration.py +474 -0
  90. hx_cli-0.1.0/tests/test_live.py +115 -0
  91. hx_cli-0.1.0/tests/test_readme.py +189 -0
  92. hx_cli-0.1.0/tests/tools/__init__.py +0 -0
  93. hx_cli-0.1.0/tests/tools/test_background.py +129 -0
  94. hx_cli-0.1.0/tests/tools/test_bash.py +162 -0
  95. hx_cli-0.1.0/tests/tools/test_files.py +235 -0
  96. hx_cli-0.1.0/tests/tools/test_output.py +55 -0
  97. hx_cli-0.1.0/tests/tools/test_todo.py +104 -0
  98. hx_cli-0.1.0/tests/tui/__init__.py +0 -0
  99. hx_cli-0.1.0/tests/tui/test_app.py +525 -0
  100. hx_cli-0.1.0/tests/tui/test_renderers.py +180 -0
  101. hx_cli-0.1.0/tests/tui/test_statusbar.py +131 -0
  102. hx_cli-0.1.0/tests/tui/test_transcript.py +99 -0
  103. hx_cli-0.1.0/uv.lock +1764 -0
@@ -0,0 +1,63 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*"]
7
+ pull_request:
8
+
9
+ jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v3
15
+ - run: uv sync --extra dev
16
+ - run: uv run ruff check .
17
+ - run: uv run ruff format --check .
18
+ - run: uv run mypy
19
+
20
+ test:
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ os: [ubuntu-latest, macos-latest]
25
+ python: ["3.11", "3.12"]
26
+ runs-on: ${{ matrix.os }}
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: astral-sh/setup-uv@v3
30
+ - run: uv sync --extra dev --python ${{ matrix.python }}
31
+ # `live` needs an API key and real spend, so it never runs in CI.
32
+ - run: uv run pytest -m "not live"
33
+
34
+ install-script:
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+ - run: sudo apt-get update && sudo apt-get install -y shellcheck
39
+ - run: shellcheck install.sh
40
+ - name: install from a clean container
41
+ run: |
42
+ docker run --rm -v "$PWD:/src" debian:bookworm-slim bash -c '
43
+ set -eux
44
+ apt-get update -qq && apt-get install -y -qq curl ca-certificates >/dev/null
45
+ cd /tmp
46
+ HX_PACKAGE=/src sh /src/install.sh
47
+ export PATH="$HOME/.local/bin:$PATH"
48
+ hx --version
49
+ '
50
+
51
+ publish:
52
+ needs: [lint, test, install-script]
53
+ if: startsWith(github.ref, 'refs/tags/v')
54
+ runs-on: ubuntu-latest
55
+ environment: pypi
56
+ permissions:
57
+ id-token: write
58
+ steps:
59
+ - uses: actions/checkout@v4
60
+ - uses: astral-sh/setup-uv@v3
61
+ - run: uv build
62
+ # Trusted publishing: no long-lived token stored in the repo.
63
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+ .DS_Store
14
+ .hx/sessions/
hx_cli-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,430 @@
1
+ Metadata-Version: 2.5
2
+ Name: hx-cli
3
+ Version: 0.1.0
4
+ Summary: HX - a terminal coding agent
5
+ Project-URL: Homepage, https://github.com/aletisunil/hx
6
+ Project-URL: Issues, https://github.com/aletisunil/hx/issues
7
+ Author: Sunil Aleti
8
+ License: MIT
9
+ Keywords: agent,cli,coding-agent,llm,openrouter,tui
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: click>=8.1
16
+ Requires-Dist: httpx>=0.27
17
+ Requires-Dist: pydantic>=2.7
18
+ Requires-Dist: pyyaml>=6.0
19
+ Requires-Dist: rich>=13.7
20
+ Requires-Dist: textual>=0.85
21
+ Provides-Extra: dev
22
+ Requires-Dist: mypy>=1.11; extra == 'dev'
23
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
24
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
25
+ Requires-Dist: pytest>=8.2; extra == 'dev'
26
+ Requires-Dist: ruff>=0.6; extra == 'dev'
27
+ Requires-Dist: textual-dev>=1.5; extra == 'dev'
28
+ Requires-Dist: types-pyyaml; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # HX
32
+
33
+ A terminal coding agent. Python core, Textual TUI, OpenRouter for models.
34
+
35
+ HX runs in your project directory, reads and edits your code, runs commands in
36
+ a sandboxed shell, and shows you what every turn costs.
37
+
38
+ ---
39
+
40
+ ## Install
41
+
42
+ ```sh
43
+ curl -fsSL https://raw.githubusercontent.com/aletisunil/hx/main/install.sh | sh
44
+ ```
45
+
46
+ The script bootstraps [uv](https://docs.astral.sh/uv/) if you don't have it,
47
+ then installs HX as an isolated tool with a pinned Python. Re-running it
48
+ upgrades in place.
49
+
50
+ If you'd rather not pipe a script into a shell:
51
+
52
+ ```sh
53
+ uv tool install hx-cli # or: pipx install hx-cli
54
+ ```
55
+
56
+ ### `hx-cli` on PyPI, `hx` in your terminal
57
+
58
+ The package is published as **`hx-cli`**. The command it installs is **`hx`**,
59
+ and that is what you type — the longer name never appears again after install.
60
+
61
+ The plain name `hx` on PyPI is registered by someone else and has no releases,
62
+ so `uv tool install hx` fails with "no versions of hx". Install `hx-cli`.
63
+
64
+ | | Name |
65
+ |---|---|
66
+ | PyPI package | `hx-cli` |
67
+ | Command | `hx` |
68
+ | Python import | `hx` |
69
+ | Config directory | `~/.hx` |
70
+
71
+ If the command isn't found after installing, add uv's bin directory to your
72
+ `PATH`:
73
+
74
+ ```sh
75
+ export PATH="$(uv tool dir --bin):$PATH"
76
+ ```
77
+
78
+ Requires Python 3.11+. macOS and Linux. Update with `hx upgrade`.
79
+
80
+ ### The API key
81
+
82
+ On first run HX asks for an OpenRouter key and saves it to `~/.hx/auth.json`
83
+ with mode 0600. Get one at <https://openrouter.ai/keys>.
84
+
85
+ To change it later, `/configure` inside the TUI, or from a shell:
86
+
87
+ ```sh
88
+ hx auth # is a key set, and where does it come from?
89
+ hx auth set # paste a new one (hidden input)
90
+ hx auth clear # remove the saved key
91
+ ```
92
+
93
+ Resolution order is `HX_OPENROUTER_API_KEY`, then `OPENROUTER_API_KEY`, then
94
+ the saved file. The environment wins, and both `/configure` and `hx auth` say
95
+ so — otherwise saving a key while a variable is set looks like a no-op.
96
+
97
+ ---
98
+
99
+ ## Running it
100
+
101
+ ```sh
102
+ hx # interactive TUI in the current directory
103
+ hx -p "explain this repo" # headless: streams to stdout, tool activity to stderr
104
+ hx resume # resume the last session here
105
+ hx resume <session-id> # resume a specific one
106
+ hx --model openai/gpt-5 # override the model for one run
107
+ hx --mode plan # start read-only
108
+ hx --cwd ../other-project # run against a different directory
109
+ hx --no-sandbox # disable OS sandboxing (rules still apply)
110
+ hx mcp list|add|remove # manage MCP servers
111
+ hx auth [set|clear] # manage the API key
112
+ hx upgrade # update to the latest release
113
+ ```
114
+
115
+ Print mode is the scriptable one: stdout carries only the assistant's text, so
116
+ it pipes cleanly.
117
+
118
+ ### Keys
119
+
120
+ | Key | Does |
121
+ |---|---|
122
+ | `enter` | send |
123
+ | `ctrl+j` | newline |
124
+ | `esc` | interrupt the current turn |
125
+ | `ctrl+c` | cancel the current turn |
126
+ | `shift+tab` | cycle permission mode |
127
+ | `ctrl+p` | command palette |
128
+ | `ctrl+r` | expand the last tool output |
129
+ | `ctrl+t` | toggle the todo sidebar |
130
+ | `ctrl+d` | quit |
131
+ | `@path` | complete a file path |
132
+ | `!command` | run a shell command directly, no model turn |
133
+
134
+ `!` still goes through the permission engine and the sandbox — it skips the
135
+ model, not the safety layers.
136
+
137
+ ### Commands
138
+
139
+ | Command | Does |
140
+ |---|---|
141
+ | `/model [query]` | pick a model; shows context window, price per Mtok, cache support |
142
+ | `/models refresh` | re-fetch the catalogue |
143
+ | `/configure` | session settings and the API key |
144
+ | `/mode [name]` | `plan`, `default`, `acceptEdits`, `bypass` |
145
+ | `/permissions` | active rules and what is enforcing them |
146
+ | `/context` | what is filling the context window |
147
+ | `/cost` | tokens, cache savings, spend |
148
+ | `/compact [focus]` | summarise older turns now |
149
+ | `/clear` | fresh session, same directory |
150
+ | `/resume` | reopen a previous session |
151
+ | `/todos` | toggle the sidebar |
152
+ | `/skills` | installed skills |
153
+ | `/agents` | subagent types |
154
+ | `/mcp` | server status |
155
+ | `/theme [name]` | `dark`, `light`, `ansi` |
156
+ | `/init` | generate an `HX.md` for the project |
157
+ | `/help` | list commands and keys |
158
+ | `/quit` | exit |
159
+
160
+ ### The status bar
161
+
162
+ Two lines: working directory and permission mode above; token counts, cache
163
+ read/write with hit rate, spend, last-turn latency, context gauge and model
164
+ below. The cache and cost fields are the point of it — a hit rate that
165
+ collapses after an edit is the visible symptom of a broken prefix.
166
+
167
+ ---
168
+
169
+ ## Configuration
170
+
171
+ Settings are JSON, merged lowest to highest:
172
+
173
+ ```
174
+ defaults < ~/.hx/settings.json < ./.hx/settings.json < HX_* env < CLI flags
175
+ ```
176
+
177
+ Permission rule lists are unioned across layers, so a project can add a deny
178
+ rule without discarding yours. Everything else is replaced.
179
+
180
+ ```jsonc
181
+ {
182
+ "theme": "dark", // dark | light | ansi
183
+ "telemetry": false,
184
+
185
+ "models": {
186
+ "model": "anthropic/claude-sonnet-4.5",
187
+ "subagent_model": null, // defaults to "model"
188
+ "max_tokens": 8192,
189
+ "temperature": null
190
+ },
191
+
192
+ "permissions": {
193
+ "mode": "default", // plan | default | acceptEdits | bypass
194
+ "allow": ["Bash(git status:*)"],
195
+ "ask": [],
196
+ "deny": ["Read(**/.env)"],
197
+ "sandbox": true,
198
+ "allow_network": false // outbound network for sandboxed commands
199
+ },
200
+
201
+ "context": {
202
+ "compact_at": 0.80, // fraction of the window that triggers compaction
203
+ "keep_recent_turns": 6, // turns kept verbatim across a compaction
204
+ "tool_output_char_cap": 25000,
205
+ "tool_output_line_cap": 2000
206
+ },
207
+
208
+ "bash": {
209
+ "timeout_seconds": 120,
210
+ "max_timeout_seconds": 600, // ceiling; caps what the model may ask for
211
+ "shell": null // defaults to $SHELL
212
+ }
213
+ }
214
+ ```
215
+
216
+ Environment overrides: `HX_MODEL`, `HX_SUBAGENT_MODEL`, `HX_MAX_TOKENS`,
217
+ `HX_PERMISSION_MODE`, `HX_SANDBOX`, `HX_COMPACT_AT`, `HX_THEME`. Also
218
+ `HX_HOME` to relocate user state.
219
+
220
+ ### Where things live
221
+
222
+ | Path | What |
223
+ |---|---|
224
+ | `~/.hx/settings.json` | your settings |
225
+ | `~/.hx/auth.json` | API key, mode 0600 |
226
+ | `~/.hx/models.json` | cached model catalogue, refreshed daily |
227
+ | `~/.hx/sessions/` | transcripts, spilled tool output, subagent sessions |
228
+ | `~/.hx/skills/`, `~/.hx/agents/` | your skills and agents |
229
+ | `./.hx/settings.json` | project settings, checked in if you like |
230
+ | `./.hx/mcp.json` | project MCP servers |
231
+ | `./.hx/skills/`, `./.hx/agents/` | project skills and agents |
232
+ | `./HX.md` | project instructions, loaded into every session |
233
+
234
+ `HX.md` is the place for things a newcomer would get wrong: how to run the
235
+ tests, conventions, what not to touch. `/init` writes a first draft. It is
236
+ loaded once per session and frozen, so it costs one prefix, not one per turn.
237
+
238
+ ---
239
+
240
+ ## Safety
241
+
242
+ Two independent layers guard every tool call, and both must pass.
243
+
244
+ **Permission rules** are `Tool(specifier)` strings — `Bash(git commit:*)`,
245
+ `Edit(src/**)`, `Read(**/.ssh/**)`. Deny beats ask beats allow, and a deny
246
+ holds even in bypass mode. Shell commands are decomposed into their real
247
+ segments first, so an allow rule for `git status` does not carry
248
+ `&& rm -rf /` along with it; a command that cannot be decomposed with
249
+ confidence prompts rather than passing.
250
+
251
+ **An OS sandbox** wraps command execution: Seatbelt on macOS, bubblewrap on
252
+ Linux. The filesystem is readable, writes are confined to the project and the
253
+ temp dir, credential paths (`~/.ssh`, `~/.aws`, and HX's own `auth.json`) are
254
+ unreadable, and outbound network is off. If neither backend is present the
255
+ status bar says `no-sandbox` rather than implying protection that is not there.
256
+
257
+ Modes cycle with shift+tab: `plan` (read-only — mutating tools are not even
258
+ offered to the model), `default`, `acceptEdits`, `bypass`.
259
+
260
+ ---
261
+
262
+ ## Context engineering
263
+
264
+ Long sessions are the normal case, so the harness is built around keeping the
265
+ provider's KV cache warm and the window from filling up.
266
+
267
+ **A stable prefix.** System prompt, tool schemas and project context are
268
+ assembled in a fixed order and never mutated mid-session. Cache breakpoints sit
269
+ at the end of that static block and at a rolling point before the recent turns,
270
+ which only advances once enough tokens have accumulated behind it.
271
+
272
+ **Late injection** carries everything that changes per turn — the todo list,
273
+ files that changed on disk since HX read them — on the tail of the newest user
274
+ message rather than in the prefix. Stale copies are stripped and regenerated
275
+ each turn, so six todo updates leave one copy in context, not six.
276
+
277
+ **Compaction** fires at 80% of the window, or on `/compact [focus]`. Older
278
+ turns are replaced by a structured summary; the recent turns and the todo list
279
+ survive verbatim, and the boundary snaps to a turn edge so a tool call is never
280
+ severed from its results. Superseded messages are flagged, not deleted, so
281
+ resume replays exactly what happened.
282
+
283
+ **Output capping** keeps the head and tail of a large tool result, spills the
284
+ rest to the session directory, and hands the model that path to grep.
285
+
286
+ ---
287
+
288
+ ## Extending it
289
+
290
+ **Skills** are directories containing `SKILL.md` with YAML frontmatter:
291
+
292
+ ```markdown
293
+ ---
294
+ name: deploy
295
+ description: Tag, build and ship a release
296
+ allowed-tools: Read, Bash # optional; narrows the toolset while active
297
+ ---
298
+
299
+ 1. Run the tests.
300
+ 2. Tag the commit.
301
+ ```
302
+
303
+ Drop them in `.hx/skills/<name>/` or `~/.hx/skills/<name>/`; a project skill
304
+ shadows a user one of the same name. Only the name and description enter the
305
+ context — the body loads when the model calls `Skill(name)`, so a hundred
306
+ installed skills cost a hundred lines, not a hundred documents.
307
+
308
+ **Subagents** run in their own context with their own transcript, tool
309
+ allowlist and model. Only the final report returns to the parent, so a long
310
+ search costs the caller one paragraph instead of every intermediate tool
311
+ result. `explore`, `plan` and `general` ship built in; add your own as
312
+ `.hx/agents/<name>.md`:
313
+
314
+ ```markdown
315
+ ---
316
+ name: reviewer
317
+ description: Reviews a diff against the project's conventions
318
+ tools: Read, Grep
319
+ model: openai/gpt-5 # optional
320
+ ---
321
+
322
+ You review code. Be specific and cite file:line.
323
+ ```
324
+
325
+ A subagent never gets the `Task` tool, so recursion is impossible by
326
+ construction.
327
+
328
+ **MCP servers** go in `.hx/mcp.json`:
329
+
330
+ ```json
331
+ {
332
+ "mcpServers": {
333
+ "local": { "command": "python", "args": ["server.py"] },
334
+ "remote": { "url": "https://example.com/mcp" }
335
+ }
336
+ }
337
+ ```
338
+
339
+ Or `hx mcp add local python server.py`. Tools arrive namespaced
340
+ `mcp__<server>__<tool>` in a deterministic order. Servers connect concurrently
341
+ with a per-server timeout; one that is broken or slow logs a warning and is
342
+ dropped rather than taking the session with it.
343
+
344
+ ---
345
+
346
+ ## Development
347
+
348
+ ```sh
349
+ git clone https://github.com/aletisunil/hx && cd hx
350
+ uv sync --extra dev
351
+
352
+ uv run pytest # the suite; live tests are deselected
353
+ uv run ruff check . && uv run ruff format --check .
354
+ uv run mypy # strict
355
+ uv run hx # run from the checkout
356
+ ```
357
+
358
+ Tests marked `live` hit the real OpenRouter API and cost money:
359
+
360
+ ```sh
361
+ OPENROUTER_API_KEY=... uv run pytest -m live
362
+ ```
363
+
364
+ Tests marked `sandbox` exercise the real OS sandbox and are skipped where no
365
+ backend exists.
366
+
367
+ ### Layout
368
+
369
+ ```
370
+ src/hx/
371
+ cli.py config.py paths.py frontmatter.py
372
+ core/ loop, context assembly, compaction, late injection, sessions, usage
373
+ providers/ OpenRouter, the model catalogue, a scripted provider for tests
374
+ tools/ Bash, Read, Write, Edit, Glob, Grep, TodoWrite, Task, output capping
375
+ permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
376
+ skills/ agents/ mcp/
377
+ tui/ Textual app, commands, theme, per-tool renderers, widgets
378
+ ```
379
+
380
+ The core is headless and emits events; the TUI and print mode are both just
381
+ consumers. Nothing under `core/`, `tools/`, `providers/` or `permissions/`
382
+ imports `tui/`.
383
+
384
+ ---
385
+
386
+ ## Releasing
387
+
388
+ For maintainers. HX publishes to PyPI as **`hx-cli`** from CI, on a tag.
389
+
390
+ One-time setup:
391
+
392
+ 1. Push the repo to `github.com/<owner>/hx` and update the URLs in
393
+ `pyproject.toml`.
394
+ 2. Create a GitHub environment named `pypi`.
395
+ 3. On PyPI, add a [trusted publisher](https://docs.pypi.org/trusted-publishers/)
396
+ for the project: owner, repo `hx`, workflow `ci.yml`, environment `pypi`.
397
+ No API token is stored anywhere.
398
+
399
+ Each release:
400
+
401
+ ```sh
402
+ # bump version in pyproject.toml, commit
403
+ git tag v0.1.0 && git push origin main --tags
404
+ ```
405
+
406
+ The `publish` job runs only on `refs/tags/v*` and only after lint, the test
407
+ matrix, and an install-script run in a clean Debian container have passed. It
408
+ builds with `uv build` and uploads via OIDC.
409
+
410
+ To check a build before tagging:
411
+
412
+ ```sh
413
+ uv build && ls dist/
414
+ ```
415
+
416
+ ---
417
+
418
+ ## Status
419
+
420
+ Feature complete against the original plan: the agent loop, OpenRouter
421
+ streaming with prefix caching and accurate cost accounting, session persistence
422
+ and resume, the tool suite, the permission engine and OS sandbox, late
423
+ injection, compaction, output capping, skills, subagents, MCP, and the TUI.
424
+
425
+ Published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/), released
426
+ from CI on a tag.
427
+
428
+ The one thing still unproven is a live OpenRouter call: the `live` tests exist
429
+ and cover the wire format, tool use and a genuine cache hit, but they need a
430
+ key and are deselected by default.