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.
- hx_cli-0.1.0/.github/workflows/ci.yml +63 -0
- hx_cli-0.1.0/.gitignore +14 -0
- hx_cli-0.1.0/PKG-INFO +430 -0
- hx_cli-0.1.0/README.md +400 -0
- hx_cli-0.1.0/install.sh +62 -0
- hx_cli-0.1.0/pyproject.toml +87 -0
- hx_cli-0.1.0/src/hx/__init__.py +5 -0
- hx_cli-0.1.0/src/hx/agents/__init__.py +1 -0
- hx_cli-0.1.0/src/hx/agents/definitions.py +106 -0
- hx_cli-0.1.0/src/hx/agents/subagent.py +190 -0
- hx_cli-0.1.0/src/hx/cli.py +667 -0
- hx_cli-0.1.0/src/hx/config.py +277 -0
- hx_cli-0.1.0/src/hx/core/__init__.py +1 -0
- hx_cli-0.1.0/src/hx/core/compaction.py +245 -0
- hx_cli-0.1.0/src/hx/core/context.py +271 -0
- hx_cli-0.1.0/src/hx/core/events.py +183 -0
- hx_cli-0.1.0/src/hx/core/lateinject.py +121 -0
- hx_cli-0.1.0/src/hx/core/loop.py +537 -0
- hx_cli-0.1.0/src/hx/core/messages.py +164 -0
- hx_cli-0.1.0/src/hx/core/session.py +208 -0
- hx_cli-0.1.0/src/hx/core/usage.py +129 -0
- hx_cli-0.1.0/src/hx/frontmatter.py +80 -0
- hx_cli-0.1.0/src/hx/mcp/__init__.py +1 -0
- hx_cli-0.1.0/src/hx/mcp/client.py +319 -0
- hx_cli-0.1.0/src/hx/mcp/manager.py +265 -0
- hx_cli-0.1.0/src/hx/paths.py +90 -0
- hx_cli-0.1.0/src/hx/permissions/__init__.py +7 -0
- hx_cli-0.1.0/src/hx/permissions/engine.py +406 -0
- hx_cli-0.1.0/src/hx/permissions/parser.py +306 -0
- hx_cli-0.1.0/src/hx/permissions/sandbox.py +227 -0
- hx_cli-0.1.0/src/hx/providers/__init__.py +1 -0
- hx_cli-0.1.0/src/hx/providers/base.py +77 -0
- hx_cli-0.1.0/src/hx/providers/fake.py +87 -0
- hx_cli-0.1.0/src/hx/providers/models.py +238 -0
- hx_cli-0.1.0/src/hx/providers/openrouter.py +468 -0
- hx_cli-0.1.0/src/hx/skills/__init__.py +1 -0
- hx_cli-0.1.0/src/hx/skills/loader.py +102 -0
- hx_cli-0.1.0/src/hx/skills/runtime.py +84 -0
- hx_cli-0.1.0/src/hx/tools/__init__.py +1 -0
- hx_cli-0.1.0/src/hx/tools/base.py +97 -0
- hx_cli-0.1.0/src/hx/tools/bash.py +544 -0
- hx_cli-0.1.0/src/hx/tools/edit.py +167 -0
- hx_cli-0.1.0/src/hx/tools/glob.py +75 -0
- hx_cli-0.1.0/src/hx/tools/grep.py +165 -0
- hx_cli-0.1.0/src/hx/tools/output.py +133 -0
- hx_cli-0.1.0/src/hx/tools/read.py +142 -0
- hx_cli-0.1.0/src/hx/tools/registry.py +149 -0
- hx_cli-0.1.0/src/hx/tools/task.py +76 -0
- hx_cli-0.1.0/src/hx/tools/todo.py +149 -0
- hx_cli-0.1.0/src/hx/tools/write.py +87 -0
- hx_cli-0.1.0/src/hx/tui/__init__.py +1 -0
- hx_cli-0.1.0/src/hx/tui/app.py +487 -0
- hx_cli-0.1.0/src/hx/tui/commands.py +399 -0
- hx_cli-0.1.0/src/hx/tui/hx.tcss +197 -0
- hx_cli-0.1.0/src/hx/tui/renderers.py +570 -0
- hx_cli-0.1.0/src/hx/tui/theme.py +322 -0
- hx_cli-0.1.0/src/hx/tui/widgets/__init__.py +1 -0
- hx_cli-0.1.0/src/hx/tui/widgets/configure.py +95 -0
- hx_cli-0.1.0/src/hx/tui/widgets/diff.py +25 -0
- hx_cli-0.1.0/src/hx/tui/widgets/input.py +145 -0
- hx_cli-0.1.0/src/hx/tui/widgets/palette.py +130 -0
- hx_cli-0.1.0/src/hx/tui/widgets/permission.py +97 -0
- hx_cli-0.1.0/src/hx/tui/widgets/statusbar.py +212 -0
- hx_cli-0.1.0/src/hx/tui/widgets/todos.py +116 -0
- hx_cli-0.1.0/src/hx/tui/widgets/transcript.py +316 -0
- hx_cli-0.1.0/src/hx/tui/widgets/working.py +78 -0
- hx_cli-0.1.0/tests/__init__.py +0 -0
- hx_cli-0.1.0/tests/agents/__init__.py +0 -0
- hx_cli-0.1.0/tests/agents/test_subagent.py +307 -0
- hx_cli-0.1.0/tests/conftest.py +49 -0
- hx_cli-0.1.0/tests/core/__init__.py +0 -0
- hx_cli-0.1.0/tests/core/test_compaction.py +137 -0
- hx_cli-0.1.0/tests/core/test_context.py +78 -0
- hx_cli-0.1.0/tests/core/test_lateinject.py +30 -0
- hx_cli-0.1.0/tests/core/test_loop.py +259 -0
- hx_cli-0.1.0/tests/core/test_usage.py +25 -0
- hx_cli-0.1.0/tests/mcp/__init__.py +0 -0
- hx_cli-0.1.0/tests/mcp/fixtures/echo_server.py +132 -0
- hx_cli-0.1.0/tests/mcp/test_manager.py +186 -0
- hx_cli-0.1.0/tests/permissions/__init__.py +0 -0
- hx_cli-0.1.0/tests/permissions/test_engine.py +143 -0
- hx_cli-0.1.0/tests/permissions/test_parser.py +31 -0
- hx_cli-0.1.0/tests/permissions/test_sandbox.py +152 -0
- hx_cli-0.1.0/tests/providers/__init__.py +0 -0
- hx_cli-0.1.0/tests/providers/test_openrouter.py +146 -0
- hx_cli-0.1.0/tests/skills/__init__.py +0 -0
- hx_cli-0.1.0/tests/skills/test_loader.py +157 -0
- hx_cli-0.1.0/tests/test_cli.py +133 -0
- hx_cli-0.1.0/tests/test_integration.py +474 -0
- hx_cli-0.1.0/tests/test_live.py +115 -0
- hx_cli-0.1.0/tests/test_readme.py +189 -0
- hx_cli-0.1.0/tests/tools/__init__.py +0 -0
- hx_cli-0.1.0/tests/tools/test_background.py +129 -0
- hx_cli-0.1.0/tests/tools/test_bash.py +162 -0
- hx_cli-0.1.0/tests/tools/test_files.py +235 -0
- hx_cli-0.1.0/tests/tools/test_output.py +55 -0
- hx_cli-0.1.0/tests/tools/test_todo.py +104 -0
- hx_cli-0.1.0/tests/tui/__init__.py +0 -0
- hx_cli-0.1.0/tests/tui/test_app.py +525 -0
- hx_cli-0.1.0/tests/tui/test_renderers.py +180 -0
- hx_cli-0.1.0/tests/tui/test_statusbar.py +131 -0
- hx_cli-0.1.0/tests/tui/test_transcript.py +99 -0
- 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
|
hx_cli-0.1.0/.gitignore
ADDED
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.
|