hx-cli 0.2.5__tar.gz → 0.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. {hx_cli-0.2.5 → hx_cli-0.2.6}/CHANGELOG.md +49 -1
  2. {hx_cli-0.2.5 → hx_cli-0.2.6}/PKG-INFO +46 -6
  3. {hx_cli-0.2.5 → hx_cli-0.2.6}/README.md +45 -5
  4. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/__init__.py +1 -1
  5. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/cli.py +218 -12
  6. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/session.py +59 -9
  7. hx_cli-0.2.6/src/hx/mcp/client.py +662 -0
  8. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/mcp/manager.py +208 -22
  9. hx_cli-0.2.6/src/hx/mcp/oauth.py +766 -0
  10. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/screen.py +38 -7
  11. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/registry.py +4 -0
  12. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/app.py +4 -3
  13. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/commands.py +101 -8
  14. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/format.py +22 -0
  15. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/renderers.py +69 -16
  16. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/runtime.py +27 -11
  17. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/login.py +3 -0
  18. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/pickers.py +79 -15
  19. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_session.py +51 -0
  20. hx_cli-0.2.6/tests/mcp/fixtures/oauth_server.py +438 -0
  21. hx_cli-0.2.6/tests/mcp/test_oauth.py +670 -0
  22. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_screen.py +27 -1
  23. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_cli.py +145 -0
  24. hx_cli-0.2.6/tests/tui/test_mcp_login.py +102 -0
  25. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_renderers.py +74 -0
  26. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/views/test_runtime.py +170 -0
  27. hx_cli-0.2.5/src/hx/mcp/client.py +0 -367
  28. {hx_cli-0.2.5 → hx_cli-0.2.6}/.github/workflows/ci.yml +0 -0
  29. {hx_cli-0.2.5 → hx_cli-0.2.6}/.gitignore +0 -0
  30. {hx_cli-0.2.5 → hx_cli-0.2.6}/examples/hooks/check.sh +0 -0
  31. {hx_cli-0.2.5 → hx_cli-0.2.6}/install.sh +0 -0
  32. {hx_cli-0.2.5 → hx_cli-0.2.6}/pyproject.toml +0 -0
  33. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/agents/__init__.py +0 -0
  34. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/agents/definitions.py +0 -0
  35. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/agents/subagent.py +0 -0
  36. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/__init__.py +0 -0
  37. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/oauth/__init__.py +0 -0
  38. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/oauth/browser.py +0 -0
  39. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/oauth/callback.py +0 -0
  40. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/oauth/codex.py +0 -0
  41. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/oauth/devin.py +0 -0
  42. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/oauth/pkce.py +0 -0
  43. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/resolve.py +0 -0
  44. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/store.py +0 -0
  45. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/config.py +0 -0
  46. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/__init__.py +0 -0
  47. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/checkpoints.py +0 -0
  48. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/compaction.py +0 -0
  49. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/context.py +0 -0
  50. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/events.py +0 -0
  51. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/images.py +0 -0
  52. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/lateinject.py +0 -0
  53. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/loop.py +0 -0
  54. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/messages.py +0 -0
  55. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/title.py +0 -0
  56. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/usage.py +0 -0
  57. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/docs.py +0 -0
  58. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/frontmatter.py +0 -0
  59. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/git.py +0 -0
  60. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/hooks/__init__.py +0 -0
  61. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/hooks/engine.py +0 -0
  62. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/hooks/spec.py +0 -0
  63. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/keys.py +0 -0
  64. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/mcp/__init__.py +0 -0
  65. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/net.py +0 -0
  66. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/paths.py +0 -0
  67. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/permissions/__init__.py +0 -0
  68. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/permissions/engine.py +0 -0
  69. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/permissions/parser.py +0 -0
  70. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/permissions/sandbox.py +0 -0
  71. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/__init__.py +0 -0
  72. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/base.py +0 -0
  73. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/codex.py +0 -0
  74. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/codex_catalogue.py +0 -0
  75. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/devin.py +0 -0
  76. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/devin_catalogue.py +0 -0
  77. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/devin_wire.py +0 -0
  78. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/fake.py +0 -0
  79. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/models.py +0 -0
  80. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/openrouter.py +0 -0
  81. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/protowire.py +0 -0
  82. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/registry.py +0 -0
  83. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/responses_codec.py +0 -0
  84. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/skills/__init__.py +0 -0
  85. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/skills/loader.py +0 -0
  86. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/skills/runtime.py +0 -0
  87. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/__init__.py +0 -0
  88. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/ansi.py +0 -0
  89. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/buffer.py +0 -0
  90. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/component.py +0 -0
  91. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/demo.py +0 -0
  92. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/editor.py +0 -0
  93. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/keydecode.py +0 -0
  94. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/loop.py +0 -0
  95. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/markdown.py +0 -0
  96. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/primitives.py +0 -0
  97. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/sanitize.py +0 -0
  98. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/syntax.py +0 -0
  99. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/terminal.py +0 -0
  100. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/undo.py +0 -0
  101. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/width.py +0 -0
  102. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/word_nav.py +0 -0
  103. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/__init__.py +0 -0
  104. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/anchors.py +0 -0
  105. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/base.py +0 -0
  106. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/bash.py +0 -0
  107. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/edit.py +0 -0
  108. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/glob.py +0 -0
  109. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/grep.py +0 -0
  110. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/output.py +0 -0
  111. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/read.py +0 -0
  112. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/symbols.py +0 -0
  113. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/task.py +0 -0
  114. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/todo.py +0 -0
  115. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/websearch.py +0 -0
  116. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/write.py +0 -0
  117. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/trace.py +0 -0
  118. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/__init__.py +0 -0
  119. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/clipboard.py +0 -0
  120. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/fuzzy.py +0 -0
  121. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/glyphs.py +0 -0
  122. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/killring.py +0 -0
  123. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/limits.py +0 -0
  124. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/paint.py +0 -0
  125. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/roles.py +0 -0
  126. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/theme.py +0 -0
  127. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/theme_json.py +0 -0
  128. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/themes/ansi.json +0 -0
  129. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/themes/dark.json +0 -0
  130. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/themes/light.json +0 -0
  131. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/__init__.py +0 -0
  132. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/blocks.py +0 -0
  133. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/dialog.py +0 -0
  134. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/permission.py +0 -0
  135. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/prompt.py +0 -0
  136. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/status.py +0 -0
  137. {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/transcript.py +0 -0
  138. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/__init__.py +0 -0
  139. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/agents/__init__.py +0 -0
  140. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/agents/test_subagent.py +0 -0
  141. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/auth/__init__.py +0 -0
  142. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/auth/test_codex_oauth.py +0 -0
  143. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/auth/test_devin_oauth.py +0 -0
  144. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/auth/test_pkce.py +0 -0
  145. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/auth/test_resolve.py +0 -0
  146. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/auth/test_store.py +0 -0
  147. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/conftest.py +0 -0
  148. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/__init__.py +0 -0
  149. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_checkpoints.py +0 -0
  150. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_compaction.py +0 -0
  151. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_context.py +0 -0
  152. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_images.py +0 -0
  153. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_lateinject.py +0 -0
  154. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_loop.py +0 -0
  155. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_loop_images.py +0 -0
  156. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_rewind.py +0 -0
  157. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_settings_layers.py +0 -0
  158. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_settings_migration.py +0 -0
  159. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_steering.py +0 -0
  160. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_title.py +0 -0
  161. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_usage.py +0 -0
  162. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/hooks/__init__.py +0 -0
  163. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/hooks/test_engine.py +0 -0
  164. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/hooks/test_example_check.py +0 -0
  165. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/mcp/__init__.py +0 -0
  166. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/mcp/fixtures/echo_server.py +0 -0
  167. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/mcp/test_manager.py +0 -0
  168. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/permissions/__init__.py +0 -0
  169. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/permissions/test_engine.py +0 -0
  170. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/permissions/test_parser.py +0 -0
  171. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/permissions/test_sandbox.py +0 -0
  172. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/__init__.py +0 -0
  173. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_catalogue.py +0 -0
  174. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_codex.py +0 -0
  175. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_codex_catalogue.py +0 -0
  176. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_devin.py +0 -0
  177. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_devin_catalogue.py +0 -0
  178. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_openrouter.py +0 -0
  179. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_protowire.py +0 -0
  180. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_registry.py +0 -0
  181. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_responses_codec.py +0 -0
  182. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/skills/__init__.py +0 -0
  183. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/skills/test_loader.py +0 -0
  184. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/__init__.py +0 -0
  185. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/conftest.py +0 -0
  186. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_ansi.py +0 -0
  187. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_keydecode.py +0 -0
  188. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_loop.py +0 -0
  189. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_markdown.py +0 -0
  190. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_primitives.py +0 -0
  191. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_sanitize.py +0 -0
  192. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_terminal.py +0 -0
  193. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_width.py +0 -0
  194. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_docs.py +0 -0
  195. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_git.py +0 -0
  196. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_install.py +0 -0
  197. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_integration.py +0 -0
  198. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_live.py +0 -0
  199. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_live_codex.py +0 -0
  200. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_live_devin.py +0 -0
  201. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_live_tavily.py +0 -0
  202. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_net.py +0 -0
  203. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_paths.py +0 -0
  204. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_readme.py +0 -0
  205. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_trace.py +0 -0
  206. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/__init__.py +0 -0
  207. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/test_background.py +0 -0
  208. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/test_bash.py +0 -0
  209. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/test_files.py +0 -0
  210. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/test_hashline.py +0 -0
  211. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/test_output.py +0 -0
  212. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/test_symbols.py +0 -0
  213. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/test_todo.py +0 -0
  214. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/test_websearch.py +0 -0
  215. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/__init__.py +0 -0
  216. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/support.py +0 -0
  217. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_autocomplete.py +0 -0
  218. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_clipboard.py +0 -0
  219. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_effort.py +0 -0
  220. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_images.py +0 -0
  221. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_keys.py +0 -0
  222. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_login.py +0 -0
  223. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_session.py +0 -0
  224. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_theme_json.py +0 -0
  225. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_trace_command.py +0 -0
  226. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/views/__init__.py +0 -0
  227. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/views/test_blocks.py +0 -0
  228. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/views/test_permission.py +0 -0
  229. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/views/test_pickers.py +0 -0
  230. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/views/test_prompt.py +0 -0
  231. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/views/test_status.py +0 -0
  232. {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/views/test_transcript.py +0 -0
  233. {hx_cli-0.2.5 → hx_cli-0.2.6}/uv.lock +0 -0
@@ -28,6 +28,53 @@ the project follows [semantic versioning](https://semver.org/).
28
28
 
29
29
  ---
30
30
 
31
+ ## [0.2.6] - 2026-09-23
32
+
33
+ ### Added
34
+
35
+ - Sign in to remote MCP servers over OAuth. A server that asks for it -
36
+ Atlassian's `https://mcp.atlassian.com/v2/mcp`, for one - shows as needing
37
+ sign-in; `/mcp login <server>` or `hx mcp login <server>` opens the browser
38
+ and the tools arrive without a restart. Tokens refresh on their own;
39
+ `/mcp logout <server>` forgets them. `"oauth": {"clientId": ...}` in
40
+ `mcp.json` covers servers without client registration.
41
+ - MCP servers that change their tool list mid-session have the new list
42
+ swapped in, whether they announce it in a reply or on their own stream.
43
+ - `hx mcp add NAME --url URL --header "Name: value"` saves headers, such as an
44
+ API token, with the server.
45
+ - `/mcp reconnect <server>` restarts one server without restarting HX.
46
+ - `/resume` always offers a list to switch to: this directory's sessions
47
+ first, then every other directory's, each row naming where it lives.
48
+ Picking one from elsewhere restarts HX in that directory on it, with the
49
+ same `--mode` and `--model`. The session already open is no longer listed,
50
+ and the list is no longer cut at 20.
51
+
52
+ ### Fixed
53
+
54
+ - `/resume` said "No previous sessions in this directory" in a fresh
55
+ directory, or inside a session resumed from another one, instead of
56
+ listing anything.
57
+ - `hx resume <id>` ran the session in whatever directory you typed it in,
58
+ not the one it was recorded in, so its paths and tool calls pointed at the
59
+ wrong project.
60
+ - `hx mcp add NAME --url URL`, `--user`, and server arguments like `npx -y`
61
+ were refused as unknown options, so none of them could be run.
62
+ - `hx mcp add` dropped headers from the entry it wrote.
63
+ - A remote MCP server that expires its session is initialized again instead
64
+ of failing every call until HX restarts.
65
+ - A multi-line shell command no longer breaks the screen. Its approval shows
66
+ one row per line, the tool call shows the first line and `+N lines`
67
+ (`ctrl+o` shows the rest), and the approval options and prompt are no longer
68
+ drawn several times over.
69
+ - `/resume` from inside a resumed session showed the resumed conversation
70
+ pushed off the top of the screen, leaving only the "Resumed" notice.
71
+ - A session title with a line break in it no longer splits its row in the
72
+ `/resume` picker.
73
+ - Started in your home directory, the status bar named it `~/.`; it now reads
74
+ `~`.
75
+
76
+ ---
77
+
31
78
  ## [0.2.5] - 2026-09-23
32
79
 
33
80
  ### Added
@@ -592,7 +639,8 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
592
639
  - `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
593
640
  self-update, and `install.sh` bootstrapping uv with a pinned Python.
594
641
 
595
- [Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.5...HEAD
642
+ [Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.6...HEAD
643
+ [0.2.6]: https://github.com/aletisunil/hx/compare/v0.2.5...v0.2.6
596
644
  [0.2.5]: https://github.com/aletisunil/hx/compare/v0.2.4...v0.2.5
597
645
  [0.2.4]: https://github.com/aletisunil/hx/compare/v0.2.3...v0.2.4
598
646
  [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.6
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
@@ -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 |
@@ -701,6 +703,13 @@ if you want a cheaper model for it — and it is counted in `/cost` like any oth
701
703
  If the call fails the session is still named, from your first message. `/title
702
704
  <text>` renames it.
703
705
 
706
+ `/resume` lists every session except the one you are in: this directory's
707
+ first, then other directories', each row naming where it lives. A session from
708
+ this directory is swapped in place. One from elsewhere belongs to that
709
+ directory - its sandbox, permission rules, prompt and tools - so HX restarts
710
+ itself there with `hx resume <id>`, keeping options like `--mode` and
711
+ `--model`.
712
+
704
713
  ### Themes
705
714
 
706
715
  A theme is a JSON file: a `vars` block of raw colours, and a `colors` block
@@ -996,10 +1005,41 @@ construction.
996
1005
  }
997
1006
  ```
998
1007
 
999
- Or `hx mcp add local python server.py`. Tools arrive namespaced
1008
+ Or `hx mcp add local python server.py`, or
1009
+ `hx mcp add remote --url https://example.com/mcp`. Tools arrive namespaced
1000
1010
  `mcp__<server>__<tool>` in a deterministic order. Servers connect concurrently
1001
1011
  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.
1012
+ dropped rather than taking the session with it. A server that changes its tool
1013
+ list mid-session (`notifications/tools/list_changed`) has the new list swapped
1014
+ in without a restart.
1015
+
1016
+ A remote server that asks for sign-in - Atlassian's, for one - is signed in to
1017
+ over OAuth, with nothing to configure:
1018
+
1019
+ ```json
1020
+ { "mcpServers": { "atlassian": { "url": "https://mcp.atlassian.com/v2/mcp" } } }
1021
+ ```
1022
+
1023
+ It shows as needing sign-in until you run `/mcp login atlassian` in a session
1024
+ or `hx mcp login atlassian` in a shell. That opens the browser, and the tools
1025
+ arrive as soon as you approve - no restart. HX follows the MCP authorization
1026
+ spec: it finds the authorization server from the server's metadata, registers
1027
+ itself as a client, signs in with PKCE, and keeps the token in `~/.hx/auth.json`
1028
+ (mode 0600), refreshing it before it expires and again if the server rejects
1029
+ it. Over SSH, paste the final redirect URL into the prompt instead.
1030
+ `/mcp logout atlassian` forgets it.
1031
+
1032
+ A server that does not offer client registration needs a client you register
1033
+ with it yourself, and anything else about the sign-in can be pinned the same
1034
+ way:
1035
+
1036
+ ```json
1037
+ "oauth": { "clientId": "...", "clientSecret": "...", "scope": "read write", "callbackPort": 33418 }
1038
+ ```
1039
+
1040
+ To send a token of your own instead - an API key - give the server an
1041
+ `Authorization` header, and OAuth stays out of the way:
1042
+ `hx mcp add jira --url https://mcp.atlassian.com/v2/mcp --header "Authorization: Basic <base64 email:token>"`.
1003
1043
 
1004
1044
  **Hooks** are shell commands HX runs at named points in a turn. Four events:
1005
1045
  `PreToolUse`, `PostToolUse`, `UserPromptSubmit` and `Stop`.
@@ -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
@@ -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 |
@@ -661,6 +663,13 @@ if you want a cheaper model for it — and it is counted in `/cost` like any oth
661
663
  If the call fails the session is still named, from your first message. `/title
662
664
  <text>` renames it.
663
665
 
666
+ `/resume` lists every session except the one you are in: this directory's
667
+ first, then other directories', each row naming where it lives. A session from
668
+ this directory is swapped in place. One from elsewhere belongs to that
669
+ directory - its sandbox, permission rules, prompt and tools - so HX restarts
670
+ itself there with `hx resume <id>`, keeping options like `--mode` and
671
+ `--model`.
672
+
664
673
  ### Themes
665
674
 
666
675
  A theme is a JSON file: a `vars` block of raw colours, and a `colors` block
@@ -956,10 +965,41 @@ construction.
956
965
  }
957
966
  ```
958
967
 
959
- Or `hx mcp add local python server.py`. Tools arrive namespaced
968
+ Or `hx mcp add local python server.py`, or
969
+ `hx mcp add remote --url https://example.com/mcp`. Tools arrive namespaced
960
970
  `mcp__<server>__<tool>` in a deterministic order. Servers connect concurrently
961
971
  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.
972
+ dropped rather than taking the session with it. A server that changes its tool
973
+ list mid-session (`notifications/tools/list_changed`) has the new list swapped
974
+ in without a restart.
975
+
976
+ A remote server that asks for sign-in - Atlassian's, for one - is signed in to
977
+ over OAuth, with nothing to configure:
978
+
979
+ ```json
980
+ { "mcpServers": { "atlassian": { "url": "https://mcp.atlassian.com/v2/mcp" } } }
981
+ ```
982
+
983
+ It shows as needing sign-in until you run `/mcp login atlassian` in a session
984
+ or `hx mcp login atlassian` in a shell. That opens the browser, and the tools
985
+ arrive as soon as you approve - no restart. HX follows the MCP authorization
986
+ spec: it finds the authorization server from the server's metadata, registers
987
+ itself as a client, signs in with PKCE, and keeps the token in `~/.hx/auth.json`
988
+ (mode 0600), refreshing it before it expires and again if the server rejects
989
+ it. Over SSH, paste the final redirect URL into the prompt instead.
990
+ `/mcp logout atlassian` forgets it.
991
+
992
+ A server that does not offer client registration needs a client you register
993
+ with it yourself, and anything else about the sign-in can be pinned the same
994
+ way:
995
+
996
+ ```json
997
+ "oauth": { "clientId": "...", "clientSecret": "...", "scope": "read write", "callbackPort": 33418 }
998
+ ```
999
+
1000
+ To send a token of your own instead - an API key - give the server an
1001
+ `Authorization` header, and OAuth stays out of the way:
1002
+ `hx mcp add jira --url https://mcp.atlassian.com/v2/mcp --header "Authorization: Basic <base64 email:token>"`.
963
1003
 
964
1004
  **Hooks** are shell commands HX runs at named points in a turn. Four events:
965
1005
  `PreToolUse`, `PostToolUse`, `UserPromptSubmit` and `Stop`.
@@ -1,5 +1,5 @@
1
1
  """HX - an agent harness for the terminal."""
2
2
 
3
- __version__ = "0.2.5"
3
+ __version__ = "0.2.6"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -38,7 +38,8 @@ Usage:
38
38
  hx prompt Print the system prompt this directory would use
39
39
  hx trace [SESSION_ID] [PATH]
40
40
  Write a session to a self-contained HTML trace
41
- hx mcp list|add|remove Manage MCP servers
41
+ hx mcp list|add|remove|login|logout
42
+ Manage MCP servers, and sign in to remote ones
42
43
  hx auth [set|clear] Show or change the OpenRouter API key
43
44
  hx docs [SECTION|--all] Print the manual, or one section of it
44
45
  hx changelog [VERSION] Print what shipped in each version
@@ -61,6 +62,14 @@ Options:
61
62
  DOC_COMMANDS = frozenset({"docs", "changelog"})
62
63
  """Commands that print a shipped document and exit, taking no run options."""
63
64
 
65
+ VERBATIM_COMMANDS = DOC_COMMANDS | {"mcp", "auth"}
66
+ """Commands with flags of their own, so their arguments are taken as typed.
67
+
68
+ `hx mcp add jira --url URL` and `hx mcp add fs npx -y server` are the
69
+ documented forms; read as run options, `--url` and `-y` were refused as
70
+ unknown before the command ever saw them.
71
+ """
72
+
64
73
 
65
74
  @dataclass(slots=True)
66
75
  class ParsedArgs:
@@ -76,6 +85,10 @@ class ParsedArgs:
76
85
  def main(argv: list[str] | None = None) -> int:
77
86
  """Parse arguments and dispatch. Returns a process exit code."""
78
87
  args = list(sys.argv[1:] if argv is None else argv)
88
+ if args and args[0] in VERBATIM_COMMANDS:
89
+ # `hx mcp add fs npx server --help` is a server's argument, not a
90
+ # request for HX's usage.
91
+ return _dispatch(args)
79
92
 
80
93
  if "--version" in args or "-V" in args:
81
94
  print(f"hx {__version__}")
@@ -114,10 +127,11 @@ def _dispatch(args: list[str]) -> int:
114
127
 
115
128
 
116
129
  def parse_args(args: list[str]) -> ParsedArgs:
117
- # The documentation commands print and exit, so their arguments are taken
118
- # verbatim: `hx docs --all` is a request for a section of the manual, not a
119
- # run of HX with an unknown option.
120
- if args and args[0] in DOC_COMMANDS:
130
+ # The documentation and management commands take their arguments verbatim:
131
+ # `hx docs --all` is a request for a section of the manual, and
132
+ # `hx mcp add jira --url URL` is for `hx mcp`, not a run of HX with an
133
+ # unknown option.
134
+ if args and args[0] in VERBATIM_COMMANDS:
121
135
  return ParsedArgs(command=args[0], rest=tuple(args[1:]))
122
136
 
123
137
  parsed = ParsedArgs()
@@ -250,6 +264,11 @@ class Runtime:
250
264
  return []
251
265
  statuses: list[Any] = await self.mcp.connect_all()
252
266
  await self.mcp.register_tools(self.tools)
267
+ for status in statuses:
268
+ if status.needs_login:
269
+ self.notices.append(
270
+ f"MCP server {status.name} needs sign-in. Run /mcp login {status.name}."
271
+ )
253
272
  return statuses
254
273
 
255
274
  async def refresh_models_if_stale(self) -> None:
@@ -582,24 +601,31 @@ def run_tui_command(parsed: ParsedArgs) -> int:
582
601
  from hx.tui.app import run_tui
583
602
 
584
603
  try:
585
- runtime = build_runtime(parsed, resume=_resume_target(parsed))
604
+ resume = _resume_target(parsed)
605
+ moved = _adopt_session_directory(parsed, resume)
606
+ runtime = build_runtime(parsed, resume=resume)
586
607
  except MissingCredential as exc:
587
608
  # Onboard for the route the configured model needs, not always for
588
609
  # OpenRouter: a user set to a Codex model wants a sign-in, not a key.
589
610
  if run_login(exc.provider_id) != 0:
590
611
  return _report(exc)
591
612
  try:
592
- runtime = build_runtime(parsed, resume=_resume_target(parsed))
613
+ runtime = build_runtime(parsed, resume=resume)
593
614
  except Exception as retry_exc:
594
615
  return _report(retry_exc)
595
616
  except Exception as exc:
596
617
  return _report(exc)
618
+ if moved:
619
+ runtime.notices.append(moved)
620
+
621
+ relaunch: str | None = None
597
622
 
598
623
  async def main_async() -> None:
624
+ nonlocal relaunch
599
625
  try:
600
626
  await runtime.connect_mcp()
601
627
  await runtime.refresh_models_if_stale()
602
- await run_tui(
628
+ relaunch = await run_tui(
603
629
  runtime.loop,
604
630
  runtime.bus,
605
631
  runtime.settings,
@@ -619,12 +645,95 @@ def run_tui_command(parsed: ParsedArgs) -> int:
619
645
  await _rename_closed_session(runtime.loop)
620
646
  finally:
621
647
  runtime.bus.close()
622
- await runtime.provider.aclose()
648
+ # Everything, not only the provider: a relaunch replaces this
649
+ # process, and an MCP server or background job left running would
650
+ # outlive the session that started it.
651
+ await runtime.aclose()
623
652
 
624
653
  asyncio.run(main_async())
654
+ if relaunch is not None:
655
+ _relaunch(relaunch)
625
656
  return 0
626
657
 
627
658
 
659
+ def _adopt_session_directory(parsed: ParsedArgs, session_id: str | None) -> str | None:
660
+ """Run ``hx resume <id>`` in the directory the session was recorded in.
661
+
662
+ A session's paths, tool calls and permission grants all mean something in
663
+ its own directory; resumed against another one, the model edits files that
664
+ are not the ones it read. An explicit ``--cwd`` still wins.
665
+
666
+ Returns a notice when the recorded directory is gone and the session is
667
+ resumed here instead.
668
+ """
669
+ if session_id is None or parsed.cwd is not None:
670
+ return None
671
+ from hx.core.session import SessionNotFound, read_meta
672
+
673
+ try:
674
+ recorded = Path(read_meta(session_id).cwd)
675
+ except SessionNotFound:
676
+ return None # build_runtime reports it
677
+ if recorded.is_dir():
678
+ parsed.cwd = recorded
679
+ return None
680
+ return f"This session was recorded in {recorded}, which no longer exists; resumed here."
681
+
682
+
683
+ RUN_OPTIONS_WITH_VALUES = frozenset(
684
+ {"--model", "--mode", "--cwd", "--system-prompt", "--append-system-prompt", "--image"}
685
+ )
686
+ """The run options that take the next argument as their value."""
687
+
688
+
689
+ def relaunch_argv(argv: list[str], session_id: str) -> list[str]:
690
+ """The arguments to start HX again on ``session_id``.
691
+
692
+ The run's own options - ``--mode bypass``, ``--model`` - carry over, since
693
+ the user asked for them and nothing about switching sessions un-asks.
694
+ What picked the old session does not: the ``resume`` and its id, and a
695
+ ``--cwd``, which the new session's own directory replaces.
696
+ """
697
+ kept: list[str] = []
698
+ index = 0
699
+ while index < len(argv):
700
+ arg = argv[index]
701
+ if arg == "--cwd":
702
+ index += 2
703
+ continue
704
+ if arg in RUN_OPTIONS_WITH_VALUES:
705
+ kept += argv[index : index + 2]
706
+ index += 2
707
+ continue
708
+ if arg == "resume":
709
+ index += 1
710
+ if index < len(argv) and not argv[index].startswith("-"):
711
+ index += 1
712
+ continue
713
+ kept.append(arg)
714
+ index += 1
715
+ return [*kept, "resume", session_id]
716
+
717
+
718
+ def _relaunch(session_id: str) -> None:
719
+ """Replace this process with HX resumed on ``session_id``.
720
+
721
+ ``exec`` rather than a child: the terminal, the pid and the shell's job
722
+ control stay exactly as they were, and there is no parent left waiting.
723
+
724
+ From the directory this process was started in, as if the user had typed
725
+ ``hx resume <id>`` there: that command moves into the session's own
726
+ directory by itself (:func:`_adopt_session_directory`), and the arguments
727
+ carried over - a script path, ``--system-prompt @p.md`` - were written
728
+ relative to here.
729
+ """
730
+ prefix = sys.orig_argv[: len(sys.orig_argv) - len(sys.argv) + 1]
731
+ command = [sys.executable, *prefix[1:], *relaunch_argv(sys.argv[1:], session_id)]
732
+ sys.stdout.flush()
733
+ sys.stderr.flush()
734
+ os.execv(sys.executable, command)
735
+
736
+
628
737
  RENAME_TIMEOUT_SECONDS = 10.0
629
738
  """A session name is not worth making the user wait for. Exit wins the tie."""
630
739
 
@@ -701,7 +810,13 @@ def run_print_command(parsed: ParsedArgs) -> int:
701
810
  )
702
811
  for status in await runtime.connect_mcp():
703
812
  if not status.connected:
704
- print(f"[mcp] {status.name} unavailable: {status.error}", file=sys.stderr)
813
+ if status.needs_login:
814
+ print(
815
+ f"[mcp] {status.name} {status.error} - run `hx mcp login {status.name}`",
816
+ file=sys.stderr,
817
+ )
818
+ else:
819
+ print(f"[mcp] {status.name} unavailable: {status.error}", file=sys.stderr)
705
820
  try:
706
821
  result = await runtime.loop.run(parsed.prompt, images)
707
822
  await _rename_closed_session(runtime.loop)
@@ -876,10 +991,14 @@ def _resume_target(parsed: ParsedArgs) -> str | None:
876
991
  MCP_USAGE = """\
877
992
  hx mcp list Show configured servers
878
993
  hx mcp add NAME COMMAND [ARGS...] Add a stdio server
879
- hx mcp add NAME --url URL Add an HTTP server
994
+ hx mcp add NAME --url URL [--header K:V]... Add an HTTP server
995
+ hx mcp login NAME Sign in to an HTTP server in the browser
996
+ hx mcp logout NAME Forget a server's sign-in
880
997
  hx mcp remove NAME Remove a server
881
998
 
882
999
  Add --user to write to ~/.hx/mcp.json instead of the project's .hx/mcp.json.
1000
+ An HTTP server that asks for sign-in gets it over OAuth unless a --header
1001
+ already sends Authorization.
883
1002
  """
884
1003
 
885
1004
 
@@ -891,6 +1010,9 @@ def run_mcp_command(args: list[str]) -> int:
891
1010
  save_config,
892
1011
  )
893
1012
 
1013
+ if args and args[0] in {"--help", "-h", "help"}:
1014
+ print(MCP_USAGE)
1015
+ return 0
894
1016
  user_level = "--user" in args
895
1017
  args = [arg for arg in args if arg != "--user"]
896
1018
  cwd = Path.cwd()
@@ -903,6 +1025,9 @@ def run_mcp_command(args: list[str]) -> int:
903
1025
  return 0
904
1026
  statuses = asyncio.run(_probe_servers(configs))
905
1027
  for status in statuses:
1028
+ if status.needs_login:
1029
+ print(f"{status.name:<20} {status.error} - run `hx mcp login {status.name}`")
1030
+ continue
906
1031
  mark = "ok" if status.connected else "unavailable"
907
1032
  detail = f" - {status.error}" if status.error else f" ({status.tool_count} tools)"
908
1033
  print(f"{status.name:<20} {mark}{detail}")
@@ -914,7 +1039,12 @@ def run_mcp_command(args: list[str]) -> int:
914
1039
  return 2
915
1040
  name = args[1]
916
1041
  if args[2] == "--url":
917
- config = MCPServerConfig(name=name, transport="http", url=args[3])
1042
+ try:
1043
+ url, headers = _parse_http_add(args[3:])
1044
+ except UsageError as exc:
1045
+ print(f"error: {exc}\n\n{MCP_USAGE}", file=sys.stderr)
1046
+ return 2
1047
+ config = MCPServerConfig(name=name, transport="http", url=url, headers=headers)
918
1048
  else:
919
1049
  config = MCPServerConfig(
920
1050
  name=name, transport="stdio", command=args[2], args=tuple(args[3:])
@@ -922,6 +1052,16 @@ def run_mcp_command(args: list[str]) -> int:
922
1052
  print(f"Added {name} to {save_config(config, cwd, user_level)}")
923
1053
  return 0
924
1054
 
1055
+ if args[0] in {"login", "logout"}:
1056
+ if len(args) < 2:
1057
+ print(MCP_USAGE, file=sys.stderr)
1058
+ return 2
1059
+ server = next((c for c in load_configs(cwd) if c.name == args[1]), None)
1060
+ if server is None:
1061
+ print(f"No server named {args[1]}.", file=sys.stderr)
1062
+ return 1
1063
+ return _mcp_login(server) if args[0] == "login" else _mcp_logout(server)
1064
+
925
1065
  if args[0] == "remove":
926
1066
  if len(args) < 2:
927
1067
  print(MCP_USAGE, file=sys.stderr)
@@ -936,6 +1076,69 @@ def run_mcp_command(args: list[str]) -> int:
936
1076
  return 2
937
1077
 
938
1078
 
1079
+ def _parse_http_add(rest: list[str]) -> tuple[str, dict[str, str]]:
1080
+ """``URL [--header 'Name: value']...`` from ``hx mcp add NAME --url``."""
1081
+ if not rest or rest[0].startswith("--"):
1082
+ raise UsageError("--url needs a URL")
1083
+ url, headers = rest[0], {}
1084
+ tail = rest[1:]
1085
+ while tail:
1086
+ flag = tail.pop(0)
1087
+ if flag not in {"--header", "-H"} or not tail:
1088
+ raise UsageError(f"unexpected {flag!r}")
1089
+ name, sep, value = tail.pop(0).partition(":")
1090
+ if not sep or not name.strip():
1091
+ raise UsageError("a header is written 'Name: value'")
1092
+ headers[name.strip()] = value.strip()
1093
+ return url, headers
1094
+
1095
+
1096
+ def _mcp_login(config: Any) -> int:
1097
+ from hx.auth.oauth.browser import OAuthError
1098
+ from hx.auth.oauth.callback import CallbackError
1099
+ from hx.mcp.client import MCPError
1100
+ from hx.mcp.manager import sign_in
1101
+ from hx.mcp.oauth import SignInNotRequired
1102
+
1103
+ try:
1104
+ asyncio.run(sign_in(config, ConsoleLogin()))
1105
+ except SignInNotRequired as exc:
1106
+ print(str(exc))
1107
+ return 0
1108
+ except (MCPError, OAuthError, CallbackError) as exc:
1109
+ print(f"error: {exc}", file=sys.stderr)
1110
+ return 1
1111
+ except KeyboardInterrupt:
1112
+ print("\nCancelled.", file=sys.stderr)
1113
+ return 1
1114
+
1115
+ status = asyncio.run(_probe_servers([config]))[0]
1116
+ if status.connected:
1117
+ print(f"Signed in to {config.name}: {status.tool_count} tools available.")
1118
+ return 0
1119
+ print(f"Signed in, but {config.name} is still unavailable: {status.error}", file=sys.stderr)
1120
+ return 1
1121
+
1122
+
1123
+ def _mcp_logout(config: Any) -> int:
1124
+ from hx.auth.store import AuthStore
1125
+ from hx.mcp.oauth import credential_id
1126
+
1127
+ if not config.url:
1128
+ print(f"{config.name} is a local server; there is no sign-in to forget.")
1129
+ return 0
1130
+ if not config.uses_oauth:
1131
+ # The stored sign-in is the server's, not this entry's: another
1132
+ # project may reach the same server over OAuth and still need it.
1133
+ print(f"{config.name} sends its own Authorization header; there is no sign-in to forget.")
1134
+ return 0
1135
+ if AuthStore().delete(credential_id(config.url)):
1136
+ print(f"Signed out of {config.name}.")
1137
+ else:
1138
+ print(f"Not signed in to {config.name}.")
1139
+ return 0
1140
+
1141
+
939
1142
  async def _probe_servers(configs: list[Any]) -> list[Any]:
940
1143
  from hx.mcp.manager import MCPManager
941
1144
 
@@ -1093,6 +1296,9 @@ def run_auth_command(args: list[str]) -> int:
1093
1296
  from hx.providers import registry
1094
1297
 
1095
1298
  action = args[0] if args else "status"
1299
+ if action in {"--help", "-h", "help"}:
1300
+ print(AUTH_USAGE)
1301
+ return 0
1096
1302
  resolver = AuthResolver()
1097
1303
 
1098
1304
  if action == "status":