hx-cli 0.1.4__tar.gz → 0.1.6__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.6/CHANGELOG.md +181 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/PKG-INFO +252 -28
- {hx_cli-0.1.4 → hx_cli-0.1.6}/README.md +250 -27
- {hx_cli-0.1.4 → hx_cli-0.1.6}/pyproject.toml +16 -2
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/__init__.py +1 -1
- hx_cli-0.1.6/src/hx/auth/__init__.py +22 -0
- hx_cli-0.1.6/src/hx/auth/oauth/__init__.py +1 -0
- hx_cli-0.1.6/src/hx/auth/oauth/callback.py +203 -0
- hx_cli-0.1.6/src/hx/auth/oauth/codex.py +326 -0
- hx_cli-0.1.6/src/hx/auth/oauth/pkce.py +42 -0
- hx_cli-0.1.6/src/hx/auth/resolve.py +247 -0
- hx_cli-0.1.6/src/hx/auth/store.py +244 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/cli.py +368 -51
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/config.py +38 -1
- hx_cli-0.1.6/src/hx/core/checkpoints.py +277 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/core/context.py +95 -4
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/core/events.py +6 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/core/lateinject.py +15 -3
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/core/loop.py +68 -8
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/core/session.py +149 -6
- hx_cli-0.1.6/src/hx/core/title.py +105 -0
- hx_cli-0.1.6/src/hx/docs.py +153 -0
- hx_cli-0.1.6/src/hx/git.py +278 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/mcp/client.py +2 -1
- hx_cli-0.1.6/src/hx/net.py +131 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/paths.py +24 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/providers/base.py +5 -0
- hx_cli-0.1.6/src/hx/providers/codex.py +187 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/providers/models.py +106 -20
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/providers/openrouter.py +18 -53
- hx_cli-0.1.6/src/hx/providers/registry.py +153 -0
- hx_cli-0.1.6/src/hx/providers/responses_codec.py +337 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tools/edit.py +11 -2
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tools/read.py +8 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tools/registry.py +16 -3
- hx_cli-0.1.6/src/hx/tools/websearch.py +415 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tools/write.py +13 -2
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/app.py +126 -12
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/commands.py +200 -12
- hx_cli-0.1.6/src/hx/tui/widgets/login.py +148 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/widgets/palette.py +33 -1
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/widgets/statusbar.py +12 -2
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/widgets/transcript.py +55 -8
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/agents/test_subagent.py +2 -0
- hx_cli-0.1.6/tests/auth/test_codex_oauth.py +130 -0
- hx_cli-0.1.6/tests/auth/test_pkce.py +27 -0
- hx_cli-0.1.6/tests/auth/test_resolve.py +166 -0
- hx_cli-0.1.6/tests/auth/test_store.py +160 -0
- hx_cli-0.1.6/tests/core/test_checkpoints.py +200 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/core/test_context.py +68 -1
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/core/test_lateinject.py +27 -5
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/core/test_loop.py +69 -0
- hx_cli-0.1.6/tests/core/test_rewind.py +206 -0
- hx_cli-0.1.6/tests/core/test_session.py +69 -0
- hx_cli-0.1.6/tests/core/test_title.py +70 -0
- hx_cli-0.1.6/tests/providers/test_catalogue.py +102 -0
- hx_cli-0.1.6/tests/providers/test_codex.py +80 -0
- hx_cli-0.1.6/tests/providers/test_registry.py +84 -0
- hx_cli-0.1.6/tests/providers/test_responses_codec.py +272 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/test_cli.py +53 -0
- hx_cli-0.1.6/tests/test_docs.py +203 -0
- hx_cli-0.1.6/tests/test_git.py +258 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/test_integration.py +224 -2
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/test_live.py +2 -1
- hx_cli-0.1.6/tests/test_live_codex.py +136 -0
- hx_cli-0.1.6/tests/test_live_tavily.py +99 -0
- hx_cli-0.1.6/tests/test_net.py +110 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/test_readme.py +37 -5
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tools/test_todo.py +2 -2
- hx_cli-0.1.6/tests/tools/test_websearch.py +296 -0
- hx_cli-0.1.6/tests/tui/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tui/test_app.py +186 -3
- hx_cli-0.1.6/tests/tui/test_login.py +238 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tui/test_transcript.py +28 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/uv.lock +11 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/.github/workflows/ci.yml +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/.gitignore +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/install.sh +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/agents/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/agents/definitions.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/agents/subagent.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/core/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/core/compaction.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/core/messages.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/core/usage.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/frontmatter.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/keys.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/mcp/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/mcp/manager.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/permissions/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/permissions/engine.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/permissions/parser.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/permissions/sandbox.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/providers/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/providers/fake.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/skills/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/skills/loader.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/skills/runtime.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tools/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tools/base.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tools/bash.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tools/glob.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tools/grep.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tools/output.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tools/task.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tools/todo.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/clipboard.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/fuzzy.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/hx.tcss +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/killring.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/renderers.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/roles.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/theme.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/theme_json.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/themes/ansi.json +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/themes/dark.json +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/themes/light.json +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/widgets/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/widgets/autocomplete.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/widgets/configure.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/widgets/diff.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/widgets/frame.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/widgets/header.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/widgets/hints.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/widgets/input.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/widgets/permission.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/widgets/rule.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/widgets/todos.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/src/hx/tui/widgets/working.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/agents/__init__.py +0 -0
- {hx_cli-0.1.4/tests/core → hx_cli-0.1.6/tests/auth}/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/conftest.py +0 -0
- {hx_cli-0.1.4/tests/mcp → hx_cli-0.1.6/tests/core}/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/core/test_compaction.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/core/test_usage.py +0 -0
- {hx_cli-0.1.4/tests/permissions → hx_cli-0.1.6/tests/mcp}/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/mcp/fixtures/echo_server.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/mcp/test_manager.py +0 -0
- {hx_cli-0.1.4/tests/providers → hx_cli-0.1.6/tests/permissions}/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/permissions/test_engine.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/permissions/test_parser.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/permissions/test_sandbox.py +0 -0
- {hx_cli-0.1.4/tests/skills → hx_cli-0.1.6/tests/providers}/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/providers/test_openrouter.py +0 -0
- {hx_cli-0.1.4/tests/tools → hx_cli-0.1.6/tests/skills}/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/skills/test_loader.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/test_install.py +0 -0
- {hx_cli-0.1.4/tests/tui → hx_cli-0.1.6/tests/tools}/__init__.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tools/test_background.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tools/test_bash.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tools/test_files.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tools/test_output.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tui/__snapshots__/test_snapshots/test_a_running_turn.raw +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tui/__snapshots__/test_snapshots/test_a_turn_with_tool_blocks.raw +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tui/__snapshots__/test_snapshots/test_the_completion_popup.raw +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tui/__snapshots__/test_snapshots/test_the_idle_screen.raw +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tui/test_autocomplete.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tui/test_clipboard.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tui/test_frame.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tui/test_keys.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tui/test_renderers.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tui/test_snapshots.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tui/test_statusbar.py +0 -0
- {hx_cli-0.1.4 → hx_cli-0.1.6}/tests/tui/test_theme_json.py +0 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
What shipped in each released version of HX, newest first.
|
|
4
|
+
|
|
5
|
+
This file is the release record and part of the product: it ships inside the
|
|
6
|
+
wheel, `hx changelog` prints it, and a session can answer "what's new?" or
|
|
7
|
+
"which version added web search?" from it rather than from a guess. `hx docs`
|
|
8
|
+
prints the manual next to it.
|
|
9
|
+
|
|
10
|
+
The rules, enforced by `tests/test_docs.py` so a release cannot forget them:
|
|
11
|
+
|
|
12
|
+
- Every released version has a `## [x.y.z] - YYYY-MM-DD` section here, and the
|
|
13
|
+
version in `src/hx/__init__.py` always has one. Bumping the version without
|
|
14
|
+
writing the section fails the suite before the tag exists.
|
|
15
|
+
- Work that has landed but is not tagged lives under `## [Unreleased]`. At
|
|
16
|
+
release time that heading is renamed to the new version and dated, and a
|
|
17
|
+
fresh empty `## [Unreleased]` takes its place.
|
|
18
|
+
- Entries say what a user can now do, in one line each, naming the command,
|
|
19
|
+
setting or key that reaches the feature. A changelog nobody can act on is a
|
|
20
|
+
git log with extra steps.
|
|
21
|
+
|
|
22
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
23
|
+
the project follows [semantic versioning](https://semver.org/).
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## [Unreleased]
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## [0.1.6] - 2026-09-10
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **Web search, over Tavily.** `WebSearch` returns ranked results with a
|
|
36
|
+
snippet each; `WebFetch` reads up to 5 URLs as markdown. Both are registered
|
|
37
|
+
only when a key resolves, so HX without one is unchanged and pays no
|
|
38
|
+
cached-prefix cost for a tool that would always fail. Set the key with
|
|
39
|
+
`hx auth set tavily`, or `TAVILY_API_KEY` / `HX_TAVILY_API_KEY` in the
|
|
40
|
+
environment. Credits spent are reported per call and per session.
|
|
41
|
+
- **Corporate TLS support.** Certificates verify against the operating
|
|
42
|
+
system's trust store, `HX_CA_BUNDLE` (also `SSL_CERT_FILE`,
|
|
43
|
+
`REQUESTS_CA_BUNDLE`) names a CA file, and `HX_SSL_NO_VERIFY=1` turns
|
|
44
|
+
verification off with a warning every session. A TLS failure is now named in
|
|
45
|
+
one line at startup and in `/model` instead of showing up as an empty model
|
|
46
|
+
catalogue with no cause.
|
|
47
|
+
- **Working-tree notices.** The git branch and files that changed on disk
|
|
48
|
+
outside the session are late-injected onto the newest user message, from one
|
|
49
|
+
`git status --porcelain=v2 --branch -z` per provider call, run in a worker
|
|
50
|
+
thread so it never blocks the UI. Off with
|
|
51
|
+
`"context": {"git_notices": false}` or `HX_GIT_NOTICES=0`.
|
|
52
|
+
- **File checkpoints.** Write and Edit park a file's previous bytes in a
|
|
53
|
+
content-addressed store under the session directory, keyed to the position in
|
|
54
|
+
the transcript, so a rewind can undo edits and not just words. A file whose
|
|
55
|
+
bytes moved since HX wrote them is reported and left alone rather than
|
|
56
|
+
overwritten.
|
|
57
|
+
- **`hx docs` and `hx changelog`.** The manual and this file, printed from the
|
|
58
|
+
installed package. `hx docs` with no argument lists the sections;
|
|
59
|
+
`hx docs credentials` prints one. `hx changelog 0.1.5` prints one release.
|
|
60
|
+
The system prompt points at both, so questions about HX itself are answered
|
|
61
|
+
from the shipped documentation.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- The model catalogue keeps the last good copy when a refresh fails, and
|
|
66
|
+
records why it failed so `/model` can say so. The static routes (the Codex
|
|
67
|
+
models) are registered even when there is no cache file and no network.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## [0.1.5] - 2026-09-09
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
75
|
+
- **A ChatGPT Plus/Pro route.** Sign in with `hx auth login openai-codex` (or
|
|
76
|
+
`/login` in the TUI) and run `openai-codex/gpt-5.3-codex` against your
|
|
77
|
+
subscription instead of paying per token. OAuth with PKCE against
|
|
78
|
+
`auth.openai.com`, a loopback listener on port 1455, a paste-the-URL fallback
|
|
79
|
+
for SSH and `HX_LOGIN_DEVICE_CODE=1` for the device-code flow; access tokens
|
|
80
|
+
refresh themselves.
|
|
81
|
+
- Credentials became one store per route in `~/.hx/auth.json` (mode 0600).
|
|
82
|
+
`hx auth` reports which routes have a credential and where it came from, and
|
|
83
|
+
`hx auth logout <provider>` forgets one.
|
|
84
|
+
- `/model` offers only routes you are actually signed in to, so the picker
|
|
85
|
+
cannot select a model the session has no way to reach.
|
|
86
|
+
|
|
87
|
+
The route serving a turn is decided by the model id alone: `openai-codex/*`
|
|
88
|
+
goes to the subscription, everything else to OpenRouter.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## [0.1.4] - 2026-09-09
|
|
93
|
+
|
|
94
|
+
### Fixed
|
|
95
|
+
|
|
96
|
+
- TUI snapshot tests no longer embed the release version, so bumping the
|
|
97
|
+
version stopped failing the suite.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## [0.1.3] - 2026-09-09
|
|
102
|
+
|
|
103
|
+
### Fixed
|
|
104
|
+
|
|
105
|
+
- An exact slash command runs on the first `enter` instead of only selecting
|
|
106
|
+
itself in the completion list.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## [0.1.2] - 2026-09-09
|
|
111
|
+
|
|
112
|
+
### Added
|
|
113
|
+
|
|
114
|
+
- **Themes as data.** A theme is a JSON file - a `vars` block of colours and a
|
|
115
|
+
`colors` block mapping semantic roles onto them. Drop one in
|
|
116
|
+
`~/.hx/themes/mine.json` and `/theme mine` picks it up, no restart. Syntax
|
|
117
|
+
highlighting is theme-driven, and pi's theme files load unchanged.
|
|
118
|
+
- **One keybinding registry.** Every key is defined once in `src/hx/keys.py`;
|
|
119
|
+
`/help`, the hints line and the README table all read from it, and any action
|
|
120
|
+
id can be rebound in `~/.hx/keybindings.json`. Conflicts and unknown ids are
|
|
121
|
+
reported at startup rather than silently resolved.
|
|
122
|
+
- **A completion popup** above the prompt for `/` commands and `@` paths, with
|
|
123
|
+
fuzzy matching; `tab` cycles, `enter` accepts, `esc` dismisses.
|
|
124
|
+
- **A readline-style prompt** with a kill ring (`ctrl+w`, `ctrl+u`, `ctrl+k`,
|
|
125
|
+
`ctrl+y`, `alt+y`), undo and redo.
|
|
126
|
+
- **System clipboard support.** `ctrl+x` copies the selected message and
|
|
127
|
+
`/copy` the last reply, through `pbcopy`/`wl-copy`/`xclip`/`xsel` with an
|
|
128
|
+
OSC 52 fallback that also works over SSH.
|
|
129
|
+
- The prompt is framed and the working status lives in its border.
|
|
130
|
+
- `/exit` as an alias for `/quit`.
|
|
131
|
+
|
|
132
|
+
### Fixed
|
|
133
|
+
|
|
134
|
+
- Permission grants stay inside what the user actually approved.
|
|
135
|
+
- A `/model` choice persists to user settings.
|
|
136
|
+
- Slash commands run in a worker, so a slow one no longer blocks the UI.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## [0.1.0] - 2026-09-08
|
|
141
|
+
|
|
142
|
+
First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/).
|
|
143
|
+
|
|
144
|
+
### Added
|
|
145
|
+
|
|
146
|
+
- **The agent loop**, streaming from OpenRouter with prefix caching, accurate
|
|
147
|
+
per-turn cost accounting and a stable request prefix.
|
|
148
|
+
- **The tool suite**: Bash (with a background job runner), Read, Write, Edit,
|
|
149
|
+
Glob, Grep, TodoWrite and Task, with output capping that keeps the head and
|
|
150
|
+
tail of a large result and spills the rest to the session directory.
|
|
151
|
+
- **The permission engine and OS sandbox.** `Tool(specifier)` rules where deny
|
|
152
|
+
beats ask beats allow, shell commands decomposed into their real segments
|
|
153
|
+
first, and Seatbelt on macOS / bubblewrap on Linux confining writes and
|
|
154
|
+
network. Modes cycle with `shift+tab`: `plan`, `default`, `acceptEdits`,
|
|
155
|
+
`bypass`.
|
|
156
|
+
- **Context engineering**: late injection for per-turn state, compaction at 80%
|
|
157
|
+
of the window (or `/compact [focus]`), and cache breakpoints that only
|
|
158
|
+
advance once enough tokens have accumulated.
|
|
159
|
+
- **Sessions**: persisted transcripts, `hx resume` / `/resume`, and a model-named
|
|
160
|
+
session title you can change with `/title`.
|
|
161
|
+
- **Skills, subagents and MCP.** Skills are `SKILL.md` directories under
|
|
162
|
+
`.hx/skills/` or `~/.hx/skills/`, loaded on demand so a hundred installed
|
|
163
|
+
skills cost a hundred lines. Subagents (`explore`, `plan`, `general`, plus
|
|
164
|
+
your own in `.hx/agents/`) run in their own context and return one report.
|
|
165
|
+
MCP servers go in `.hx/mcp.json` or `hx mcp add`.
|
|
166
|
+
- **The TUI**: transcript, status bar with token, cache and cost counters, todo
|
|
167
|
+
sidebar, model picker, command palette and per-tool renderers.
|
|
168
|
+
- **Print mode** (`hx -p`), where stdout carries only the assistant's text, so
|
|
169
|
+
it pipes.
|
|
170
|
+
- **Configuration** merged from defaults, `~/.hx/settings.json`,
|
|
171
|
+
`./.hx/settings.json`, `HX_*` environment variables and CLI flags, with
|
|
172
|
+
permission lists and prompt appends unioned rather than replaced.
|
|
173
|
+
- `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
|
|
174
|
+
self-update, and `install.sh` bootstrapping uv with a pinned Python.
|
|
175
|
+
|
|
176
|
+
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.1.5...HEAD
|
|
177
|
+
[0.1.5]: https://github.com/aletisunil/hx/compare/v0.1.4...v0.1.5
|
|
178
|
+
[0.1.4]: https://github.com/aletisunil/hx/compare/v0.1.3...v0.1.4
|
|
179
|
+
[0.1.3]: https://github.com/aletisunil/hx/compare/v0.1.2...v0.1.3
|
|
180
|
+
[0.1.2]: https://github.com/aletisunil/hx/compare/v0.1.0...v0.1.2
|
|
181
|
+
[0.1.0]: https://github.com/aletisunil/hx/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hx-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: HX - a terminal coding agent
|
|
5
5
|
Project-URL: Homepage, https://github.com/aletisunil/hx
|
|
6
6
|
Project-URL: Issues, https://github.com/aletisunil/hx/issues
|
|
@@ -18,6 +18,7 @@ Requires-Dist: pydantic>=2.7
|
|
|
18
18
|
Requires-Dist: pyyaml>=6.0
|
|
19
19
|
Requires-Dist: rich>=13.7
|
|
20
20
|
Requires-Dist: textual>=0.85
|
|
21
|
+
Requires-Dist: truststore>=0.10
|
|
21
22
|
Provides-Extra: dev
|
|
22
23
|
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
23
24
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
@@ -79,16 +80,24 @@ export PATH="$(uv tool dir --bin):$PATH"
|
|
|
79
80
|
|
|
80
81
|
Requires Python 3.11+. macOS and Linux. Update with `hx upgrade`.
|
|
81
82
|
|
|
82
|
-
###
|
|
83
|
+
### Credentials
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
HX reaches models over two routes. Which one serves a turn is decided by the
|
|
86
|
+
model id alone, so "what paid for that" is always answerable by reading it:
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
| Model id | Route | Billing |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `anthropic/claude-sonnet-4.5`, `openai/gpt-5`, … | OpenRouter | per token, API key |
|
|
91
|
+
| `openai-codex/gpt-5.3-codex` | ChatGPT Plus/Pro | your subscription |
|
|
92
|
+
|
|
93
|
+
Credentials live in `~/.hx/auth.json`, mode 0600, one entry per route.
|
|
94
|
+
|
|
95
|
+
**OpenRouter.** On first run HX asks for a key. Get one at
|
|
96
|
+
<https://openrouter.ai/keys>. Change it later with `/configure` in the TUI, or:
|
|
88
97
|
|
|
89
98
|
```sh
|
|
90
|
-
hx auth #
|
|
91
|
-
hx auth set # paste a new
|
|
99
|
+
hx auth # which routes have a credential, and where from
|
|
100
|
+
hx auth set # paste a new OpenRouter key (hidden input)
|
|
92
101
|
hx auth clear # remove the saved key
|
|
93
102
|
```
|
|
94
103
|
|
|
@@ -96,6 +105,89 @@ Resolution order is `HX_OPENROUTER_API_KEY`, then `OPENROUTER_API_KEY`, then
|
|
|
96
105
|
the saved file. The environment wins, and both `/configure` and `hx auth` say
|
|
97
106
|
so — otherwise saving a key while a variable is set looks like a no-op.
|
|
98
107
|
|
|
108
|
+
**ChatGPT Plus/Pro.** Sign in over OAuth and run Codex models against your
|
|
109
|
+
subscription instead of paying per token
|
|
110
|
+
([OpenAI endorses this for OSS harnesses](https://developers.openai.com/community/codex-for-oss)):
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
hx auth login openai-codex # or /login inside the TUI
|
|
114
|
+
hx --model openai-codex/gpt-5.3-codex
|
|
115
|
+
hx auth logout openai-codex
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The browser opens to `auth.openai.com` and redirects to a loopback listener on
|
|
119
|
+
port 1455. Over SSH that redirect cannot reach your machine, so paste the final
|
|
120
|
+
URL into the prompt instead — or set `HX_LOGIN_DEVICE_CODE=1` for the
|
|
121
|
+
device-code flow. Access tokens are refreshed automatically; `/model` only
|
|
122
|
+
offers routes you are actually signed in to.
|
|
123
|
+
|
|
124
|
+
**Web search.** Optional, and the one credential that is not a model route.
|
|
125
|
+
It powers two tools:
|
|
126
|
+
|
|
127
|
+
| Tool | Does |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `WebSearch` | search the web, returning ranked results with a snippet each |
|
|
130
|
+
| `WebFetch` | read up to 5 URLs, returned as markdown |
|
|
131
|
+
|
|
132
|
+
[Tavily](https://app.tavily.com) bills per search rather than per token, so it
|
|
133
|
+
works identically on both routes — including the ChatGPT subscription one,
|
|
134
|
+
where a provider-native search tool is not on offer.
|
|
135
|
+
|
|
136
|
+
Get a key at <https://app.tavily.com> — the free tier is 1000 searches a month
|
|
137
|
+
and takes no card — then save it:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
hx auth set tavily # paste the key, hidden input, saved to ~/.hx/auth.json
|
|
141
|
+
hx auth # confirm: `tavily key tvly-a…f3d9 …/auth.json`
|
|
142
|
+
hx auth clear tavily # remove it again
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Or keep it in the environment instead, which wins only when nothing is saved:
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
export TAVILY_API_KEY=tvly-... # HX_TAVILY_API_KEY is read first
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Without a key, HX works exactly as before** — `WebSearch` and `WebFetch` are
|
|
152
|
+
simply not registered, and `hx auth` says so:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
tavily not configured WebSearch and WebFetch are off
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
There is no nag, no failing tool and no startup warning. Leaving them out is
|
|
159
|
+
deliberate: an advertised tool that always fails still costs cached-prefix
|
|
160
|
+
tokens every turn, and a model told "no key" just calls it again. Adding a key
|
|
161
|
+
turns both on from the next `hx`; no other setting changes.
|
|
162
|
+
|
|
163
|
+
Credits are reported per call and per session at the foot of every result —
|
|
164
|
+
`[Tavily: 1 credit, 4 this session]`. A search costs 1 credit, `search_depth:
|
|
165
|
+
"advanced"` costs 2, and fetching costs 1 per 5 URLs. Tavily's docs describe a
|
|
166
|
+
`usage` field for this, but the live API does not send one, so HX prices every
|
|
167
|
+
call from those published rates and marks the figure `~1` rather than passing
|
|
168
|
+
an estimate off as measured. Check `app.tavily.com` for the authoritative
|
|
169
|
+
number.
|
|
170
|
+
|
|
171
|
+
Credits are *not* in the status bar's cost, which prices tokens: `/cost`
|
|
172
|
+
under-reports a session that searched.
|
|
173
|
+
|
|
174
|
+
**Behind a TLS-inspecting proxy.** HX verifies certificates against the
|
|
175
|
+
operating system's trust store, so a corporate CA the machine already trusts
|
|
176
|
+
needs no setup. When the CA lives only in a file, name it:
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
export HX_CA_BUNDLE=/path/to/corp-ca.pem # SSL_CERT_FILE and REQUESTS_CA_BUNDLE also work
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`HX_SSL_NO_VERIFY=1` turns verification off entirely; it prints a warning at
|
|
183
|
+
startup every session, because it is not a fix. A TLS failure is reported in
|
|
184
|
+
one line - `/model` and the startup notice both name the certificate problem
|
|
185
|
+
rather than reporting an empty catalogue with no cause.
|
|
186
|
+
|
|
187
|
+
There is no Claude Pro/Max route. Anthropic rejects OAuth tokens unless the
|
|
188
|
+
request impersonates Claude Code, which HX will not do. Claude models stay
|
|
189
|
+
available through OpenRouter.
|
|
190
|
+
|
|
99
191
|
---
|
|
100
192
|
|
|
101
193
|
## Running it
|
|
@@ -105,18 +197,54 @@ hx # interactive TUI in the current directory
|
|
|
105
197
|
hx -p "explain this repo" # headless: streams to stdout, tool activity to stderr
|
|
106
198
|
hx resume # resume the last session here
|
|
107
199
|
hx resume <session-id> # resume a specific one
|
|
200
|
+
hx prompt # print the system prompt this directory would use
|
|
108
201
|
hx --model openai/gpt-5 # override the model for one run
|
|
109
202
|
hx --mode plan # start read-only
|
|
110
203
|
hx --cwd ../other-project # run against a different directory
|
|
111
204
|
hx --no-sandbox # disable OS sandboxing (rules still apply)
|
|
205
|
+
hx --system-prompt @p.md # replace the system prompt for one run
|
|
206
|
+
hx --append-system-prompt "Always run the tests" # add to it; repeatable
|
|
112
207
|
hx mcp list|add|remove # manage MCP servers
|
|
113
|
-
hx auth [set|clear] # manage the
|
|
208
|
+
hx auth [set|clear] # manage the OpenRouter key
|
|
209
|
+
hx auth login [provider] # sign in (openrouter, openai-codex)
|
|
210
|
+
hx auth logout <provider> # forget a stored credential
|
|
211
|
+
hx docs [section] # the manual; no argument lists its sections
|
|
212
|
+
hx changelog [version] # what shipped in each version
|
|
114
213
|
hx upgrade # update to the latest release
|
|
115
214
|
```
|
|
116
215
|
|
|
117
216
|
Print mode is the scriptable one: stdout carries only the assistant's text, so
|
|
118
217
|
it pipes cleanly.
|
|
119
218
|
|
|
219
|
+
### The docs ship with the binary
|
|
220
|
+
|
|
221
|
+
`README.md` and [`CHANGELOG.md`](CHANGELOG.md) are included in the wheel, so
|
|
222
|
+
the installed tool can print the manual and the release record for the version
|
|
223
|
+
you are actually running:
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
hx docs # the section titles
|
|
227
|
+
hx docs credentials # one section, printed whole
|
|
228
|
+
hx docs --all # the entire manual
|
|
229
|
+
hx changelog # every release, newest first
|
|
230
|
+
hx changelog 0.1.5 # one release
|
|
231
|
+
hx changelog unreleased # what has landed since the last tag
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The system prompt points a session at these two commands, so "how do I set the
|
|
235
|
+
API key?" or "what changed in the last release?" is answered from the shipped
|
|
236
|
+
documentation rather than from the model's recollection of some other version.
|
|
237
|
+
The call still goes through the permission engine like any other command;
|
|
238
|
+
allowlist it if you would rather not be asked:
|
|
239
|
+
|
|
240
|
+
```jsonc
|
|
241
|
+
{ "permissions": { "allow": ["Bash(hx docs:*)", "Bash(hx changelog:*)"] } }
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`CHANGELOG.md` lists what each version added, and a release cannot skip it:
|
|
245
|
+
`tests/test_docs.py` fails when `__version__` has no section (see
|
|
246
|
+
[Releasing](#releasing)).
|
|
247
|
+
|
|
120
248
|
### Keys
|
|
121
249
|
|
|
122
250
|
Every key below is defined once, in `src/hx/keys.py`. `/help`, the hints line
|
|
@@ -192,13 +320,18 @@ than being silently resolved.
|
|
|
192
320
|
| `/model [query]` | pick a model; shows context window, price per Mtok, cache support |
|
|
193
321
|
| `/models refresh` | re-fetch the catalogue |
|
|
194
322
|
| `/configure` | session settings and the API key |
|
|
323
|
+
| `/login [provider]` | sign in to a model route |
|
|
324
|
+
| `/logout <provider>` | forget a stored credential |
|
|
195
325
|
| `/mode [name]` | `plan`, `default`, `acceptEdits`, `bypass` |
|
|
196
326
|
| `/permissions` | active rules and what is enforcing them |
|
|
197
327
|
| `/context` | what is filling the context window |
|
|
198
328
|
| `/cost` | tokens, cache savings, spend |
|
|
199
329
|
| `/compact [focus]` | summarise older turns now |
|
|
200
330
|
| `/clear` | fresh session, same directory |
|
|
201
|
-
| `/resume` | reopen a previous session |
|
|
331
|
+
| `/resume` | reopen a previous session, listed by name |
|
|
332
|
+
| `/rewind` | go back to an earlier prompt, restoring the files HX changed |
|
|
333
|
+
| `/title [text]` | show or set this session's name |
|
|
334
|
+
| `/prompt` | the system prompt this session is running with |
|
|
202
335
|
| `/todos` | toggle the sidebar |
|
|
203
336
|
| `/skills` | installed skills |
|
|
204
337
|
| `/agents` | subagent types |
|
|
@@ -226,8 +359,9 @@ Settings are JSON, merged lowest to highest:
|
|
|
226
359
|
defaults < ~/.hx/settings.json < ./.hx/settings.json < HX_* env < CLI flags
|
|
227
360
|
```
|
|
228
361
|
|
|
229
|
-
Permission rule lists are unioned across layers, so a
|
|
230
|
-
rule
|
|
362
|
+
Permission rule lists and `prompt.append` are unioned across layers, so a
|
|
363
|
+
project can add a deny rule — or a line to the system prompt — without
|
|
364
|
+
discarding yours. Everything else is replaced.
|
|
231
365
|
|
|
232
366
|
```jsonc
|
|
233
367
|
{
|
|
@@ -238,6 +372,7 @@ rule without discarding yours. Everything else is replaced.
|
|
|
238
372
|
"models": {
|
|
239
373
|
"model": "anthropic/claude-sonnet-4.5",
|
|
240
374
|
"subagent_model": null, // defaults to "model"
|
|
375
|
+
"title_model": null, // model that names sessions; defaults to "model"
|
|
241
376
|
"max_tokens": 8192,
|
|
242
377
|
"temperature": null
|
|
243
378
|
},
|
|
@@ -255,20 +390,64 @@ rule without discarding yours. Everything else is replaced.
|
|
|
255
390
|
"compact_at": 0.80, // fraction of the window that triggers compaction
|
|
256
391
|
"keep_recent_turns": 6, // turns kept verbatim across a compaction
|
|
257
392
|
"tool_output_char_cap": 25000,
|
|
258
|
-
"tool_output_line_cap": 2000
|
|
393
|
+
"tool_output_line_cap": 2000,
|
|
394
|
+
"git_notices": true // late-inject the branch and outside file changes
|
|
259
395
|
},
|
|
260
396
|
|
|
261
397
|
"bash": {
|
|
262
398
|
"timeout_seconds": 120,
|
|
263
399
|
"max_timeout_seconds": 600, // ceiling; caps what the model may ask for
|
|
264
400
|
"shell": null // defaults to $SHELL
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
"prompt": {
|
|
404
|
+
"system": null, // replaces the built-in system prompt
|
|
405
|
+
"append": [] // added after it; unioned across layers
|
|
265
406
|
}
|
|
266
407
|
}
|
|
267
408
|
```
|
|
268
409
|
|
|
269
410
|
Environment overrides: `HX_MODEL`, `HX_SUBAGENT_MODEL`, `HX_MAX_TOKENS`,
|
|
270
|
-
`HX_PERMISSION_MODE`, `HX_SANDBOX`, `HX_COMPACT_AT`, `
|
|
271
|
-
`HX_QUIET_STARTUP`. Also `HX_HOME` to relocate user state
|
|
411
|
+
`HX_PERMISSION_MODE`, `HX_SANDBOX`, `HX_COMPACT_AT`, `HX_GIT_NOTICES`,
|
|
412
|
+
`HX_THEME`, `HX_QUIET_STARTUP`. Also `HX_HOME` to relocate user state, `HX_TAVILY_API_KEY`
|
|
413
|
+
for web search, and `HX_CA_BUNDLE` / `HX_SSL_NO_VERIFY` for TLS (see
|
|
414
|
+
[Credentials](#credentials)).
|
|
415
|
+
|
|
416
|
+
### The system prompt
|
|
417
|
+
|
|
418
|
+
`hx prompt` prints the prompt this directory resolves to, with its source on
|
|
419
|
+
stderr so stdout stays pipeable. `/prompt` shows the same thing inside a running
|
|
420
|
+
session.
|
|
421
|
+
|
|
422
|
+
The built-in prompt is `SYSTEM_PROMPT` in `src/hx/core/context.py`. It is
|
|
423
|
+
replaced by the first of these that exists:
|
|
424
|
+
|
|
425
|
+
| Source | Scope |
|
|
426
|
+
|---|---|
|
|
427
|
+
| `--system-prompt TEXT` or `@path`, or `prompt.system` | one run |
|
|
428
|
+
| `./.hx/system-prompt.md` | this project |
|
|
429
|
+
| `~/.hx/system-prompt.md` | you, everywhere |
|
|
430
|
+
| the built-in prompt | fallback |
|
|
431
|
+
|
|
432
|
+
Appended text is added after whichever prompt won, in this order:
|
|
433
|
+
`~/.hx/system-prompt-append.md`, `./.hx/system-prompt-append.md`, then each
|
|
434
|
+
`--append-system-prompt` value (and `prompt.append`) in the order given. Nothing
|
|
435
|
+
appended is ever discarded by a later layer.
|
|
436
|
+
|
|
437
|
+
This is a different lever from `HX.md`: `HX.md` describes *the project* and is
|
|
438
|
+
injected as its own context section, while these files change *the agent's
|
|
439
|
+
instructions*. Both are read once at startup and frozen, because they sit above
|
|
440
|
+
the first cache breakpoint — an override edited mid-session applies on the next
|
|
441
|
+
run, and `/prompt` says so when it spots one.
|
|
442
|
+
|
|
443
|
+
### Sessions are named
|
|
444
|
+
|
|
445
|
+
After the first exchange, HX asks the model for a short name for the session and
|
|
446
|
+
writes it to `~/.hx/sessions/<id>/meta.json`, so `/resume` lists work rather
|
|
447
|
+
than timestamps. It is one small call — cap 32 output tokens, `models.title_model`
|
|
448
|
+
if you want a cheaper model for it — and it is counted in `/cost` like any other.
|
|
449
|
+
If the call fails the session is still named, from your first message. `/title
|
|
450
|
+
<text>` renames it.
|
|
272
451
|
|
|
273
452
|
### Themes
|
|
274
453
|
|
|
@@ -298,14 +477,17 @@ files load here unchanged. `src/hx/tui/themes/dark.json` is the reference.
|
|
|
298
477
|
| Path | What |
|
|
299
478
|
|---|---|
|
|
300
479
|
| `~/.hx/settings.json` | your settings |
|
|
301
|
-
| `~/.hx/auth.json` | API
|
|
480
|
+
| `~/.hx/auth.json` | API keys and logins, mode 0600 |
|
|
302
481
|
| `~/.hx/models.json` | cached model catalogue, refreshed daily |
|
|
303
482
|
| `~/.hx/themes/` | your themes, one JSON file each |
|
|
304
483
|
| `~/.hx/keybindings.json` | your key overrides |
|
|
484
|
+
| `~/.hx/system-prompt.md` | your system prompt, replacing the built-in one |
|
|
485
|
+
| `~/.hx/system-prompt-append.md` | text appended to whichever prompt is in force |
|
|
305
486
|
| `~/.hx/sessions/` | transcripts, spilled tool output, subagent sessions |
|
|
306
487
|
| `~/.hx/skills/`, `~/.hx/agents/` | your skills and agents |
|
|
307
488
|
| `./.hx/settings.json` | project settings, checked in if you like |
|
|
308
489
|
| `./.hx/mcp.json` | project MCP servers |
|
|
490
|
+
| `./.hx/system-prompt.md`, `./.hx/system-prompt-append.md` | project prompt overrides |
|
|
309
491
|
| `./.hx/skills/`, `./.hx/agents/` | project skills and agents |
|
|
310
492
|
| `./HX.md` | project instructions, loaded into every session |
|
|
311
493
|
|
|
@@ -347,10 +529,31 @@ assembled in a fixed order and never mutated mid-session. Cache breakpoints sit
|
|
|
347
529
|
at the end of that static block and at a rolling point before the recent turns,
|
|
348
530
|
which only advances once enough tokens have accumulated behind it.
|
|
349
531
|
|
|
350
|
-
**Late injection** carries everything that changes per turn
|
|
351
|
-
files that changed on disk since HX read them
|
|
352
|
-
|
|
353
|
-
|
|
532
|
+
**Late injection** carries everything that changes per turn - the todo list,
|
|
533
|
+
files that changed on disk since HX read them, the git branch and any file the
|
|
534
|
+
working tree gained or lost outside the session - on the tail of the newest
|
|
535
|
+
user message rather than in the prefix. Stale copies are stripped and
|
|
536
|
+
regenerated each turn, so six todo updates leave one copy in context, not six.
|
|
537
|
+
|
|
538
|
+
**Working-tree notices** come from one `git status --porcelain=v2 --branch -z`
|
|
539
|
+
per provider call, run without a shell and bounded by a two-second timeout. It
|
|
540
|
+
runs in a worker thread, as every injector does: a turn makes one of these per
|
|
541
|
+
tool round-trip, and on the event loop a slow repository would freeze the UI
|
|
542
|
+
each time. A file HX has already read is left to the stale-file notice, so no
|
|
543
|
+
path is reported twice; outside a repository the watcher disables itself for
|
|
544
|
+
the session. Turn it off with `"context": {"git_notices": false}`.
|
|
545
|
+
|
|
546
|
+
**Rewind** (`/rewind`) picks an earlier prompt and takes the session back to
|
|
547
|
+
it: the transcript is cut there and every file HX wrote after it is restored
|
|
548
|
+
from a content-addressed snapshot taken before each Write and Edit. A file
|
|
549
|
+
someone else changed in the meantime is reported and left alone, and shell
|
|
550
|
+
commands are outside the scheme - the report says so rather than implying the
|
|
551
|
+
tree was fully returned. The prompt that was cut comes back in the input box,
|
|
552
|
+
because a rewind is usually the first half of saying it differently. Nothing is
|
|
553
|
+
deleted from the session file: the rewind is one more append, and replaying the
|
|
554
|
+
file reproduces the state - which is also how an undone compaction gives its
|
|
555
|
+
messages back. The cost ledger is deliberately not rewound; those tokens were
|
|
556
|
+
spent.
|
|
354
557
|
|
|
355
558
|
**Compaction** fires at 80% of the window, or on `/compact [focus]`. Older
|
|
356
559
|
turns are replaced by a structured summary; the recent turns and the todo list
|
|
@@ -433,12 +636,20 @@ uv run mypy # strict
|
|
|
433
636
|
uv run hx # run from the checkout
|
|
434
637
|
```
|
|
435
638
|
|
|
436
|
-
Tests marked `live` hit
|
|
639
|
+
Tests marked `live` hit a real API. The OpenRouter ones cost money:
|
|
437
640
|
|
|
438
641
|
```sh
|
|
439
|
-
OPENROUTER_API_KEY=... uv run pytest -m live
|
|
642
|
+
OPENROUTER_API_KEY=... uv run pytest -m live tests/test_live.py
|
|
643
|
+
|
|
644
|
+
# the Codex route draws on your ChatGPT subscription instead
|
|
645
|
+
hx auth login openai-codex
|
|
646
|
+
uv run pytest -m live tests/test_live_codex.py
|
|
440
647
|
```
|
|
441
648
|
|
|
649
|
+
They are the only place the wire formats, streaming, tool use and a genuine
|
|
650
|
+
cache hit are proven against a real service; everything else runs against the
|
|
651
|
+
scripted provider.
|
|
652
|
+
|
|
442
653
|
Tests marked `sandbox` exercise the real OS sandbox and are skipped where no
|
|
443
654
|
backend exists.
|
|
444
655
|
|
|
@@ -453,10 +664,12 @@ uv run pytest tests/tui/test_snapshots.py --snapshot-update
|
|
|
453
664
|
|
|
454
665
|
```
|
|
455
666
|
src/hx/
|
|
456
|
-
cli.py config.py paths.py frontmatter.py
|
|
667
|
+
cli.py config.py paths.py frontmatter.py git.py
|
|
457
668
|
core/ loop, context assembly, compaction, late injection, sessions, usage
|
|
458
|
-
|
|
459
|
-
|
|
669
|
+
auth/ credential store, OAuth flows, per-route resolution
|
|
670
|
+
providers/ OpenRouter, Codex, the model catalogue, a scripted provider for tests
|
|
671
|
+
tools/ Bash, Read, Write, Edit, Glob, Grep, TodoWrite, Task, WebSearch,
|
|
672
|
+
WebFetch, output capping
|
|
460
673
|
permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
|
|
461
674
|
skills/ agents/ mcp/
|
|
462
675
|
keys.py keybinding registry: ids, defaults, descriptions, user overrides
|
|
@@ -486,10 +699,18 @@ One-time setup:
|
|
|
486
699
|
Each release:
|
|
487
700
|
|
|
488
701
|
```sh
|
|
489
|
-
#
|
|
702
|
+
# 1. move CHANGELOG.md's [Unreleased] entries under the new version, dated,
|
|
703
|
+
# and leave a fresh empty [Unreleased] behind
|
|
704
|
+
# 2. bump __version__ in src/hx/__init__.py
|
|
705
|
+
# 3. uv run pytest tests/test_docs.py # the version must have a section
|
|
706
|
+
git commit -am "release: X.Y.Z"
|
|
490
707
|
git tag v$(uv run hx --version | cut -d' ' -f2) && git push origin main --tags
|
|
491
708
|
```
|
|
492
709
|
|
|
710
|
+
Step 1 is not optional and is not a convention: `tests/test_docs.py` fails the
|
|
711
|
+
whole suite when `__version__` has no `CHANGELOG.md` section, so a release that
|
|
712
|
+
skips it cannot get past CI to the `publish` job.
|
|
713
|
+
|
|
493
714
|
The `publish` job runs only on `refs/tags/v*` and only after lint, the test
|
|
494
715
|
matrix, and an install-script run in a clean Debian container have passed. It
|
|
495
716
|
builds with `uv build` and uploads via OIDC.
|
|
@@ -509,9 +730,12 @@ streaming with prefix caching and accurate cost accounting, session persistence
|
|
|
509
730
|
and resume, the tool suite, the permission engine and OS sandbox, late
|
|
510
731
|
injection, compaction, output capping, skills, subagents, MCP, and the TUI.
|
|
511
732
|
|
|
733
|
+
Two routes to a model: an OpenRouter API key, or a ChatGPT Plus/Pro
|
|
734
|
+
subscription over OAuth. The model id decides which.
|
|
735
|
+
|
|
512
736
|
Published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/), released
|
|
513
737
|
from CI on a tag.
|
|
514
738
|
|
|
515
|
-
The one thing still unproven is a live
|
|
516
|
-
and cover
|
|
517
|
-
|
|
739
|
+
The one thing still unproven in CI is a live call on either route: the `live`
|
|
740
|
+
tests exist and cover both wire formats, tool use, reasoning replay and a
|
|
741
|
+
genuine cache hit, but they need a credential and are deselected by default.
|