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.
Files changed (154) hide show
  1. {hx_cli-0.1.2 → hx_cli-0.1.5}/PKG-INFO +112 -17
  2. {hx_cli-0.1.2 → hx_cli-0.1.5}/README.md +111 -16
  3. hx_cli-0.1.5/install.sh +136 -0
  4. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/__init__.py +1 -1
  5. hx_cli-0.1.5/src/hx/auth/__init__.py +22 -0
  6. hx_cli-0.1.5/src/hx/auth/oauth/__init__.py +1 -0
  7. hx_cli-0.1.5/src/hx/auth/oauth/callback.py +203 -0
  8. hx_cli-0.1.5/src/hx/auth/oauth/codex.py +325 -0
  9. hx_cli-0.1.5/src/hx/auth/oauth/pkce.py +42 -0
  10. hx_cli-0.1.5/src/hx/auth/resolve.py +239 -0
  11. hx_cli-0.1.5/src/hx/auth/store.py +242 -0
  12. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/cli.py +234 -44
  13. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/config.py +34 -1
  14. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/context.py +83 -4
  15. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/events.py +6 -0
  16. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/loop.py +65 -5
  17. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/session.py +17 -1
  18. hx_cli-0.1.5/src/hx/core/title.py +105 -0
  19. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/paths.py +18 -0
  20. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/providers/base.py +5 -0
  21. hx_cli-0.1.5/src/hx/providers/codex.py +186 -0
  22. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/providers/models.py +71 -11
  23. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/providers/openrouter.py +15 -51
  24. hx_cli-0.1.5/src/hx/providers/registry.py +153 -0
  25. hx_cli-0.1.5/src/hx/providers/responses_codec.py +337 -0
  26. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/app.py +78 -16
  27. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/commands.py +160 -11
  28. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/input.py +20 -6
  29. hx_cli-0.1.5/src/hx/tui/widgets/login.py +148 -0
  30. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/statusbar.py +12 -2
  31. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/transcript.py +55 -8
  32. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/agents/test_subagent.py +2 -0
  33. hx_cli-0.1.5/tests/auth/test_codex_oauth.py +130 -0
  34. hx_cli-0.1.5/tests/auth/test_pkce.py +27 -0
  35. hx_cli-0.1.5/tests/auth/test_resolve.py +166 -0
  36. hx_cli-0.1.5/tests/auth/test_store.py +160 -0
  37. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/core/test_context.py +68 -1
  38. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/core/test_loop.py +69 -0
  39. hx_cli-0.1.5/tests/core/test_session.py +69 -0
  40. hx_cli-0.1.5/tests/core/test_title.py +70 -0
  41. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/permissions/test_sandbox.py +0 -26
  42. hx_cli-0.1.5/tests/providers/test_codex.py +80 -0
  43. hx_cli-0.1.5/tests/providers/test_registry.py +84 -0
  44. hx_cli-0.1.5/tests/providers/test_responses_codec.py +272 -0
  45. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/test_cli.py +53 -0
  46. hx_cli-0.1.5/tests/test_install.py +53 -0
  47. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/test_integration.py +5 -1
  48. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/test_live.py +2 -1
  49. hx_cli-0.1.5/tests/test_live_codex.py +136 -0
  50. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/test_readme.py +18 -2
  51. hx_cli-0.1.5/tests/tools/test_output.py +31 -0
  52. hx_cli-0.1.5/tests/tui/__init__.py +0 -0
  53. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/__snapshots__/test_snapshots/test_a_running_turn.raw +81 -81
  54. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/__snapshots__/test_snapshots/test_a_turn_with_tool_blocks.raw +83 -83
  55. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/__snapshots__/test_snapshots/test_the_completion_popup.raw +80 -80
  56. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/__snapshots__/test_snapshots/test_the_idle_screen.raw +79 -79
  57. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_app.py +207 -3
  58. hx_cli-0.1.5/tests/tui/test_login.py +238 -0
  59. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_snapshots.py +18 -1
  60. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_transcript.py +28 -0
  61. hx_cli-0.1.2/install.sh +0 -62
  62. hx_cli-0.1.2/tests/tools/test_output.py +0 -55
  63. {hx_cli-0.1.2 → hx_cli-0.1.5}/.github/workflows/ci.yml +0 -0
  64. {hx_cli-0.1.2 → hx_cli-0.1.5}/.gitignore +0 -0
  65. {hx_cli-0.1.2 → hx_cli-0.1.5}/pyproject.toml +0 -0
  66. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/agents/__init__.py +0 -0
  67. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/agents/definitions.py +0 -0
  68. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/agents/subagent.py +0 -0
  69. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/__init__.py +0 -0
  70. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/compaction.py +0 -0
  71. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/lateinject.py +0 -0
  72. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/messages.py +0 -0
  73. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/core/usage.py +0 -0
  74. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/frontmatter.py +0 -0
  75. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/keys.py +0 -0
  76. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/mcp/__init__.py +0 -0
  77. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/mcp/client.py +0 -0
  78. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/mcp/manager.py +0 -0
  79. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/permissions/__init__.py +0 -0
  80. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/permissions/engine.py +0 -0
  81. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/permissions/parser.py +0 -0
  82. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/permissions/sandbox.py +0 -0
  83. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/providers/__init__.py +0 -0
  84. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/providers/fake.py +0 -0
  85. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/skills/__init__.py +0 -0
  86. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/skills/loader.py +0 -0
  87. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/skills/runtime.py +0 -0
  88. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/__init__.py +0 -0
  89. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/base.py +0 -0
  90. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/bash.py +0 -0
  91. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/edit.py +0 -0
  92. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/glob.py +0 -0
  93. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/grep.py +0 -0
  94. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/output.py +0 -0
  95. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/read.py +0 -0
  96. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/registry.py +0 -0
  97. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/task.py +0 -0
  98. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/todo.py +0 -0
  99. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tools/write.py +0 -0
  100. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/__init__.py +0 -0
  101. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/clipboard.py +0 -0
  102. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/fuzzy.py +0 -0
  103. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/hx.tcss +0 -0
  104. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/killring.py +0 -0
  105. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/renderers.py +0 -0
  106. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/roles.py +0 -0
  107. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/theme.py +0 -0
  108. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/theme_json.py +0 -0
  109. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/themes/ansi.json +0 -0
  110. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/themes/dark.json +0 -0
  111. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/themes/light.json +0 -0
  112. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/__init__.py +0 -0
  113. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/autocomplete.py +0 -0
  114. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/configure.py +0 -0
  115. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/diff.py +0 -0
  116. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/frame.py +0 -0
  117. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/header.py +0 -0
  118. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/hints.py +0 -0
  119. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/palette.py +0 -0
  120. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/permission.py +0 -0
  121. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/rule.py +0 -0
  122. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/todos.py +0 -0
  123. {hx_cli-0.1.2 → hx_cli-0.1.5}/src/hx/tui/widgets/working.py +0 -0
  124. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/__init__.py +0 -0
  125. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/agents/__init__.py +0 -0
  126. {hx_cli-0.1.2/tests/core → hx_cli-0.1.5/tests/auth}/__init__.py +0 -0
  127. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/conftest.py +0 -0
  128. {hx_cli-0.1.2/tests/mcp → hx_cli-0.1.5/tests/core}/__init__.py +0 -0
  129. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/core/test_compaction.py +0 -0
  130. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/core/test_lateinject.py +0 -0
  131. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/core/test_usage.py +0 -0
  132. {hx_cli-0.1.2/tests/permissions → hx_cli-0.1.5/tests/mcp}/__init__.py +0 -0
  133. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/mcp/fixtures/echo_server.py +0 -0
  134. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/mcp/test_manager.py +0 -0
  135. {hx_cli-0.1.2/tests/providers → hx_cli-0.1.5/tests/permissions}/__init__.py +0 -0
  136. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/permissions/test_engine.py +0 -0
  137. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/permissions/test_parser.py +0 -0
  138. {hx_cli-0.1.2/tests/skills → hx_cli-0.1.5/tests/providers}/__init__.py +0 -0
  139. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/providers/test_openrouter.py +0 -0
  140. {hx_cli-0.1.2/tests/tools → hx_cli-0.1.5/tests/skills}/__init__.py +0 -0
  141. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/skills/test_loader.py +0 -0
  142. {hx_cli-0.1.2/tests/tui → hx_cli-0.1.5/tests/tools}/__init__.py +0 -0
  143. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tools/test_background.py +0 -0
  144. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tools/test_bash.py +0 -0
  145. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tools/test_files.py +0 -0
  146. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tools/test_todo.py +0 -0
  147. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_autocomplete.py +0 -0
  148. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_clipboard.py +0 -0
  149. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_frame.py +0 -0
  150. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_keys.py +0 -0
  151. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_renderers.py +0 -0
  152. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_statusbar.py +0 -0
  153. {hx_cli-0.1.2 → hx_cli-0.1.5}/tests/tui/test_theme_json.py +0 -0
  154. {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.2
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
- ### The API key
82
+ ### Credentials
83
83
 
84
- On first run HX asks for an OpenRouter key and saves it to `~/.hx/auth.json`
85
- with mode 0600. Get one at <https://openrouter.ai/keys>.
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
- To change it later, `/configure` inside the TUI, or from a shell:
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 # is a key set, and where does it come from?
91
- hx auth set # paste a new one (hidden input)
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 API key
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 project can add a deny
230
- rule without discarding yours. Everything else is replaced.
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 the real OpenRouter API and cost money:
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
- providers/ OpenRouter, the model catalogue, a scripted provider for tests
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 OpenRouter call: the `live` tests exist
516
- and cover the wire format, tool use and a genuine cache hit, but they need a
517
- key and are deselected by default.
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
- ### The API key
50
+ ### Credentials
51
51
 
52
- On first run HX asks for an OpenRouter key and saves it to `~/.hx/auth.json`
53
- with mode 0600. Get one at <https://openrouter.ai/keys>.
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
- To change it later, `/configure` inside the TUI, or from a shell:
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 # is a key set, and where does it come from?
59
- hx auth set # paste a new one (hidden input)
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 API key
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 project can add a deny
198
- rule without discarding yours. Everything else is replaced.
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 the real OpenRouter API and cost money:
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
- providers/ OpenRouter, the model catalogue, a scripted provider for tests
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 OpenRouter call: the `live` tests exist
484
- and cover the wire format, tool use and a genuine cache hit, but they need a
485
- key and are deselected by default.
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.
@@ -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 "$@"
@@ -1,5 +1,5 @@
1
1
  """HX - a terminal coding agent."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.5"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -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."""