hx-cli 0.2.5__tar.gz → 0.2.7__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 (243) hide show
  1. {hx_cli-0.2.5 → hx_cli-0.2.7}/.github/workflows/ci.yml +13 -0
  2. {hx_cli-0.2.5 → hx_cli-0.2.7}/.gitignore +1 -0
  3. {hx_cli-0.2.5 → hx_cli-0.2.7}/CHANGELOG.md +111 -2
  4. {hx_cli-0.2.5 → hx_cli-0.2.7}/PKG-INFO +76 -25
  5. {hx_cli-0.2.5 → hx_cli-0.2.7}/README.md +75 -24
  6. {hx_cli-0.2.5 → hx_cli-0.2.7}/pyproject.toml +1 -1
  7. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/__init__.py +1 -1
  8. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/cli.py +218 -12
  9. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/config.py +1 -1
  10. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/compaction.py +6 -0
  11. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/loop.py +15 -11
  12. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/session.py +59 -9
  13. hx_cli-0.2.7/src/hx/mcp/client.py +662 -0
  14. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/mcp/manager.py +208 -22
  15. hx_cli-0.2.7/src/hx/mcp/oauth.py +766 -0
  16. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/permissions/engine.py +1 -1
  17. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/permissions/sandbox.py +84 -22
  18. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/openrouter.py +16 -6
  19. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/keydecode.py +173 -15
  20. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/loop.py +5 -1
  21. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/markdown.py +19 -7
  22. hx_cli-0.2.7/src/hx/term/modifiers.py +116 -0
  23. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/primitives.py +54 -3
  24. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/screen.py +38 -7
  25. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/terminal.py +114 -66
  26. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/registry.py +4 -0
  27. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/app.py +4 -3
  28. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/commands.py +135 -24
  29. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/format.py +22 -0
  30. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/limits.py +4 -0
  31. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/renderers.py +69 -16
  32. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/runtime.py +78 -31
  33. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/blocks.py +49 -6
  34. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/login.py +3 -0
  35. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/permission.py +14 -5
  36. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/pickers.py +79 -15
  37. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/prompt.py +149 -15
  38. hx_cli-0.2.7/tests/conftest.py +28 -0
  39. hx_cli-0.2.7/tests/e2e/conftest.py +302 -0
  40. hx_cli-0.2.7/tests/e2e/report.py +205 -0
  41. hx_cli-0.2.7/tests/e2e/stub.py +363 -0
  42. hx_cli-0.2.7/tests/e2e/terminal.py +473 -0
  43. hx_cli-0.2.7/tests/e2e/test_chat.py +213 -0
  44. hx_cli-0.2.7/tests/e2e/test_cli.py +143 -0
  45. hx_cli-0.2.7/tests/e2e/test_commands.py +255 -0
  46. hx_cli-0.2.7/tests/e2e/test_extend.py +276 -0
  47. hx_cli-0.2.7/tests/e2e/test_input.py +457 -0
  48. hx_cli-0.2.7/tests/e2e/test_print.py +275 -0
  49. hx_cli-0.2.7/tests/e2e/test_tools.py +209 -0
  50. hx_cli-0.2.5/src/hx/mcp/client.py +0 -367
  51. hx_cli-0.2.5/src/hx/providers/fake.py +0 -124
  52. hx_cli-0.2.5/tests/agents/test_subagent.py +0 -310
  53. hx_cli-0.2.5/tests/auth/__init__.py +0 -0
  54. hx_cli-0.2.5/tests/auth/test_codex_oauth.py +0 -226
  55. hx_cli-0.2.5/tests/auth/test_devin_oauth.py +0 -119
  56. hx_cli-0.2.5/tests/auth/test_pkce.py +0 -27
  57. hx_cli-0.2.5/tests/auth/test_resolve.py +0 -166
  58. hx_cli-0.2.5/tests/auth/test_store.py +0 -218
  59. hx_cli-0.2.5/tests/conftest.py +0 -63
  60. hx_cli-0.2.5/tests/core/__init__.py +0 -0
  61. hx_cli-0.2.5/tests/core/test_checkpoints.py +0 -200
  62. hx_cli-0.2.5/tests/core/test_compaction.py +0 -137
  63. hx_cli-0.2.5/tests/core/test_context.py +0 -158
  64. hx_cli-0.2.5/tests/core/test_images.py +0 -281
  65. hx_cli-0.2.5/tests/core/test_lateinject.py +0 -52
  66. hx_cli-0.2.5/tests/core/test_loop.py +0 -738
  67. hx_cli-0.2.5/tests/core/test_loop_images.py +0 -181
  68. hx_cli-0.2.5/tests/core/test_rewind.py +0 -206
  69. hx_cli-0.2.5/tests/core/test_session.py +0 -264
  70. hx_cli-0.2.5/tests/core/test_settings_layers.py +0 -67
  71. hx_cli-0.2.5/tests/core/test_settings_migration.py +0 -152
  72. hx_cli-0.2.5/tests/core/test_steering.py +0 -203
  73. hx_cli-0.2.5/tests/core/test_title.py +0 -70
  74. hx_cli-0.2.5/tests/core/test_usage.py +0 -25
  75. hx_cli-0.2.5/tests/hooks/__init__.py +0 -0
  76. hx_cli-0.2.5/tests/hooks/test_engine.py +0 -205
  77. hx_cli-0.2.5/tests/hooks/test_example_check.py +0 -98
  78. hx_cli-0.2.5/tests/mcp/__init__.py +0 -0
  79. hx_cli-0.2.5/tests/mcp/test_manager.py +0 -240
  80. hx_cli-0.2.5/tests/permissions/__init__.py +0 -0
  81. hx_cli-0.2.5/tests/permissions/test_engine.py +0 -570
  82. hx_cli-0.2.5/tests/permissions/test_parser.py +0 -202
  83. hx_cli-0.2.5/tests/permissions/test_sandbox.py +0 -189
  84. hx_cli-0.2.5/tests/providers/__init__.py +0 -0
  85. hx_cli-0.2.5/tests/providers/test_catalogue.py +0 -349
  86. hx_cli-0.2.5/tests/providers/test_codex.py +0 -113
  87. hx_cli-0.2.5/tests/providers/test_codex_catalogue.py +0 -205
  88. hx_cli-0.2.5/tests/providers/test_devin.py +0 -489
  89. hx_cli-0.2.5/tests/providers/test_devin_catalogue.py +0 -229
  90. hx_cli-0.2.5/tests/providers/test_openrouter.py +0 -308
  91. hx_cli-0.2.5/tests/providers/test_protowire.py +0 -100
  92. hx_cli-0.2.5/tests/providers/test_registry.py +0 -117
  93. hx_cli-0.2.5/tests/providers/test_responses_codec.py +0 -330
  94. hx_cli-0.2.5/tests/skills/__init__.py +0 -0
  95. hx_cli-0.2.5/tests/skills/test_loader.py +0 -157
  96. hx_cli-0.2.5/tests/term/__init__.py +0 -0
  97. hx_cli-0.2.5/tests/term/conftest.py +0 -59
  98. hx_cli-0.2.5/tests/term/test_ansi.py +0 -190
  99. hx_cli-0.2.5/tests/term/test_keydecode.py +0 -233
  100. hx_cli-0.2.5/tests/term/test_loop.py +0 -221
  101. hx_cli-0.2.5/tests/term/test_markdown.py +0 -260
  102. hx_cli-0.2.5/tests/term/test_primitives.py +0 -206
  103. hx_cli-0.2.5/tests/term/test_sanitize.py +0 -77
  104. hx_cli-0.2.5/tests/term/test_screen.py +0 -673
  105. hx_cli-0.2.5/tests/term/test_terminal.py +0 -321
  106. hx_cli-0.2.5/tests/term/test_width.py +0 -109
  107. hx_cli-0.2.5/tests/test_cli.py +0 -264
  108. hx_cli-0.2.5/tests/test_docs.py +0 -203
  109. hx_cli-0.2.5/tests/test_git.py +0 -355
  110. hx_cli-0.2.5/tests/test_install.py +0 -55
  111. hx_cli-0.2.5/tests/test_integration.py +0 -696
  112. hx_cli-0.2.5/tests/test_net.py +0 -110
  113. hx_cli-0.2.5/tests/test_paths.py +0 -54
  114. hx_cli-0.2.5/tests/test_readme.py +0 -309
  115. hx_cli-0.2.5/tests/test_trace.py +0 -538
  116. hx_cli-0.2.5/tests/tools/__init__.py +0 -0
  117. hx_cli-0.2.5/tests/tools/test_background.py +0 -207
  118. hx_cli-0.2.5/tests/tools/test_bash.py +0 -210
  119. hx_cli-0.2.5/tests/tools/test_files.py +0 -284
  120. hx_cli-0.2.5/tests/tools/test_hashline.py +0 -197
  121. hx_cli-0.2.5/tests/tools/test_output.py +0 -31
  122. hx_cli-0.2.5/tests/tools/test_symbols.py +0 -127
  123. hx_cli-0.2.5/tests/tools/test_todo.py +0 -104
  124. hx_cli-0.2.5/tests/tools/test_websearch.py +0 -296
  125. hx_cli-0.2.5/tests/tui/__init__.py +0 -0
  126. hx_cli-0.2.5/tests/tui/support.py +0 -146
  127. hx_cli-0.2.5/tests/tui/test_autocomplete.py +0 -186
  128. hx_cli-0.2.5/tests/tui/test_clipboard.py +0 -111
  129. hx_cli-0.2.5/tests/tui/test_effort.py +0 -167
  130. hx_cli-0.2.5/tests/tui/test_images.py +0 -319
  131. hx_cli-0.2.5/tests/tui/test_keys.py +0 -136
  132. hx_cli-0.2.5/tests/tui/test_login.py +0 -424
  133. hx_cli-0.2.5/tests/tui/test_renderers.py +0 -204
  134. hx_cli-0.2.5/tests/tui/test_session.py +0 -1134
  135. hx_cli-0.2.5/tests/tui/test_theme_json.py +0 -105
  136. hx_cli-0.2.5/tests/tui/test_trace_command.py +0 -257
  137. hx_cli-0.2.5/tests/tui/views/__init__.py +0 -0
  138. hx_cli-0.2.5/tests/tui/views/test_blocks.py +0 -264
  139. hx_cli-0.2.5/tests/tui/views/test_permission.py +0 -328
  140. hx_cli-0.2.5/tests/tui/views/test_pickers.py +0 -314
  141. hx_cli-0.2.5/tests/tui/views/test_prompt.py +0 -374
  142. hx_cli-0.2.5/tests/tui/views/test_runtime.py +0 -567
  143. hx_cli-0.2.5/tests/tui/views/test_status.py +0 -197
  144. hx_cli-0.2.5/tests/tui/views/test_transcript.py +0 -211
  145. {hx_cli-0.2.5 → hx_cli-0.2.7}/examples/hooks/check.sh +0 -0
  146. {hx_cli-0.2.5 → hx_cli-0.2.7}/install.sh +0 -0
  147. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/agents/__init__.py +0 -0
  148. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/agents/definitions.py +0 -0
  149. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/agents/subagent.py +0 -0
  150. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/__init__.py +0 -0
  151. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/oauth/__init__.py +0 -0
  152. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/oauth/browser.py +0 -0
  153. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/oauth/callback.py +0 -0
  154. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/oauth/codex.py +0 -0
  155. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/oauth/devin.py +0 -0
  156. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/oauth/pkce.py +0 -0
  157. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/resolve.py +0 -0
  158. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/store.py +0 -0
  159. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/__init__.py +0 -0
  160. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/checkpoints.py +0 -0
  161. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/context.py +0 -0
  162. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/events.py +0 -0
  163. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/images.py +0 -0
  164. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/lateinject.py +0 -0
  165. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/messages.py +0 -0
  166. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/title.py +0 -0
  167. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/usage.py +0 -0
  168. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/docs.py +0 -0
  169. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/frontmatter.py +0 -0
  170. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/git.py +0 -0
  171. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/hooks/__init__.py +0 -0
  172. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/hooks/engine.py +0 -0
  173. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/hooks/spec.py +0 -0
  174. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/keys.py +0 -0
  175. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/mcp/__init__.py +0 -0
  176. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/net.py +0 -0
  177. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/paths.py +0 -0
  178. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/permissions/__init__.py +0 -0
  179. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/permissions/parser.py +0 -0
  180. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/__init__.py +0 -0
  181. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/base.py +0 -0
  182. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/codex.py +0 -0
  183. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/codex_catalogue.py +0 -0
  184. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/devin.py +0 -0
  185. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/devin_catalogue.py +0 -0
  186. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/devin_wire.py +0 -0
  187. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/models.py +0 -0
  188. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/protowire.py +0 -0
  189. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/registry.py +0 -0
  190. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/responses_codec.py +0 -0
  191. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/skills/__init__.py +0 -0
  192. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/skills/loader.py +0 -0
  193. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/skills/runtime.py +0 -0
  194. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/__init__.py +0 -0
  195. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/ansi.py +0 -0
  196. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/buffer.py +0 -0
  197. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/component.py +0 -0
  198. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/demo.py +0 -0
  199. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/editor.py +0 -0
  200. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/sanitize.py +0 -0
  201. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/syntax.py +0 -0
  202. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/undo.py +0 -0
  203. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/width.py +0 -0
  204. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/word_nav.py +0 -0
  205. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/__init__.py +0 -0
  206. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/anchors.py +0 -0
  207. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/base.py +0 -0
  208. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/bash.py +0 -0
  209. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/edit.py +0 -0
  210. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/glob.py +0 -0
  211. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/grep.py +0 -0
  212. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/output.py +0 -0
  213. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/read.py +0 -0
  214. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/symbols.py +0 -0
  215. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/task.py +0 -0
  216. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/todo.py +0 -0
  217. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/websearch.py +0 -0
  218. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/write.py +0 -0
  219. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/trace.py +0 -0
  220. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/__init__.py +0 -0
  221. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/clipboard.py +0 -0
  222. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/fuzzy.py +0 -0
  223. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/glyphs.py +0 -0
  224. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/killring.py +0 -0
  225. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/paint.py +0 -0
  226. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/roles.py +0 -0
  227. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/theme.py +0 -0
  228. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/theme_json.py +0 -0
  229. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/themes/ansi.json +0 -0
  230. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/themes/dark.json +0 -0
  231. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/themes/light.json +0 -0
  232. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/__init__.py +0 -0
  233. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/dialog.py +0 -0
  234. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/status.py +0 -0
  235. {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/transcript.py +0 -0
  236. {hx_cli-0.2.5 → hx_cli-0.2.7}/tests/__init__.py +0 -0
  237. {hx_cli-0.2.5/tests/agents → hx_cli-0.2.7/tests/e2e}/__init__.py +0 -0
  238. {hx_cli-0.2.5/tests/mcp → hx_cli-0.2.7/tests/e2e}/fixtures/echo_server.py +0 -0
  239. {hx_cli-0.2.5 → hx_cli-0.2.7}/tests/test_live.py +0 -0
  240. {hx_cli-0.2.5 → hx_cli-0.2.7}/tests/test_live_codex.py +0 -0
  241. {hx_cli-0.2.5 → hx_cli-0.2.7}/tests/test_live_devin.py +0 -0
  242. {hx_cli-0.2.5 → hx_cli-0.2.7}/tests/test_live_tavily.py +0 -0
  243. {hx_cli-0.2.5 → hx_cli-0.2.7}/uv.lock +0 -0
@@ -36,8 +36,21 @@ jobs:
36
36
  - uses: actions/checkout@v4
37
37
  - uses: astral-sh/setup-uv@v3
38
38
  - run: uv sync --extra dev --python ${{ matrix.python }}
39
+ # bubblewrap, so the Linux sandbox tests run instead of skipping. Ubuntu
40
+ # 24.04 confines unprivileged user namespaces with AppArmor, which bwrap
41
+ # needs; the runner is throwaway, so lift that rather than ship a profile.
42
+ - if: runner.os == 'Linux'
43
+ run: |
44
+ sudo apt-get update && sudo apt-get install -y bubblewrap
45
+ sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
39
46
  # `live` needs an API key and real spend, so it never runs in CI.
40
47
  - run: uv run pytest -m "not live"
48
+ # What every end-to-end test saw, screen by screen - pass or fail.
49
+ - uses: actions/upload-artifact@v4
50
+ if: always()
51
+ with:
52
+ name: e2e-report-${{ matrix.os }}-${{ matrix.python }}
53
+ path: e2e-report/
41
54
 
42
55
  install-script:
43
56
  runs-on: ubuntu-latest
@@ -17,3 +17,4 @@ htmlcov/
17
17
  # and the rest - lives under $HX_HOME/projects/<slug>, outside the checkout
18
18
  # entirely, so there is nothing here left to hide.
19
19
  snapshot_report.html
20
+ e2e-report/
@@ -7,7 +7,7 @@ wheel, `hx changelog` prints it, and a session can answer "what's new?" or
7
7
  "which version added web search?" from it rather than from a guess. `hx docs`
8
8
  prints the manual next to it.
9
9
 
10
- The rules, enforced by `tests/test_docs.py` so a release cannot forget them:
10
+ The rules, enforced by `tests/e2e/test_cli.py` so a release cannot forget them:
11
11
 
12
12
  - Every released version has a `## [x.y.z] - YYYY-MM-DD` section here, and the
13
13
  version in `src/hx/__init__.py` always has one. Bumping the version without
@@ -28,6 +28,113 @@ the project follows [semantic versioning](https://semver.org/).
28
28
 
29
29
  ---
30
30
 
31
+ ## [0.2.7] - 2026-09-27
32
+
33
+ ### Added
34
+
35
+ - `HX_OPENROUTER_BASE_URL` points the OpenRouter route at any OpenAI-compatible
36
+ endpoint - a gateway, a recording proxy.
37
+ - Pressing `esc` on a running turn leaves an "Interrupted" line in the transcript,
38
+ so a reply cut off mid-sentence no longer reads as a finished one.
39
+ - A paste over 10 lines or 1000 characters shows in the prompt as
40
+ `[Pasted text #1 +40 lines]` and is sent in full; the token moves and deletes
41
+ as one character, and survives history recall and `ctrl+k`/`ctrl+y`.
42
+ - `\` then `enter` inserts a newline in any terminal, including Apple's Terminal
43
+ and tmux without extended keys, where `shift+enter` cannot be told from `enter`. `\\` then
44
+ `enter` sends a message that ends in a backslash.
45
+
46
+ ### Fixed
47
+
48
+ - `shift+enter` inserts a newline instead of sending the message in kitty, Ghostty,
49
+ WezTerm, iTerm2 and tmux (with `extended-keys on`): HX now asks the terminal for
50
+ the kitty keyboard protocol, falling back to xterm's modifyOtherKeys, and hands
51
+ it back on exit, `ctrl+z` and `/fullscreen`. `ctrl+shift+z` (redo) works there too.
52
+ - `shift+enter` also inserts a newline on macOS in terminals that send it as a
53
+ plain `enter` (VS Code's, Apple's Terminal) or as `option+enter` (the Claude
54
+ desktop app's terminal panel): HX asks macOS which modifiers are held when the
55
+ key arrives. A real `option+enter` still steers, an `enter` hard on the heels of
56
+ a shifted character (`?` then `enter`) still sends, and `HX_MODIFIER_PROBE=0`
57
+ turns the check off.
58
+ - A multi-line paste keeps its line breaks. Terminals send them as carriage
59
+ returns, which HX was deleting, so a pasted table arrived as one long line.
60
+ - Lines typed with `shift+enter` stay separate lines in the transcript instead of
61
+ being joined into one paragraph.
62
+ - On macOS the sandbox now refuses *writes* to credential paths (`~/.ssh`,
63
+ `~/.aws`, `~/.hx/auth.json`, ...) as well as reads, so a session opened in your
64
+ home directory cannot plant an `authorized_keys` entry.
65
+ - On Linux the same holds for a credential path that does not exist yet: a
66
+ session opened in your home directory with no `~/.ssh` could create one and
67
+ plant an `authorized_keys` on the host. HX now creates such a path, empty and
68
+ private (`0700` for a directory, `0600` for a file), and mounts a read-only
69
+ shadow over it. Existing credential directories are shadowed read-only too, so
70
+ a write fails with "Read-only file system" instead of vanishing.
71
+ - Shell commands the model runs show their output in the transcript again - the
72
+ last lines, with `ctrl+o` for the rest - instead of only "Took 12ms".
73
+ - `/context` no longer fails with "'UsageLedger' object has no attribute
74
+ 'total_tokens'"; it lists what fills the window.
75
+ - `/copy` copies the last reply and says how it was copied, instead of failing.
76
+ - `/theme light` reports "Theme: light" rather than "Theme: None".
77
+ - `/compact` on a short session says only that there is nothing to compact, no
78
+ longer followed by a "Compacting…" that never finishes; a compaction that
79
+ saves nothing still reports that it finished, and the context gauge keeps
80
+ counting the system prompt afterwards.
81
+ - `/help` lines its key descriptions up however long a binding is, and a long
82
+ row in `/agents`, `/cost` or any other command output wraps under its own
83
+ column instead of at the left edge.
84
+ - The "always allow" option on an approval names where the rule is really
85
+ saved (`~/.hx`), and the answered approval shows a file relative to the
86
+ project rather than as an absolute path.
87
+ - `/permissions` no longer says the same thing twice.
88
+
89
+ ---
90
+
91
+ ## [0.2.6] - 2026-09-23
92
+
93
+ ### Added
94
+
95
+ - Sign in to remote MCP servers over OAuth. A server that asks for it -
96
+ Atlassian's `https://mcp.atlassian.com/v2/mcp`, for one - shows as needing
97
+ sign-in; `/mcp login <server>` or `hx mcp login <server>` opens the browser
98
+ and the tools arrive without a restart. Tokens refresh on their own;
99
+ `/mcp logout <server>` forgets them. `"oauth": {"clientId": ...}` in
100
+ `mcp.json` covers servers without client registration.
101
+ - MCP servers that change their tool list mid-session have the new list
102
+ swapped in, whether they announce it in a reply or on their own stream.
103
+ - `hx mcp add NAME --url URL --header "Name: value"` saves headers, such as an
104
+ API token, with the server.
105
+ - `/mcp reconnect <server>` restarts one server without restarting HX.
106
+ - `/resume` always offers a list to switch to: this directory's sessions
107
+ first, then every other directory's, each row naming where it lives.
108
+ Picking one from elsewhere restarts HX in that directory on it, with the
109
+ same `--mode` and `--model`. The session already open is no longer listed,
110
+ and the list is no longer cut at 20.
111
+
112
+ ### Fixed
113
+
114
+ - `/resume` said "No previous sessions in this directory" in a fresh
115
+ directory, or inside a session resumed from another one, instead of
116
+ listing anything.
117
+ - `hx resume <id>` ran the session in whatever directory you typed it in,
118
+ not the one it was recorded in, so its paths and tool calls pointed at the
119
+ wrong project.
120
+ - `hx mcp add NAME --url URL`, `--user`, and server arguments like `npx -y`
121
+ were refused as unknown options, so none of them could be run.
122
+ - `hx mcp add` dropped headers from the entry it wrote.
123
+ - A remote MCP server that expires its session is initialized again instead
124
+ of failing every call until HX restarts.
125
+ - A multi-line shell command no longer breaks the screen. Its approval shows
126
+ one row per line, the tool call shows the first line and `+N lines`
127
+ (`ctrl+o` shows the rest), and the approval options and prompt are no longer
128
+ drawn several times over.
129
+ - `/resume` from inside a resumed session showed the resumed conversation
130
+ pushed off the top of the screen, leaving only the "Resumed" notice.
131
+ - A session title with a line break in it no longer splits its row in the
132
+ `/resume` picker.
133
+ - Started in your home directory, the status bar named it `~/.`; it now reads
134
+ `~`.
135
+
136
+ ---
137
+
31
138
  ## [0.2.5] - 2026-09-23
32
139
 
33
140
  ### Added
@@ -592,7 +699,9 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
592
699
  - `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
593
700
  self-update, and `install.sh` bootstrapping uv with a pinned Python.
594
701
 
595
- [Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.5...HEAD
702
+ [Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.7...HEAD
703
+ [0.2.7]: https://github.com/aletisunil/hx/compare/v0.2.6...v0.2.7
704
+ [0.2.6]: https://github.com/aletisunil/hx/compare/v0.2.5...v0.2.6
596
705
  [0.2.5]: https://github.com/aletisunil/hx/compare/v0.2.4...v0.2.5
597
706
  [0.2.4]: https://github.com/aletisunil/hx/compare/v0.2.3...v0.2.4
598
707
  [0.2.3]: https://github.com/aletisunil/hx/compare/v0.2.2...v0.2.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hx-cli
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: HX - an agent harness for the terminal
5
5
  Project-URL: Homepage, https://hx.sunilaleti.dev
6
6
  Project-URL: Documentation, https://hx.sunilaleti.dev
@@ -299,7 +299,7 @@ hx # interactive TUI in the current directory
299
299
  hx -p "explain this repo" # headless: streams to stdout, tool activity to stderr
300
300
  hx -p "why?" --image s.png # attach an image to a headless prompt; repeatable
301
301
  hx resume # resume the last session here
302
- hx resume <session-id> # resume a specific one
302
+ hx resume <session-id> # resume a specific one, in the directory it was recorded in
303
303
  hx prompt # print the system prompt this directory would use
304
304
  hx trace # write the last session here to an HTML page
305
305
  hx trace out.html # the same, to a path you choose
@@ -311,6 +311,8 @@ hx --no-sandbox # disable OS sandboxing (rules still apply)
311
311
  hx --system-prompt @p.md # replace the system prompt for one run
312
312
  hx --append-system-prompt "Always run the tests" # add to it; repeatable
313
313
  hx mcp list|add|remove # manage MCP servers
314
+ hx mcp login <server> # sign in to a remote MCP server in the browser
315
+ hx mcp logout <server> # forget a remote MCP server's sign-in
314
316
  hx auth [set|clear] # manage the OpenRouter key
315
317
  hx auth login [provider] # sign in (openrouter, openai-codex, devin)
316
318
  hx auth logout <provider> # forget a stored credential
@@ -348,7 +350,7 @@ allowlist it if you would rather not be asked:
348
350
  ```
349
351
 
350
352
  `CHANGELOG.md` lists what each version added, and a release cannot skip it:
351
- `tests/test_docs.py` fails when `__version__` has no section (see
353
+ `tests/e2e/test_cli.py` fails when `__version__` has no section (see
352
354
  [Releasing](#releasing)).
353
355
 
354
356
  ### Keys
@@ -360,7 +362,7 @@ be rebound (see [Keybindings](#keybindings)).
360
362
  | Key | Does |
361
363
  |---|---|
362
364
  | `enter` | send |
363
- | `ctrl+j` | newline (also `shift+enter`) |
365
+ | `shift+enter` | newline (also `ctrl+j`; in a terminal that cannot send `shift+enter`, end the line with `\` and press `enter`) |
364
366
  | `esc` | interrupt the current turn |
365
367
  | `ctrl+c` | copy the selected text; with nothing selected, clear the prompt (twice on an empty prompt exits) |
366
368
  | `ctrl+d` | exit, when the prompt is empty |
@@ -478,14 +480,14 @@ than being silently resolved.
478
480
  | `/cost` | tokens, cache savings, spend |
479
481
  | `/compact [focus]` | summarise older turns now |
480
482
  | `/clear` | fresh session, same directory |
481
- | `/resume` | reopen a previous session, listed by name |
483
+ | `/resume` | switch to another session - this directory's first, then the rest |
482
484
  | `/rewind` | go back to an earlier prompt, restoring the files HX changed |
483
485
  | `/title [text]` | show or set this session's name |
484
486
  | `/prompt` | the system prompt this session is running with |
485
487
  | `/todos` | show the current plan |
486
488
  | `/skills` | installed skills |
487
489
  | `/agents` | subagent types |
488
- | `/mcp` | server status |
490
+ | `/mcp [login\|logout\|reconnect <server>]` | server status, and signing in to remote servers |
489
491
  | `/hooks` | configured hooks, and any that were refused |
490
492
  | `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
491
493
  | `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
@@ -662,8 +664,11 @@ merge: it is read per layer, and only from your own settings. See
662
664
  Environment overrides: `HX_MODEL`, `HX_SUBAGENT_MODEL`, `HX_MAX_TOKENS`,
663
665
  `HX_PERMISSION_MODE`, `HX_SANDBOX`, `HX_COMPACT_AT`, `HX_GIT_NOTICES`,
664
666
  `HX_THEME`, `HX_QUIET_STARTUP`. Also `HX_HOME` to relocate user state, `HX_TAVILY_API_KEY`
665
- for web search, and `HX_CA_BUNDLE` / `HX_SSL_NO_VERIFY` for TLS (see
666
- [Credentials](#credentials)).
667
+ for web search, `HX_CA_BUNDLE` / `HX_SSL_NO_VERIFY` for TLS (see
668
+ [Credentials](#credentials)), and `HX_OPENROUTER_BASE_URL` to point the OpenRouter
669
+ route at another OpenAI-compatible endpoint - a gateway, or a recording proxy.
670
+ `HX_MODIFIER_PROBE=0` stops HX asking macOS whether shift is held when an
671
+ `enter` arrives (see [Keys](#keys)).
667
672
 
668
673
  ### The system prompt
669
674
 
@@ -701,6 +706,13 @@ if you want a cheaper model for it — and it is counted in `/cost` like any oth
701
706
  If the call fails the session is still named, from your first message. `/title
702
707
  <text>` renames it.
703
708
 
709
+ `/resume` lists every session except the one you are in: this directory's
710
+ first, then other directories', each row naming where it lives. A session from
711
+ this directory is swapped in place. One from elsewhere belongs to that
712
+ directory - its sandbox, permission rules, prompt and tools - so HX restarts
713
+ itself there with `hx resume <id>`, keeping options like `--mode` and
714
+ `--model`.
715
+
704
716
  ### Themes
705
717
 
706
718
  A theme is a JSON file: a `vars` block of raw colours, and a `colors` block
@@ -765,7 +777,8 @@ confidence prompts rather than passing.
765
777
  **An OS sandbox** wraps command execution: Seatbelt on macOS, bubblewrap on
766
778
  Linux. The filesystem is readable, writes are confined to the project and the
767
779
  temp dir, credential paths (`~/.ssh`, `~/.aws`, and HX's own `auth.json`) are
768
- unreadable, and outbound network is off. If neither backend is present the
780
+ unreadable and unwritable - even inside the project, and even before they
781
+ exist - and outbound network is off. If neither backend is present the
769
782
  status bar says `no-sandbox` rather than implying protection that is not there.
770
783
 
771
784
  Modes cycle with shift+tab: `plan` (read-only — mutating tools are not even
@@ -996,10 +1009,41 @@ construction.
996
1009
  }
997
1010
  ```
998
1011
 
999
- Or `hx mcp add local python server.py`. Tools arrive namespaced
1012
+ Or `hx mcp add local python server.py`, or
1013
+ `hx mcp add remote --url https://example.com/mcp`. Tools arrive namespaced
1000
1014
  `mcp__<server>__<tool>` in a deterministic order. Servers connect concurrently
1001
1015
  with a per-server timeout; one that is broken or slow logs a warning and is
1002
- dropped rather than taking the session with it.
1016
+ dropped rather than taking the session with it. A server that changes its tool
1017
+ list mid-session (`notifications/tools/list_changed`) has the new list swapped
1018
+ in without a restart.
1019
+
1020
+ A remote server that asks for sign-in - Atlassian's, for one - is signed in to
1021
+ over OAuth, with nothing to configure:
1022
+
1023
+ ```json
1024
+ { "mcpServers": { "atlassian": { "url": "https://mcp.atlassian.com/v2/mcp" } } }
1025
+ ```
1026
+
1027
+ It shows as needing sign-in until you run `/mcp login atlassian` in a session
1028
+ or `hx mcp login atlassian` in a shell. That opens the browser, and the tools
1029
+ arrive as soon as you approve - no restart. HX follows the MCP authorization
1030
+ spec: it finds the authorization server from the server's metadata, registers
1031
+ itself as a client, signs in with PKCE, and keeps the token in `~/.hx/auth.json`
1032
+ (mode 0600), refreshing it before it expires and again if the server rejects
1033
+ it. Over SSH, paste the final redirect URL into the prompt instead.
1034
+ `/mcp logout atlassian` forgets it.
1035
+
1036
+ A server that does not offer client registration needs a client you register
1037
+ with it yourself, and anything else about the sign-in can be pinned the same
1038
+ way:
1039
+
1040
+ ```json
1041
+ "oauth": { "clientId": "...", "clientSecret": "...", "scope": "read write", "callbackPort": 33418 }
1042
+ ```
1043
+
1044
+ To send a token of your own instead - an API key - give the server an
1045
+ `Authorization` header, and OAuth stays out of the way:
1046
+ `hx mcp add jira --url https://mcp.atlassian.com/v2/mcp --header "Authorization: Basic <base64 email:token>"`.
1003
1047
 
1004
1048
  **Hooks** are shell commands HX runs at named points in a turn. Four events:
1005
1049
  `PreToolUse`, `PostToolUse`, `UserPromptSubmit` and `Stop`.
@@ -1077,6 +1121,23 @@ uv run mypy # strict
1077
1121
  uv run hx # run from the checkout
1078
1122
  ```
1079
1123
 
1124
+ The suite is end-to-end. Each test in `tests/e2e/` starts the installed `hx`
1125
+ binary - on a real pseudo-terminal for the TUI, or as a plain subprocess for
1126
+ print mode and the other commands - in a private `$HOME` and `$HX_HOME`, and
1127
+ points it at a local OpenRouter stand-in with `HX_OPENROUTER_BASE_URL`.
1128
+ Everything between the keyboard and the socket is the shipped code; only the
1129
+ model's replies are scripted. The screen is read back through a VT emulator,
1130
+ so assertions are on the cells a terminal shows, not on the bytes HX meant to
1131
+ write.
1132
+
1133
+ Every run writes `e2e-report/`: `index.html` shows each test's outcome and
1134
+ every screen it checked, rendered in colour, and one text file per screen lets
1135
+ two runs be compared with `diff -r`. CI uploads it from every test job as an
1136
+ `e2e-report-*` artifact. Set `HX_E2E_REPORT` to write it somewhere else.
1137
+
1138
+ Tests that need the OS sandbox are skipped where there is no backend
1139
+ (`sandbox-exec` on macOS, `bwrap` on Linux).
1140
+
1080
1141
  Tests marked `live` hit a real API. The OpenRouter ones cost money:
1081
1142
 
1082
1143
  ```sh
@@ -1093,17 +1154,7 @@ uv run pytest -m live tests/test_live_devin.py
1093
1154
 
1094
1155
  They are the only place the wire formats, streaming, tool use and a genuine
1095
1156
  cache hit are proven against a real service; everything else runs against the
1096
- scripted provider.
1097
-
1098
- Tests marked `sandbox` exercise the real OS sandbox and are skipped where no
1099
- backend exists.
1100
-
1101
- The TUI has SVG layout snapshots. Read the diff before accepting a change to
1102
- them - they exist to catch a frame that quietly lost a row:
1103
-
1104
- ```sh
1105
- uv run pytest tests/tui/test_snapshots.py --snapshot-update
1106
- ```
1157
+ stand-in.
1107
1158
 
1108
1159
  ### Layout
1109
1160
 
@@ -1112,7 +1163,7 @@ src/hx/
1112
1163
  cli.py config.py paths.py frontmatter.py git.py
1113
1164
  core/ loop, context assembly, compaction, late injection, sessions, usage
1114
1165
  auth/ credential store, OAuth flows, per-route resolution
1115
- providers/ OpenRouter, Codex, Devin, the model catalogue, a scripted provider for tests
1166
+ providers/ OpenRouter, Codex, Devin, the model catalogue
1116
1167
  tools/ Bash, Read, Write, Edit, Glob, Grep, Symbols, TodoWrite, Task,
1117
1168
  WebSearch, WebFetch, line anchors, output capping
1118
1169
  permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
@@ -1149,12 +1200,12 @@ Each release:
1149
1200
  # 1. move CHANGELOG.md's [Unreleased] entries under the new version, dated,
1150
1201
  # and leave a fresh empty [Unreleased] behind
1151
1202
  # 2. bump __version__ in src/hx/__init__.py
1152
- # 3. uv run pytest tests/test_docs.py # the version must have a section
1203
+ # 3. uv run pytest tests/e2e/test_cli.py # the version must have a section
1153
1204
  git commit -am "release: X.Y.Z"
1154
1205
  git tag v$(uv run hx --version | cut -d' ' -f2) && git push origin main --tags
1155
1206
  ```
1156
1207
 
1157
- Step 1 is not optional and is not a convention: `tests/test_docs.py` fails the
1208
+ Step 1 is not optional and is not a convention: `tests/e2e/test_cli.py` fails the
1158
1209
  whole suite when `__version__` has no `CHANGELOG.md` section, so a release that
1159
1210
  skips it cannot get past CI to the `publish` job. The file ships inside the
1160
1211
  wheel and `hx changelog` prints it, so a version with no section there reaches
@@ -259,7 +259,7 @@ hx # interactive TUI in the current directory
259
259
  hx -p "explain this repo" # headless: streams to stdout, tool activity to stderr
260
260
  hx -p "why?" --image s.png # attach an image to a headless prompt; repeatable
261
261
  hx resume # resume the last session here
262
- hx resume <session-id> # resume a specific one
262
+ hx resume <session-id> # resume a specific one, in the directory it was recorded in
263
263
  hx prompt # print the system prompt this directory would use
264
264
  hx trace # write the last session here to an HTML page
265
265
  hx trace out.html # the same, to a path you choose
@@ -271,6 +271,8 @@ hx --no-sandbox # disable OS sandboxing (rules still apply)
271
271
  hx --system-prompt @p.md # replace the system prompt for one run
272
272
  hx --append-system-prompt "Always run the tests" # add to it; repeatable
273
273
  hx mcp list|add|remove # manage MCP servers
274
+ hx mcp login <server> # sign in to a remote MCP server in the browser
275
+ hx mcp logout <server> # forget a remote MCP server's sign-in
274
276
  hx auth [set|clear] # manage the OpenRouter key
275
277
  hx auth login [provider] # sign in (openrouter, openai-codex, devin)
276
278
  hx auth logout <provider> # forget a stored credential
@@ -308,7 +310,7 @@ allowlist it if you would rather not be asked:
308
310
  ```
309
311
 
310
312
  `CHANGELOG.md` lists what each version added, and a release cannot skip it:
311
- `tests/test_docs.py` fails when `__version__` has no section (see
313
+ `tests/e2e/test_cli.py` fails when `__version__` has no section (see
312
314
  [Releasing](#releasing)).
313
315
 
314
316
  ### Keys
@@ -320,7 +322,7 @@ be rebound (see [Keybindings](#keybindings)).
320
322
  | Key | Does |
321
323
  |---|---|
322
324
  | `enter` | send |
323
- | `ctrl+j` | newline (also `shift+enter`) |
325
+ | `shift+enter` | newline (also `ctrl+j`; in a terminal that cannot send `shift+enter`, end the line with `\` and press `enter`) |
324
326
  | `esc` | interrupt the current turn |
325
327
  | `ctrl+c` | copy the selected text; with nothing selected, clear the prompt (twice on an empty prompt exits) |
326
328
  | `ctrl+d` | exit, when the prompt is empty |
@@ -438,14 +440,14 @@ than being silently resolved.
438
440
  | `/cost` | tokens, cache savings, spend |
439
441
  | `/compact [focus]` | summarise older turns now |
440
442
  | `/clear` | fresh session, same directory |
441
- | `/resume` | reopen a previous session, listed by name |
443
+ | `/resume` | switch to another session - this directory's first, then the rest |
442
444
  | `/rewind` | go back to an earlier prompt, restoring the files HX changed |
443
445
  | `/title [text]` | show or set this session's name |
444
446
  | `/prompt` | the system prompt this session is running with |
445
447
  | `/todos` | show the current plan |
446
448
  | `/skills` | installed skills |
447
449
  | `/agents` | subagent types |
448
- | `/mcp` | server status |
450
+ | `/mcp [login\|logout\|reconnect <server>]` | server status, and signing in to remote servers |
449
451
  | `/hooks` | configured hooks, and any that were refused |
450
452
  | `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
451
453
  | `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
@@ -622,8 +624,11 @@ merge: it is read per layer, and only from your own settings. See
622
624
  Environment overrides: `HX_MODEL`, `HX_SUBAGENT_MODEL`, `HX_MAX_TOKENS`,
623
625
  `HX_PERMISSION_MODE`, `HX_SANDBOX`, `HX_COMPACT_AT`, `HX_GIT_NOTICES`,
624
626
  `HX_THEME`, `HX_QUIET_STARTUP`. Also `HX_HOME` to relocate user state, `HX_TAVILY_API_KEY`
625
- for web search, and `HX_CA_BUNDLE` / `HX_SSL_NO_VERIFY` for TLS (see
626
- [Credentials](#credentials)).
627
+ for web search, `HX_CA_BUNDLE` / `HX_SSL_NO_VERIFY` for TLS (see
628
+ [Credentials](#credentials)), and `HX_OPENROUTER_BASE_URL` to point the OpenRouter
629
+ route at another OpenAI-compatible endpoint - a gateway, or a recording proxy.
630
+ `HX_MODIFIER_PROBE=0` stops HX asking macOS whether shift is held when an
631
+ `enter` arrives (see [Keys](#keys)).
627
632
 
628
633
  ### The system prompt
629
634
 
@@ -661,6 +666,13 @@ if you want a cheaper model for it — and it is counted in `/cost` like any oth
661
666
  If the call fails the session is still named, from your first message. `/title
662
667
  <text>` renames it.
663
668
 
669
+ `/resume` lists every session except the one you are in: this directory's
670
+ first, then other directories', each row naming where it lives. A session from
671
+ this directory is swapped in place. One from elsewhere belongs to that
672
+ directory - its sandbox, permission rules, prompt and tools - so HX restarts
673
+ itself there with `hx resume <id>`, keeping options like `--mode` and
674
+ `--model`.
675
+
664
676
  ### Themes
665
677
 
666
678
  A theme is a JSON file: a `vars` block of raw colours, and a `colors` block
@@ -725,7 +737,8 @@ confidence prompts rather than passing.
725
737
  **An OS sandbox** wraps command execution: Seatbelt on macOS, bubblewrap on
726
738
  Linux. The filesystem is readable, writes are confined to the project and the
727
739
  temp dir, credential paths (`~/.ssh`, `~/.aws`, and HX's own `auth.json`) are
728
- unreadable, and outbound network is off. If neither backend is present the
740
+ unreadable and unwritable - even inside the project, and even before they
741
+ exist - and outbound network is off. If neither backend is present the
729
742
  status bar says `no-sandbox` rather than implying protection that is not there.
730
743
 
731
744
  Modes cycle with shift+tab: `plan` (read-only — mutating tools are not even
@@ -956,10 +969,41 @@ construction.
956
969
  }
957
970
  ```
958
971
 
959
- Or `hx mcp add local python server.py`. Tools arrive namespaced
972
+ Or `hx mcp add local python server.py`, or
973
+ `hx mcp add remote --url https://example.com/mcp`. Tools arrive namespaced
960
974
  `mcp__<server>__<tool>` in a deterministic order. Servers connect concurrently
961
975
  with a per-server timeout; one that is broken or slow logs a warning and is
962
- dropped rather than taking the session with it.
976
+ dropped rather than taking the session with it. A server that changes its tool
977
+ list mid-session (`notifications/tools/list_changed`) has the new list swapped
978
+ in without a restart.
979
+
980
+ A remote server that asks for sign-in - Atlassian's, for one - is signed in to
981
+ over OAuth, with nothing to configure:
982
+
983
+ ```json
984
+ { "mcpServers": { "atlassian": { "url": "https://mcp.atlassian.com/v2/mcp" } } }
985
+ ```
986
+
987
+ It shows as needing sign-in until you run `/mcp login atlassian` in a session
988
+ or `hx mcp login atlassian` in a shell. That opens the browser, and the tools
989
+ arrive as soon as you approve - no restart. HX follows the MCP authorization
990
+ spec: it finds the authorization server from the server's metadata, registers
991
+ itself as a client, signs in with PKCE, and keeps the token in `~/.hx/auth.json`
992
+ (mode 0600), refreshing it before it expires and again if the server rejects
993
+ it. Over SSH, paste the final redirect URL into the prompt instead.
994
+ `/mcp logout atlassian` forgets it.
995
+
996
+ A server that does not offer client registration needs a client you register
997
+ with it yourself, and anything else about the sign-in can be pinned the same
998
+ way:
999
+
1000
+ ```json
1001
+ "oauth": { "clientId": "...", "clientSecret": "...", "scope": "read write", "callbackPort": 33418 }
1002
+ ```
1003
+
1004
+ To send a token of your own instead - an API key - give the server an
1005
+ `Authorization` header, and OAuth stays out of the way:
1006
+ `hx mcp add jira --url https://mcp.atlassian.com/v2/mcp --header "Authorization: Basic <base64 email:token>"`.
963
1007
 
964
1008
  **Hooks** are shell commands HX runs at named points in a turn. Four events:
965
1009
  `PreToolUse`, `PostToolUse`, `UserPromptSubmit` and `Stop`.
@@ -1037,6 +1081,23 @@ uv run mypy # strict
1037
1081
  uv run hx # run from the checkout
1038
1082
  ```
1039
1083
 
1084
+ The suite is end-to-end. Each test in `tests/e2e/` starts the installed `hx`
1085
+ binary - on a real pseudo-terminal for the TUI, or as a plain subprocess for
1086
+ print mode and the other commands - in a private `$HOME` and `$HX_HOME`, and
1087
+ points it at a local OpenRouter stand-in with `HX_OPENROUTER_BASE_URL`.
1088
+ Everything between the keyboard and the socket is the shipped code; only the
1089
+ model's replies are scripted. The screen is read back through a VT emulator,
1090
+ so assertions are on the cells a terminal shows, not on the bytes HX meant to
1091
+ write.
1092
+
1093
+ Every run writes `e2e-report/`: `index.html` shows each test's outcome and
1094
+ every screen it checked, rendered in colour, and one text file per screen lets
1095
+ two runs be compared with `diff -r`. CI uploads it from every test job as an
1096
+ `e2e-report-*` artifact. Set `HX_E2E_REPORT` to write it somewhere else.
1097
+
1098
+ Tests that need the OS sandbox are skipped where there is no backend
1099
+ (`sandbox-exec` on macOS, `bwrap` on Linux).
1100
+
1040
1101
  Tests marked `live` hit a real API. The OpenRouter ones cost money:
1041
1102
 
1042
1103
  ```sh
@@ -1053,17 +1114,7 @@ uv run pytest -m live tests/test_live_devin.py
1053
1114
 
1054
1115
  They are the only place the wire formats, streaming, tool use and a genuine
1055
1116
  cache hit are proven against a real service; everything else runs against the
1056
- scripted provider.
1057
-
1058
- Tests marked `sandbox` exercise the real OS sandbox and are skipped where no
1059
- backend exists.
1060
-
1061
- The TUI has SVG layout snapshots. Read the diff before accepting a change to
1062
- them - they exist to catch a frame that quietly lost a row:
1063
-
1064
- ```sh
1065
- uv run pytest tests/tui/test_snapshots.py --snapshot-update
1066
- ```
1117
+ stand-in.
1067
1118
 
1068
1119
  ### Layout
1069
1120
 
@@ -1072,7 +1123,7 @@ src/hx/
1072
1123
  cli.py config.py paths.py frontmatter.py git.py
1073
1124
  core/ loop, context assembly, compaction, late injection, sessions, usage
1074
1125
  auth/ credential store, OAuth flows, per-route resolution
1075
- providers/ OpenRouter, Codex, Devin, the model catalogue, a scripted provider for tests
1126
+ providers/ OpenRouter, Codex, Devin, the model catalogue
1076
1127
  tools/ Bash, Read, Write, Edit, Glob, Grep, Symbols, TodoWrite, Task,
1077
1128
  WebSearch, WebFetch, line anchors, output capping
1078
1129
  permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
@@ -1109,12 +1160,12 @@ Each release:
1109
1160
  # 1. move CHANGELOG.md's [Unreleased] entries under the new version, dated,
1110
1161
  # and leave a fresh empty [Unreleased] behind
1111
1162
  # 2. bump __version__ in src/hx/__init__.py
1112
- # 3. uv run pytest tests/test_docs.py # the version must have a section
1163
+ # 3. uv run pytest tests/e2e/test_cli.py # the version must have a section
1113
1164
  git commit -am "release: X.Y.Z"
1114
1165
  git tag v$(uv run hx --version | cut -d' ' -f2) && git push origin main --tags
1115
1166
  ```
1116
1167
 
1117
- Step 1 is not optional and is not a convention: `tests/test_docs.py` fails the
1168
+ Step 1 is not optional and is not a convention: `tests/e2e/test_cli.py` fails the
1118
1169
  whole suite when `__version__` has no `CHANGELOG.md` section, so a release that
1119
1170
  skips it cannot get past CI to the `publish` job. The file ships inside the
1120
1171
  wheel and `hx changelog` prints it, so a version with no section there reaches
@@ -124,5 +124,5 @@ asyncio_mode = "auto"
124
124
  addopts = "-q --strict-markers -m 'not live'"
125
125
  markers = [
126
126
  "live: hits a real API (OpenRouter, or Tavily for the web search tools)",
127
- "sandbox: requires sandbox-exec (macOS) or bwrap (Linux)",
127
+ "e2e: drives the installed hx binary against a local OpenRouter stand-in",
128
128
  ]
@@ -1,5 +1,5 @@
1
1
  """HX - an agent harness for the terminal."""
2
2
 
3
- __version__ = "0.2.5"
3
+ __version__ = "0.2.7"
4
4
 
5
5
  __all__ = ["__version__"]