hx-cli 0.1.2__tar.gz → 0.1.5__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.2 → hx_cli-0.1.5}/PKG-INFO +112 -17
- {hx_cli-0.1.2 → hx_cli-0.1.5}/README.md +111 -16
- hx_cli-0.1.5/install.sh +136 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/__init__.py +1 -1
- hx_cli-0.1.5/src/hx/auth/__init__.py +22 -0
- hx_cli-0.1.5/src/hx/auth/oauth/__init__.py +1 -0
- hx_cli-0.1.5/src/hx/auth/oauth/callback.py +203 -0
- hx_cli-0.1.5/src/hx/auth/oauth/codex.py +325 -0
- hx_cli-0.1.5/src/hx/auth/oauth/pkce.py +42 -0
- hx_cli-0.1.5/src/hx/auth/resolve.py +239 -0
- hx_cli-0.1.5/src/hx/auth/store.py +242 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/cli.py +234 -44
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/config.py +34 -1
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/context.py +83 -4
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/events.py +6 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/loop.py +65 -5
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/session.py +17 -1
- hx_cli-0.1.5/src/hx/core/title.py +105 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/paths.py +18 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/providers/base.py +5 -0
- hx_cli-0.1.5/src/hx/providers/codex.py +186 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/providers/models.py +71 -11
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/providers/openrouter.py +15 -51
- hx_cli-0.1.5/src/hx/providers/registry.py +153 -0
- hx_cli-0.1.5/src/hx/providers/responses_codec.py +337 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/app.py +78 -16
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/commands.py +160 -11
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/input.py +20 -6
- hx_cli-0.1.5/src/hx/tui/widgets/login.py +148 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/statusbar.py +12 -2
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/transcript.py +55 -8
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/agents/test_subagent.py +2 -0
- hx_cli-0.1.5/tests/auth/test_codex_oauth.py +130 -0
- hx_cli-0.1.5/tests/auth/test_pkce.py +27 -0
- hx_cli-0.1.5/tests/auth/test_resolve.py +166 -0
- hx_cli-0.1.5/tests/auth/test_store.py +160 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/core/test_context.py +68 -1
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/core/test_loop.py +69 -0
- hx_cli-0.1.5/tests/core/test_session.py +69 -0
- hx_cli-0.1.5/tests/core/test_title.py +70 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/permissions/test_sandbox.py +0 -26
- hx_cli-0.1.5/tests/providers/test_codex.py +80 -0
- hx_cli-0.1.5/tests/providers/test_registry.py +84 -0
- hx_cli-0.1.5/tests/providers/test_responses_codec.py +272 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/test_cli.py +53 -0
- hx_cli-0.1.5/tests/test_install.py +53 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/test_integration.py +5 -1
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/test_live.py +2 -1
- hx_cli-0.1.5/tests/test_live_codex.py +136 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/test_readme.py +18 -2
- hx_cli-0.1.5/tests/tools/test_output.py +31 -0
- hx_cli-0.1.5/tests/tui/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/__snapshots__/test_snapshots/test_a_running_turn.raw +81 -81
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/__snapshots__/test_snapshots/test_a_turn_with_tool_blocks.raw +83 -83
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/__snapshots__/test_snapshots/test_the_completion_popup.raw +80 -80
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/__snapshots__/test_snapshots/test_the_idle_screen.raw +79 -79
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_app.py +207 -3
- hx_cli-0.1.5/tests/tui/test_login.py +238 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_snapshots.py +18 -1
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_transcript.py +28 -0
- hx_cli-0.1.2/install.sh +0 -62
- hx_cli-0.1.2/tests/tools/test_output.py +0 -55
- {hx_cli-0.1.2 → hx_cli-0.1.5}/.github/workflows/ci.yml +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/.gitignore +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/pyproject.toml +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/agents/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/agents/definitions.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/agents/subagent.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/compaction.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/lateinject.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/messages.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/usage.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/frontmatter.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/keys.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/mcp/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/mcp/client.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/mcp/manager.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/permissions/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/permissions/engine.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/permissions/parser.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/permissions/sandbox.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/providers/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/providers/fake.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/skills/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/skills/loader.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/skills/runtime.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/base.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/bash.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/edit.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/glob.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/grep.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/output.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/read.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/registry.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/task.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/todo.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/write.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/clipboard.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/fuzzy.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/hx.tcss +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/killring.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/renderers.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/roles.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/theme.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/theme_json.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/themes/ansi.json +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/themes/dark.json +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/themes/light.json +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/autocomplete.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/configure.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/diff.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/frame.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/header.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/hints.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/palette.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/permission.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/rule.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/todos.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/working.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/agents/__init__.py +0 -0
- {hx_cli-0.1.2/tests/core → hx_cli-0.1.5/tests/auth}/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/conftest.py +0 -0
- {hx_cli-0.1.2/tests/mcp → hx_cli-0.1.5/tests/core}/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/core/test_compaction.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/core/test_lateinject.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/core/test_usage.py +0 -0
- {hx_cli-0.1.2/tests/permissions → hx_cli-0.1.5/tests/mcp}/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/mcp/fixtures/echo_server.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/mcp/test_manager.py +0 -0
- {hx_cli-0.1.2/tests/providers → hx_cli-0.1.5/tests/permissions}/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/permissions/test_engine.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/permissions/test_parser.py +0 -0
- {hx_cli-0.1.2/tests/skills → hx_cli-0.1.5/tests/providers}/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/providers/test_openrouter.py +0 -0
- {hx_cli-0.1.2/tests/tools → hx_cli-0.1.5/tests/skills}/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/skills/test_loader.py +0 -0
- {hx_cli-0.1.2/tests/tui → hx_cli-0.1.5/tests/tools}/__init__.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tools/test_background.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tools/test_bash.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tools/test_files.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tools/test_todo.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_autocomplete.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_clipboard.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_frame.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_keys.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_renderers.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_statusbar.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_theme_json.py +0 -0
- {hx_cli-0.1.2 → hx_cli-0.1.5}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hx-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
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
|
|
@@ -79,16 +79,24 @@ export PATH="$(uv tool dir --bin):$PATH"
|
|
|
79
79
|
|
|
80
80
|
Requires Python 3.11+. macOS and Linux. Update with `hx upgrade`.
|
|
81
81
|
|
|
82
|
-
###
|
|
82
|
+
### Credentials
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
HX reaches models over two routes. Which one serves a turn is decided by the
|
|
85
|
+
model id alone, so "what paid for that" is always answerable by reading it:
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
| Model id | Route | Billing |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `anthropic/claude-sonnet-4.5`, `openai/gpt-5`, … | OpenRouter | per token, API key |
|
|
90
|
+
| `openai-codex/gpt-5.3-codex` | ChatGPT Plus/Pro | your subscription |
|
|
91
|
+
|
|
92
|
+
Credentials live in `~/.hx/auth.json`, mode 0600, one entry per route.
|
|
93
|
+
|
|
94
|
+
**OpenRouter.** On first run HX asks for a key. Get one at
|
|
95
|
+
<https://openrouter.ai/keys>. Change it later with `/configure` in the TUI, or:
|
|
88
96
|
|
|
89
97
|
```sh
|
|
90
|
-
hx auth #
|
|
91
|
-
hx auth set # paste a new
|
|
98
|
+
hx auth # which routes have a credential, and where from
|
|
99
|
+
hx auth set # paste a new OpenRouter key (hidden input)
|
|
92
100
|
hx auth clear # remove the saved key
|
|
93
101
|
```
|
|
94
102
|
|
|
@@ -96,6 +104,26 @@ Resolution order is `HX_OPENROUTER_API_KEY`, then `OPENROUTER_API_KEY`, then
|
|
|
96
104
|
the saved file. The environment wins, and both `/configure` and `hx auth` say
|
|
97
105
|
so — otherwise saving a key while a variable is set looks like a no-op.
|
|
98
106
|
|
|
107
|
+
**ChatGPT Plus/Pro.** Sign in over OAuth and run Codex models against your
|
|
108
|
+
subscription instead of paying per token
|
|
109
|
+
([OpenAI endorses this for OSS harnesses](https://developers.openai.com/community/codex-for-oss)):
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
hx auth login openai-codex # or /login inside the TUI
|
|
113
|
+
hx --model openai-codex/gpt-5.3-codex
|
|
114
|
+
hx auth logout openai-codex
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The browser opens to `auth.openai.com` and redirects to a loopback listener on
|
|
118
|
+
port 1455. Over SSH that redirect cannot reach your machine, so paste the final
|
|
119
|
+
URL into the prompt instead — or set `HX_LOGIN_DEVICE_CODE=1` for the
|
|
120
|
+
device-code flow. Access tokens are refreshed automatically; `/model` only
|
|
121
|
+
offers routes you are actually signed in to.
|
|
122
|
+
|
|
123
|
+
There is no Claude Pro/Max route. Anthropic rejects OAuth tokens unless the
|
|
124
|
+
request impersonates Claude Code, which HX will not do. Claude models stay
|
|
125
|
+
available through OpenRouter.
|
|
126
|
+
|
|
99
127
|
---
|
|
100
128
|
|
|
101
129
|
## Running it
|
|
@@ -105,12 +133,17 @@ hx # interactive TUI in the current directory
|
|
|
105
133
|
hx -p "explain this repo" # headless: streams to stdout, tool activity to stderr
|
|
106
134
|
hx resume # resume the last session here
|
|
107
135
|
hx resume <session-id> # resume a specific one
|
|
136
|
+
hx prompt # print the system prompt this directory would use
|
|
108
137
|
hx --model openai/gpt-5 # override the model for one run
|
|
109
138
|
hx --mode plan # start read-only
|
|
110
139
|
hx --cwd ../other-project # run against a different directory
|
|
111
140
|
hx --no-sandbox # disable OS sandboxing (rules still apply)
|
|
141
|
+
hx --system-prompt @p.md # replace the system prompt for one run
|
|
142
|
+
hx --append-system-prompt "Always run the tests" # add to it; repeatable
|
|
112
143
|
hx mcp list|add|remove # manage MCP servers
|
|
113
|
-
hx auth [set|clear] # manage the
|
|
144
|
+
hx auth [set|clear] # manage the OpenRouter key
|
|
145
|
+
hx auth login [provider] # sign in (openrouter, openai-codex)
|
|
146
|
+
hx auth logout <provider> # forget a stored credential
|
|
114
147
|
hx upgrade # update to the latest release
|
|
115
148
|
```
|
|
116
149
|
|
|
@@ -192,13 +225,17 @@ than being silently resolved.
|
|
|
192
225
|
| `/model [query]` | pick a model; shows context window, price per Mtok, cache support |
|
|
193
226
|
| `/models refresh` | re-fetch the catalogue |
|
|
194
227
|
| `/configure` | session settings and the API key |
|
|
228
|
+
| `/login [provider]` | sign in to a model route |
|
|
229
|
+
| `/logout <provider>` | forget a stored credential |
|
|
195
230
|
| `/mode [name]` | `plan`, `default`, `acceptEdits`, `bypass` |
|
|
196
231
|
| `/permissions` | active rules and what is enforcing them |
|
|
197
232
|
| `/context` | what is filling the context window |
|
|
198
233
|
| `/cost` | tokens, cache savings, spend |
|
|
199
234
|
| `/compact [focus]` | summarise older turns now |
|
|
200
235
|
| `/clear` | fresh session, same directory |
|
|
201
|
-
| `/resume` | reopen a previous session |
|
|
236
|
+
| `/resume` | reopen a previous session, listed by name |
|
|
237
|
+
| `/title [text]` | show or set this session's name |
|
|
238
|
+
| `/prompt` | the system prompt this session is running with |
|
|
202
239
|
| `/todos` | toggle the sidebar |
|
|
203
240
|
| `/skills` | installed skills |
|
|
204
241
|
| `/agents` | subagent types |
|
|
@@ -226,8 +263,9 @@ Settings are JSON, merged lowest to highest:
|
|
|
226
263
|
defaults < ~/.hx/settings.json < ./.hx/settings.json < HX_* env < CLI flags
|
|
227
264
|
```
|
|
228
265
|
|
|
229
|
-
Permission rule lists are unioned across layers, so a
|
|
230
|
-
rule
|
|
266
|
+
Permission rule lists and `prompt.append` are unioned across layers, so a
|
|
267
|
+
project can add a deny rule — or a line to the system prompt — without
|
|
268
|
+
discarding yours. Everything else is replaced.
|
|
231
269
|
|
|
232
270
|
```jsonc
|
|
233
271
|
{
|
|
@@ -238,6 +276,7 @@ rule without discarding yours. Everything else is replaced.
|
|
|
238
276
|
"models": {
|
|
239
277
|
"model": "anthropic/claude-sonnet-4.5",
|
|
240
278
|
"subagent_model": null, // defaults to "model"
|
|
279
|
+
"title_model": null, // model that names sessions; defaults to "model"
|
|
241
280
|
"max_tokens": 8192,
|
|
242
281
|
"temperature": null
|
|
243
282
|
},
|
|
@@ -262,6 +301,11 @@ rule without discarding yours. Everything else is replaced.
|
|
|
262
301
|
"timeout_seconds": 120,
|
|
263
302
|
"max_timeout_seconds": 600, // ceiling; caps what the model may ask for
|
|
264
303
|
"shell": null // defaults to $SHELL
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
"prompt": {
|
|
307
|
+
"system": null, // replaces the built-in system prompt
|
|
308
|
+
"append": [] // added after it; unioned across layers
|
|
265
309
|
}
|
|
266
310
|
}
|
|
267
311
|
```
|
|
@@ -270,6 +314,42 @@ Environment overrides: `HX_MODEL`, `HX_SUBAGENT_MODEL`, `HX_MAX_TOKENS`,
|
|
|
270
314
|
`HX_PERMISSION_MODE`, `HX_SANDBOX`, `HX_COMPACT_AT`, `HX_THEME`,
|
|
271
315
|
`HX_QUIET_STARTUP`. Also `HX_HOME` to relocate user state.
|
|
272
316
|
|
|
317
|
+
### The system prompt
|
|
318
|
+
|
|
319
|
+
`hx prompt` prints the prompt this directory resolves to, with its source on
|
|
320
|
+
stderr so stdout stays pipeable. `/prompt` shows the same thing inside a running
|
|
321
|
+
session.
|
|
322
|
+
|
|
323
|
+
The built-in prompt is `SYSTEM_PROMPT` in `src/hx/core/context.py`. It is
|
|
324
|
+
replaced by the first of these that exists:
|
|
325
|
+
|
|
326
|
+
| Source | Scope |
|
|
327
|
+
|---|---|
|
|
328
|
+
| `--system-prompt TEXT` or `@path`, or `prompt.system` | one run |
|
|
329
|
+
| `./.hx/system-prompt.md` | this project |
|
|
330
|
+
| `~/.hx/system-prompt.md` | you, everywhere |
|
|
331
|
+
| the built-in prompt | fallback |
|
|
332
|
+
|
|
333
|
+
Appended text is added after whichever prompt won, in this order:
|
|
334
|
+
`~/.hx/system-prompt-append.md`, `./.hx/system-prompt-append.md`, then each
|
|
335
|
+
`--append-system-prompt` value (and `prompt.append`) in the order given. Nothing
|
|
336
|
+
appended is ever discarded by a later layer.
|
|
337
|
+
|
|
338
|
+
This is a different lever from `HX.md`: `HX.md` describes *the project* and is
|
|
339
|
+
injected as its own context section, while these files change *the agent's
|
|
340
|
+
instructions*. Both are read once at startup and frozen, because they sit above
|
|
341
|
+
the first cache breakpoint — an override edited mid-session applies on the next
|
|
342
|
+
run, and `/prompt` says so when it spots one.
|
|
343
|
+
|
|
344
|
+
### Sessions are named
|
|
345
|
+
|
|
346
|
+
After the first exchange, HX asks the model for a short name for the session and
|
|
347
|
+
writes it to `~/.hx/sessions/<id>/meta.json`, so `/resume` lists work rather
|
|
348
|
+
than timestamps. It is one small call — cap 32 output tokens, `models.title_model`
|
|
349
|
+
if you want a cheaper model for it — and it is counted in `/cost` like any other.
|
|
350
|
+
If the call fails the session is still named, from your first message. `/title
|
|
351
|
+
<text>` renames it.
|
|
352
|
+
|
|
273
353
|
### Themes
|
|
274
354
|
|
|
275
355
|
A theme is a JSON file: a `vars` block of raw colours, and a `colors` block
|
|
@@ -302,10 +382,13 @@ files load here unchanged. `src/hx/tui/themes/dark.json` is the reference.
|
|
|
302
382
|
| `~/.hx/models.json` | cached model catalogue, refreshed daily |
|
|
303
383
|
| `~/.hx/themes/` | your themes, one JSON file each |
|
|
304
384
|
| `~/.hx/keybindings.json` | your key overrides |
|
|
385
|
+
| `~/.hx/system-prompt.md` | your system prompt, replacing the built-in one |
|
|
386
|
+
| `~/.hx/system-prompt-append.md` | text appended to whichever prompt is in force |
|
|
305
387
|
| `~/.hx/sessions/` | transcripts, spilled tool output, subagent sessions |
|
|
306
388
|
| `~/.hx/skills/`, `~/.hx/agents/` | your skills and agents |
|
|
307
389
|
| `./.hx/settings.json` | project settings, checked in if you like |
|
|
308
390
|
| `./.hx/mcp.json` | project MCP servers |
|
|
391
|
+
| `./.hx/system-prompt.md`, `./.hx/system-prompt-append.md` | project prompt overrides |
|
|
309
392
|
| `./.hx/skills/`, `./.hx/agents/` | project skills and agents |
|
|
310
393
|
| `./HX.md` | project instructions, loaded into every session |
|
|
311
394
|
|
|
@@ -433,12 +516,20 @@ uv run mypy # strict
|
|
|
433
516
|
uv run hx # run from the checkout
|
|
434
517
|
```
|
|
435
518
|
|
|
436
|
-
Tests marked `live` hit
|
|
519
|
+
Tests marked `live` hit a real API. The OpenRouter ones cost money:
|
|
437
520
|
|
|
438
521
|
```sh
|
|
439
|
-
OPENROUTER_API_KEY=... uv run pytest -m live
|
|
522
|
+
OPENROUTER_API_KEY=... uv run pytest -m live tests/test_live.py
|
|
523
|
+
|
|
524
|
+
# the Codex route draws on your ChatGPT subscription instead
|
|
525
|
+
hx auth login openai-codex
|
|
526
|
+
uv run pytest -m live tests/test_live_codex.py
|
|
440
527
|
```
|
|
441
528
|
|
|
529
|
+
They are the only place the wire formats, streaming, tool use and a genuine
|
|
530
|
+
cache hit are proven against a real service; everything else runs against the
|
|
531
|
+
scripted provider.
|
|
532
|
+
|
|
442
533
|
Tests marked `sandbox` exercise the real OS sandbox and are skipped where no
|
|
443
534
|
backend exists.
|
|
444
535
|
|
|
@@ -455,7 +546,8 @@ uv run pytest tests/tui/test_snapshots.py --snapshot-update
|
|
|
455
546
|
src/hx/
|
|
456
547
|
cli.py config.py paths.py frontmatter.py
|
|
457
548
|
core/ loop, context assembly, compaction, late injection, sessions, usage
|
|
458
|
-
|
|
549
|
+
auth/ credential store, OAuth flows, per-route resolution
|
|
550
|
+
providers/ OpenRouter, Codex, the model catalogue, a scripted provider for tests
|
|
459
551
|
tools/ Bash, Read, Write, Edit, Glob, Grep, TodoWrite, Task, output capping
|
|
460
552
|
permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
|
|
461
553
|
skills/ agents/ mcp/
|
|
@@ -509,9 +601,12 @@ streaming with prefix caching and accurate cost accounting, session persistence
|
|
|
509
601
|
and resume, the tool suite, the permission engine and OS sandbox, late
|
|
510
602
|
injection, compaction, output capping, skills, subagents, MCP, and the TUI.
|
|
511
603
|
|
|
604
|
+
Two routes to a model: an OpenRouter API key, or a ChatGPT Plus/Pro
|
|
605
|
+
subscription over OAuth. The model id decides which.
|
|
606
|
+
|
|
512
607
|
Published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/), released
|
|
513
608
|
from CI on a tag.
|
|
514
609
|
|
|
515
|
-
The one thing still unproven is a live
|
|
516
|
-
and cover
|
|
517
|
-
|
|
610
|
+
The one thing still unproven in CI is a live call on either route: the `live`
|
|
611
|
+
tests exist and cover both wire formats, tool use, reasoning replay and a
|
|
612
|
+
genuine cache hit, but they need a credential and are deselected by default.
|
|
@@ -47,16 +47,24 @@ export PATH="$(uv tool dir --bin):$PATH"
|
|
|
47
47
|
|
|
48
48
|
Requires Python 3.11+. macOS and Linux. Update with `hx upgrade`.
|
|
49
49
|
|
|
50
|
-
###
|
|
50
|
+
### Credentials
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
HX reaches models over two routes. Which one serves a turn is decided by the
|
|
53
|
+
model id alone, so "what paid for that" is always answerable by reading it:
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
| Model id | Route | Billing |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `anthropic/claude-sonnet-4.5`, `openai/gpt-5`, … | OpenRouter | per token, API key |
|
|
58
|
+
| `openai-codex/gpt-5.3-codex` | ChatGPT Plus/Pro | your subscription |
|
|
59
|
+
|
|
60
|
+
Credentials live in `~/.hx/auth.json`, mode 0600, one entry per route.
|
|
61
|
+
|
|
62
|
+
**OpenRouter.** On first run HX asks for a key. Get one at
|
|
63
|
+
<https://openrouter.ai/keys>. Change it later with `/configure` in the TUI, or:
|
|
56
64
|
|
|
57
65
|
```sh
|
|
58
|
-
hx auth #
|
|
59
|
-
hx auth set # paste a new
|
|
66
|
+
hx auth # which routes have a credential, and where from
|
|
67
|
+
hx auth set # paste a new OpenRouter key (hidden input)
|
|
60
68
|
hx auth clear # remove the saved key
|
|
61
69
|
```
|
|
62
70
|
|
|
@@ -64,6 +72,26 @@ Resolution order is `HX_OPENROUTER_API_KEY`, then `OPENROUTER_API_KEY`, then
|
|
|
64
72
|
the saved file. The environment wins, and both `/configure` and `hx auth` say
|
|
65
73
|
so — otherwise saving a key while a variable is set looks like a no-op.
|
|
66
74
|
|
|
75
|
+
**ChatGPT Plus/Pro.** Sign in over OAuth and run Codex models against your
|
|
76
|
+
subscription instead of paying per token
|
|
77
|
+
([OpenAI endorses this for OSS harnesses](https://developers.openai.com/community/codex-for-oss)):
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
hx auth login openai-codex # or /login inside the TUI
|
|
81
|
+
hx --model openai-codex/gpt-5.3-codex
|
|
82
|
+
hx auth logout openai-codex
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The browser opens to `auth.openai.com` and redirects to a loopback listener on
|
|
86
|
+
port 1455. Over SSH that redirect cannot reach your machine, so paste the final
|
|
87
|
+
URL into the prompt instead — or set `HX_LOGIN_DEVICE_CODE=1` for the
|
|
88
|
+
device-code flow. Access tokens are refreshed automatically; `/model` only
|
|
89
|
+
offers routes you are actually signed in to.
|
|
90
|
+
|
|
91
|
+
There is no Claude Pro/Max route. Anthropic rejects OAuth tokens unless the
|
|
92
|
+
request impersonates Claude Code, which HX will not do. Claude models stay
|
|
93
|
+
available through OpenRouter.
|
|
94
|
+
|
|
67
95
|
---
|
|
68
96
|
|
|
69
97
|
## Running it
|
|
@@ -73,12 +101,17 @@ hx # interactive TUI in the current directory
|
|
|
73
101
|
hx -p "explain this repo" # headless: streams to stdout, tool activity to stderr
|
|
74
102
|
hx resume # resume the last session here
|
|
75
103
|
hx resume <session-id> # resume a specific one
|
|
104
|
+
hx prompt # print the system prompt this directory would use
|
|
76
105
|
hx --model openai/gpt-5 # override the model for one run
|
|
77
106
|
hx --mode plan # start read-only
|
|
78
107
|
hx --cwd ../other-project # run against a different directory
|
|
79
108
|
hx --no-sandbox # disable OS sandboxing (rules still apply)
|
|
109
|
+
hx --system-prompt @p.md # replace the system prompt for one run
|
|
110
|
+
hx --append-system-prompt "Always run the tests" # add to it; repeatable
|
|
80
111
|
hx mcp list|add|remove # manage MCP servers
|
|
81
|
-
hx auth [set|clear] # manage the
|
|
112
|
+
hx auth [set|clear] # manage the OpenRouter key
|
|
113
|
+
hx auth login [provider] # sign in (openrouter, openai-codex)
|
|
114
|
+
hx auth logout <provider> # forget a stored credential
|
|
82
115
|
hx upgrade # update to the latest release
|
|
83
116
|
```
|
|
84
117
|
|
|
@@ -160,13 +193,17 @@ than being silently resolved.
|
|
|
160
193
|
| `/model [query]` | pick a model; shows context window, price per Mtok, cache support |
|
|
161
194
|
| `/models refresh` | re-fetch the catalogue |
|
|
162
195
|
| `/configure` | session settings and the API key |
|
|
196
|
+
| `/login [provider]` | sign in to a model route |
|
|
197
|
+
| `/logout <provider>` | forget a stored credential |
|
|
163
198
|
| `/mode [name]` | `plan`, `default`, `acceptEdits`, `bypass` |
|
|
164
199
|
| `/permissions` | active rules and what is enforcing them |
|
|
165
200
|
| `/context` | what is filling the context window |
|
|
166
201
|
| `/cost` | tokens, cache savings, spend |
|
|
167
202
|
| `/compact [focus]` | summarise older turns now |
|
|
168
203
|
| `/clear` | fresh session, same directory |
|
|
169
|
-
| `/resume` | reopen a previous session |
|
|
204
|
+
| `/resume` | reopen a previous session, listed by name |
|
|
205
|
+
| `/title [text]` | show or set this session's name |
|
|
206
|
+
| `/prompt` | the system prompt this session is running with |
|
|
170
207
|
| `/todos` | toggle the sidebar |
|
|
171
208
|
| `/skills` | installed skills |
|
|
172
209
|
| `/agents` | subagent types |
|
|
@@ -194,8 +231,9 @@ Settings are JSON, merged lowest to highest:
|
|
|
194
231
|
defaults < ~/.hx/settings.json < ./.hx/settings.json < HX_* env < CLI flags
|
|
195
232
|
```
|
|
196
233
|
|
|
197
|
-
Permission rule lists are unioned across layers, so a
|
|
198
|
-
rule
|
|
234
|
+
Permission rule lists and `prompt.append` are unioned across layers, so a
|
|
235
|
+
project can add a deny rule — or a line to the system prompt — without
|
|
236
|
+
discarding yours. Everything else is replaced.
|
|
199
237
|
|
|
200
238
|
```jsonc
|
|
201
239
|
{
|
|
@@ -206,6 +244,7 @@ rule without discarding yours. Everything else is replaced.
|
|
|
206
244
|
"models": {
|
|
207
245
|
"model": "anthropic/claude-sonnet-4.5",
|
|
208
246
|
"subagent_model": null, // defaults to "model"
|
|
247
|
+
"title_model": null, // model that names sessions; defaults to "model"
|
|
209
248
|
"max_tokens": 8192,
|
|
210
249
|
"temperature": null
|
|
211
250
|
},
|
|
@@ -230,6 +269,11 @@ rule without discarding yours. Everything else is replaced.
|
|
|
230
269
|
"timeout_seconds": 120,
|
|
231
270
|
"max_timeout_seconds": 600, // ceiling; caps what the model may ask for
|
|
232
271
|
"shell": null // defaults to $SHELL
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
"prompt": {
|
|
275
|
+
"system": null, // replaces the built-in system prompt
|
|
276
|
+
"append": [] // added after it; unioned across layers
|
|
233
277
|
}
|
|
234
278
|
}
|
|
235
279
|
```
|
|
@@ -238,6 +282,42 @@ Environment overrides: `HX_MODEL`, `HX_SUBAGENT_MODEL`, `HX_MAX_TOKENS`,
|
|
|
238
282
|
`HX_PERMISSION_MODE`, `HX_SANDBOX`, `HX_COMPACT_AT`, `HX_THEME`,
|
|
239
283
|
`HX_QUIET_STARTUP`. Also `HX_HOME` to relocate user state.
|
|
240
284
|
|
|
285
|
+
### The system prompt
|
|
286
|
+
|
|
287
|
+
`hx prompt` prints the prompt this directory resolves to, with its source on
|
|
288
|
+
stderr so stdout stays pipeable. `/prompt` shows the same thing inside a running
|
|
289
|
+
session.
|
|
290
|
+
|
|
291
|
+
The built-in prompt is `SYSTEM_PROMPT` in `src/hx/core/context.py`. It is
|
|
292
|
+
replaced by the first of these that exists:
|
|
293
|
+
|
|
294
|
+
| Source | Scope |
|
|
295
|
+
|---|---|
|
|
296
|
+
| `--system-prompt TEXT` or `@path`, or `prompt.system` | one run |
|
|
297
|
+
| `./.hx/system-prompt.md` | this project |
|
|
298
|
+
| `~/.hx/system-prompt.md` | you, everywhere |
|
|
299
|
+
| the built-in prompt | fallback |
|
|
300
|
+
|
|
301
|
+
Appended text is added after whichever prompt won, in this order:
|
|
302
|
+
`~/.hx/system-prompt-append.md`, `./.hx/system-prompt-append.md`, then each
|
|
303
|
+
`--append-system-prompt` value (and `prompt.append`) in the order given. Nothing
|
|
304
|
+
appended is ever discarded by a later layer.
|
|
305
|
+
|
|
306
|
+
This is a different lever from `HX.md`: `HX.md` describes *the project* and is
|
|
307
|
+
injected as its own context section, while these files change *the agent's
|
|
308
|
+
instructions*. Both are read once at startup and frozen, because they sit above
|
|
309
|
+
the first cache breakpoint — an override edited mid-session applies on the next
|
|
310
|
+
run, and `/prompt` says so when it spots one.
|
|
311
|
+
|
|
312
|
+
### Sessions are named
|
|
313
|
+
|
|
314
|
+
After the first exchange, HX asks the model for a short name for the session and
|
|
315
|
+
writes it to `~/.hx/sessions/<id>/meta.json`, so `/resume` lists work rather
|
|
316
|
+
than timestamps. It is one small call — cap 32 output tokens, `models.title_model`
|
|
317
|
+
if you want a cheaper model for it — and it is counted in `/cost` like any other.
|
|
318
|
+
If the call fails the session is still named, from your first message. `/title
|
|
319
|
+
<text>` renames it.
|
|
320
|
+
|
|
241
321
|
### Themes
|
|
242
322
|
|
|
243
323
|
A theme is a JSON file: a `vars` block of raw colours, and a `colors` block
|
|
@@ -270,10 +350,13 @@ files load here unchanged. `src/hx/tui/themes/dark.json` is the reference.
|
|
|
270
350
|
| `~/.hx/models.json` | cached model catalogue, refreshed daily |
|
|
271
351
|
| `~/.hx/themes/` | your themes, one JSON file each |
|
|
272
352
|
| `~/.hx/keybindings.json` | your key overrides |
|
|
353
|
+
| `~/.hx/system-prompt.md` | your system prompt, replacing the built-in one |
|
|
354
|
+
| `~/.hx/system-prompt-append.md` | text appended to whichever prompt is in force |
|
|
273
355
|
| `~/.hx/sessions/` | transcripts, spilled tool output, subagent sessions |
|
|
274
356
|
| `~/.hx/skills/`, `~/.hx/agents/` | your skills and agents |
|
|
275
357
|
| `./.hx/settings.json` | project settings, checked in if you like |
|
|
276
358
|
| `./.hx/mcp.json` | project MCP servers |
|
|
359
|
+
| `./.hx/system-prompt.md`, `./.hx/system-prompt-append.md` | project prompt overrides |
|
|
277
360
|
| `./.hx/skills/`, `./.hx/agents/` | project skills and agents |
|
|
278
361
|
| `./HX.md` | project instructions, loaded into every session |
|
|
279
362
|
|
|
@@ -401,12 +484,20 @@ uv run mypy # strict
|
|
|
401
484
|
uv run hx # run from the checkout
|
|
402
485
|
```
|
|
403
486
|
|
|
404
|
-
Tests marked `live` hit
|
|
487
|
+
Tests marked `live` hit a real API. The OpenRouter ones cost money:
|
|
405
488
|
|
|
406
489
|
```sh
|
|
407
|
-
OPENROUTER_API_KEY=... uv run pytest -m live
|
|
490
|
+
OPENROUTER_API_KEY=... uv run pytest -m live tests/test_live.py
|
|
491
|
+
|
|
492
|
+
# the Codex route draws on your ChatGPT subscription instead
|
|
493
|
+
hx auth login openai-codex
|
|
494
|
+
uv run pytest -m live tests/test_live_codex.py
|
|
408
495
|
```
|
|
409
496
|
|
|
497
|
+
They are the only place the wire formats, streaming, tool use and a genuine
|
|
498
|
+
cache hit are proven against a real service; everything else runs against the
|
|
499
|
+
scripted provider.
|
|
500
|
+
|
|
410
501
|
Tests marked `sandbox` exercise the real OS sandbox and are skipped where no
|
|
411
502
|
backend exists.
|
|
412
503
|
|
|
@@ -423,7 +514,8 @@ uv run pytest tests/tui/test_snapshots.py --snapshot-update
|
|
|
423
514
|
src/hx/
|
|
424
515
|
cli.py config.py paths.py frontmatter.py
|
|
425
516
|
core/ loop, context assembly, compaction, late injection, sessions, usage
|
|
426
|
-
|
|
517
|
+
auth/ credential store, OAuth flows, per-route resolution
|
|
518
|
+
providers/ OpenRouter, Codex, the model catalogue, a scripted provider for tests
|
|
427
519
|
tools/ Bash, Read, Write, Edit, Glob, Grep, TodoWrite, Task, output capping
|
|
428
520
|
permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
|
|
429
521
|
skills/ agents/ mcp/
|
|
@@ -477,9 +569,12 @@ streaming with prefix caching and accurate cost accounting, session persistence
|
|
|
477
569
|
and resume, the tool suite, the permission engine and OS sandbox, late
|
|
478
570
|
injection, compaction, output capping, skills, subagents, MCP, and the TUI.
|
|
479
571
|
|
|
572
|
+
Two routes to a model: an OpenRouter API key, or a ChatGPT Plus/Pro
|
|
573
|
+
subscription over OAuth. The model id decides which.
|
|
574
|
+
|
|
480
575
|
Published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/), released
|
|
481
576
|
from CI on a tag.
|
|
482
577
|
|
|
483
|
-
The one thing still unproven is a live
|
|
484
|
-
and cover
|
|
485
|
-
|
|
578
|
+
The one thing still unproven in CI is a live call on either route: the `live`
|
|
579
|
+
tests exist and cover both wire formats, tool use, reasoning replay and a
|
|
580
|
+
genuine cache hit, but they need a credential and are deselected by default.
|
hx_cli-0.1.5/install.sh
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# HX installer.
|
|
3
|
+
#
|
|
4
|
+
# curl -fsSL https://raw.githubusercontent.com/aletisunil/hx/main/install.sh | sh
|
|
5
|
+
#
|
|
6
|
+
# Bootstraps `uv` if it is missing, then installs hx as an isolated uv tool with
|
|
7
|
+
# a pinned Python. Idempotent: re-running upgrades in place.
|
|
8
|
+
|
|
9
|
+
set -eu
|
|
10
|
+
|
|
11
|
+
# The PyPI distribution is hx-cli; the command it installs is `hx`.
|
|
12
|
+
HX_PACKAGE="${HX_PACKAGE:-hx-cli}"
|
|
13
|
+
HX_PYTHON="${HX_PYTHON:-3.12}"
|
|
14
|
+
HX_SUPPORT_EMAIL="iam@sunilaleti.dev"
|
|
15
|
+
UV_INSTALLER="https://astral.sh/uv/install.sh"
|
|
16
|
+
|
|
17
|
+
log() { printf '\033[0;36m==>\033[0m %s\n' "$1"; }
|
|
18
|
+
warn() { printf '\033[0;33mwarning:\033[0m %s\n' "$1" >&2; }
|
|
19
|
+
die() {
|
|
20
|
+
printf '\033[0;31merror:\033[0m %s\n' "$1" >&2
|
|
21
|
+
printf 'Need help? Contact \033[1;33m%s\033[0m\n' "$HX_SUPPORT_EMAIL" >&2
|
|
22
|
+
exit 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
have() { command -v "$1" >/dev/null 2>&1; }
|
|
26
|
+
|
|
27
|
+
should_animate() {
|
|
28
|
+
[ -t 1 ] && [ -z "${HX_NO_ANIMATION:-}" ] && [ -z "${CI:-}" ]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
render_logo() {
|
|
32
|
+
padding=$1
|
|
33
|
+
clear_prefix=
|
|
34
|
+
[ "${2:-}" = clear ] && clear_prefix='\033[2K\r'
|
|
35
|
+
|
|
36
|
+
printf '\033[0;36m'
|
|
37
|
+
printf '%b%s%s\n' "$clear_prefix" "$padding" ' _ _ __ __'
|
|
38
|
+
printf '%b%s%s\n' "$clear_prefix" "$padding" '| | | | \ \/ /'
|
|
39
|
+
printf '%b%s%s\n' "$clear_prefix" "$padding" '| |_| | > <'
|
|
40
|
+
printf '%b%s%s\n' "$clear_prefix" "$padding" "| _ | / /\\ \\"
|
|
41
|
+
printf '%b%s%s\n' "$clear_prefix" "$padding" "|_| |_|/_/ \\_\\"
|
|
42
|
+
printf '\033[0m'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
animate_logo() {
|
|
46
|
+
if ! should_animate; then
|
|
47
|
+
render_logo ''
|
|
48
|
+
return
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Slide in, overshoot slightly, then settle into place.
|
|
52
|
+
render_logo ' ' clear
|
|
53
|
+
sleep 0.07
|
|
54
|
+
printf '\033[5A'
|
|
55
|
+
render_logo ' ' clear
|
|
56
|
+
sleep 0.07
|
|
57
|
+
printf '\033[5A'
|
|
58
|
+
render_logo ' ' clear
|
|
59
|
+
sleep 0.07
|
|
60
|
+
printf '\033[5A'
|
|
61
|
+
render_logo '' clear
|
|
62
|
+
sleep 0.07
|
|
63
|
+
printf '\033[5A'
|
|
64
|
+
render_logo ' ' clear
|
|
65
|
+
sleep 0.07
|
|
66
|
+
printf '\033[5A'
|
|
67
|
+
render_logo '' clear
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
stream_text() {
|
|
71
|
+
message=$1
|
|
72
|
+
printf ' '
|
|
73
|
+
if should_animate; then
|
|
74
|
+
# Word-by-word output keeps the effect portable across macOS and Linux.
|
|
75
|
+
# shellcheck disable=SC2086 # Intentional word splitting for animation.
|
|
76
|
+
for word in $message; do
|
|
77
|
+
printf '%s ' "$word"
|
|
78
|
+
sleep 0.03
|
|
79
|
+
done
|
|
80
|
+
printf '\n'
|
|
81
|
+
else
|
|
82
|
+
printf '%s\n' "$message"
|
|
83
|
+
fi
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
banner() {
|
|
87
|
+
animate_logo
|
|
88
|
+
printf '\n'
|
|
89
|
+
stream_text 'HX is a terminal coding agent that works inside your project.'
|
|
90
|
+
stream_text 'It reads and edits files, runs commands in a sandbox, and handles larger tasks with persistent sessions, skills, tools, and subagents.'
|
|
91
|
+
stream_text 'Connect to models through OpenRouter and see context, cost, and usage for every turn.'
|
|
92
|
+
printf ' Questions or issues? Contact \033[1;33m%s\033[0m\n\n' "$HX_SUPPORT_EMAIL"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
ensure_uv() {
|
|
96
|
+
if have uv; then
|
|
97
|
+
log "uv found: $(uv --version)"
|
|
98
|
+
return
|
|
99
|
+
fi
|
|
100
|
+
log "Installing uv"
|
|
101
|
+
have curl || have wget || die "need curl or wget to bootstrap uv"
|
|
102
|
+
if have curl; then
|
|
103
|
+
curl -LsSf "$UV_INSTALLER" | sh
|
|
104
|
+
else
|
|
105
|
+
wget -qO- "$UV_INSTALLER" | sh
|
|
106
|
+
fi
|
|
107
|
+
# The uv installer drops binaries here but does not touch the current shell.
|
|
108
|
+
for candidate in "$HOME/.local/bin" "$HOME/.cargo/bin"; do
|
|
109
|
+
[ -x "$candidate/uv" ] && PATH="$candidate:$PATH"
|
|
110
|
+
done
|
|
111
|
+
export PATH
|
|
112
|
+
have uv || die "uv installed but not on PATH; open a new shell and re-run"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
install_hx() {
|
|
116
|
+
log "Installing $HX_PACKAGE (Python $HX_PYTHON)"
|
|
117
|
+
uv tool install --python "$HX_PYTHON" --force "$HX_PACKAGE"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
check_path() {
|
|
121
|
+
have hx && return
|
|
122
|
+
bindir="$(uv tool dir --bin 2>/dev/null || echo "$HOME/.local/bin")"
|
|
123
|
+
warn "hx is not on your PATH yet."
|
|
124
|
+
# shellcheck disable=SC2016 # $PATH is literal text for the user to copy.
|
|
125
|
+
printf '\n Add this to your shell profile:\n\n export PATH="%s:$PATH"\n\n' "$bindir"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
main() {
|
|
129
|
+
banner
|
|
130
|
+
ensure_uv
|
|
131
|
+
install_hx
|
|
132
|
+
check_path
|
|
133
|
+
log "Done. Run 'hx' inside a project directory."
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
main "$@"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Credential storage and OAuth flows.
|
|
2
|
+
|
|
3
|
+
One credential per provider id, stored in ``~/.hx/auth.json``. Providers ask
|
|
4
|
+
:func:`hx.auth.resolve.resolve` for request auth; they never read the file or
|
|
5
|
+
the environment themselves.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from hx.auth.store import (
|
|
9
|
+
ApiKeyCredential,
|
|
10
|
+
AuthStore,
|
|
11
|
+
Credential,
|
|
12
|
+
CredentialInfo,
|
|
13
|
+
OAuthCredential,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ApiKeyCredential",
|
|
18
|
+
"AuthStore",
|
|
19
|
+
"Credential",
|
|
20
|
+
"CredentialInfo",
|
|
21
|
+
"OAuthCredential",
|
|
22
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""OAuth flows. One module per provider, plus shared PKCE and callback helpers."""
|