hx-cli 0.2.1__tar.gz → 0.2.3__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 (222) hide show
  1. {hx_cli-0.2.1 → hx_cli-0.2.3}/CHANGELOG.md +76 -1
  2. {hx_cli-0.2.1 → hx_cli-0.2.3}/PKG-INFO +174 -15
  3. {hx_cli-0.2.1 → hx_cli-0.2.3}/README.md +169 -12
  4. hx_cli-0.2.3/examples/hooks/check.sh +103 -0
  5. {hx_cli-0.2.1 → hx_cli-0.2.3}/install.sh +1 -1
  6. {hx_cli-0.2.1 → hx_cli-0.2.3}/pyproject.toml +15 -2
  7. hx_cli-0.2.3/src/hx/__init__.py +5 -0
  8. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/agents/subagent.py +6 -0
  9. hx_cli-0.2.3/src/hx/auth/oauth/browser.py +133 -0
  10. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/auth/oauth/codex.py +10 -103
  11. hx_cli-0.2.3/src/hx/auth/oauth/devin.py +169 -0
  12. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/auth/resolve.py +8 -0
  13. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/cli.py +47 -15
  14. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/config.py +14 -2
  15. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/core/loop.py +98 -7
  16. hx_cli-0.2.3/src/hx/hooks/__init__.py +6 -0
  17. hx_cli-0.2.3/src/hx/hooks/engine.py +227 -0
  18. hx_cli-0.2.3/src/hx/hooks/spec.py +80 -0
  19. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/permissions/engine.py +2 -2
  20. hx_cli-0.2.3/src/hx/providers/devin.py +589 -0
  21. hx_cli-0.2.3/src/hx/providers/devin_catalogue.py +301 -0
  22. hx_cli-0.2.3/src/hx/providers/devin_wire.py +512 -0
  23. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/providers/models.py +205 -92
  24. hx_cli-0.2.3/src/hx/providers/protowire.py +225 -0
  25. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/providers/registry.py +71 -2
  26. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/loop.py +46 -5
  27. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/terminal.py +82 -8
  28. hx_cli-0.2.3/src/hx/tools/anchors.py +125 -0
  29. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tools/edit.py +124 -8
  30. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tools/read.py +38 -5
  31. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tools/registry.py +5 -0
  32. hx_cli-0.2.3/src/hx/tools/symbols.py +391 -0
  33. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/commands.py +48 -14
  34. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/renderers.py +17 -1
  35. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/runtime.py +83 -4
  36. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/views/login.py +50 -29
  37. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/views/status.py +1 -3
  38. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/agents/test_subagent.py +4 -3
  39. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/auth/test_codex_oauth.py +4 -4
  40. hx_cli-0.2.3/tests/auth/test_devin_oauth.py +119 -0
  41. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/conftest.py +8 -0
  42. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/core/test_loop.py +128 -0
  43. hx_cli-0.2.3/tests/hooks/test_engine.py +205 -0
  44. hx_cli-0.2.3/tests/hooks/test_example_check.py +98 -0
  45. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/providers/test_catalogue.py +10 -6
  46. hx_cli-0.2.3/tests/providers/test_devin.py +453 -0
  47. hx_cli-0.2.3/tests/providers/test_devin_catalogue.py +206 -0
  48. hx_cli-0.2.3/tests/providers/test_protowire.py +100 -0
  49. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/providers/test_registry.py +35 -2
  50. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/term/test_loop.py +75 -1
  51. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/term/test_terminal.py +86 -0
  52. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/test_cli.py +1 -1
  53. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/test_install.py +1 -1
  54. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/test_live_codex.py +8 -2
  55. hx_cli-0.2.3/tests/test_live_devin.py +130 -0
  56. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tools/test_files.py +19 -3
  57. hx_cli-0.2.3/tests/tools/test_hashline.py +197 -0
  58. hx_cli-0.2.3/tests/tools/test_symbols.py +127 -0
  59. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tui/test_login.py +58 -1
  60. hx_cli-0.2.3/tests/tui/views/__init__.py +0 -0
  61. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tui/views/test_runtime.py +77 -2
  62. {hx_cli-0.2.1 → hx_cli-0.2.3}/uv.lock +64 -12
  63. hx_cli-0.2.1/src/hx/__init__.py +0 -5
  64. {hx_cli-0.2.1 → hx_cli-0.2.3}/.github/workflows/ci.yml +0 -0
  65. {hx_cli-0.2.1 → hx_cli-0.2.3}/.gitignore +0 -0
  66. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/agents/__init__.py +0 -0
  67. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/agents/definitions.py +0 -0
  68. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/auth/__init__.py +0 -0
  69. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/auth/oauth/__init__.py +0 -0
  70. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/auth/oauth/callback.py +0 -0
  71. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/auth/oauth/pkce.py +0 -0
  72. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/auth/store.py +0 -0
  73. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/core/__init__.py +0 -0
  74. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/core/checkpoints.py +0 -0
  75. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/core/compaction.py +0 -0
  76. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/core/context.py +0 -0
  77. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/core/events.py +0 -0
  78. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/core/lateinject.py +0 -0
  79. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/core/messages.py +0 -0
  80. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/core/session.py +0 -0
  81. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/core/title.py +0 -0
  82. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/core/usage.py +0 -0
  83. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/docs.py +0 -0
  84. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/frontmatter.py +0 -0
  85. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/git.py +0 -0
  86. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/keys.py +0 -0
  87. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/mcp/__init__.py +0 -0
  88. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/mcp/client.py +0 -0
  89. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/mcp/manager.py +0 -0
  90. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/net.py +0 -0
  91. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/paths.py +0 -0
  92. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/permissions/__init__.py +0 -0
  93. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/permissions/parser.py +0 -0
  94. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/permissions/sandbox.py +0 -0
  95. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/providers/__init__.py +0 -0
  96. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/providers/base.py +0 -0
  97. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/providers/codex.py +0 -0
  98. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/providers/codex_catalogue.py +0 -0
  99. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/providers/fake.py +0 -0
  100. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/providers/openrouter.py +0 -0
  101. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/providers/responses_codec.py +0 -0
  102. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/skills/__init__.py +0 -0
  103. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/skills/loader.py +0 -0
  104. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/skills/runtime.py +0 -0
  105. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/__init__.py +0 -0
  106. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/ansi.py +0 -0
  107. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/buffer.py +0 -0
  108. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/component.py +0 -0
  109. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/demo.py +0 -0
  110. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/editor.py +0 -0
  111. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/keydecode.py +0 -0
  112. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/markdown.py +0 -0
  113. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/primitives.py +0 -0
  114. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/sanitize.py +0 -0
  115. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/screen.py +0 -0
  116. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/syntax.py +0 -0
  117. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/undo.py +0 -0
  118. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/width.py +0 -0
  119. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/term/word_nav.py +0 -0
  120. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tools/__init__.py +0 -0
  121. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tools/base.py +0 -0
  122. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tools/bash.py +0 -0
  123. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tools/glob.py +0 -0
  124. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tools/grep.py +0 -0
  125. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tools/output.py +0 -0
  126. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tools/task.py +0 -0
  127. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tools/todo.py +0 -0
  128. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tools/websearch.py +0 -0
  129. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tools/write.py +0 -0
  130. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/__init__.py +0 -0
  131. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/app.py +0 -0
  132. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/clipboard.py +0 -0
  133. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/format.py +0 -0
  134. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/fuzzy.py +0 -0
  135. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/glyphs.py +0 -0
  136. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/killring.py +0 -0
  137. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/limits.py +0 -0
  138. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/paint.py +0 -0
  139. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/roles.py +0 -0
  140. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/theme.py +0 -0
  141. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/theme_json.py +0 -0
  142. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/themes/ansi.json +0 -0
  143. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/themes/dark.json +0 -0
  144. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/themes/light.json +0 -0
  145. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/views/__init__.py +0 -0
  146. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/views/blocks.py +0 -0
  147. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/views/dialog.py +0 -0
  148. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/views/permission.py +0 -0
  149. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/views/pickers.py +0 -0
  150. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/views/prompt.py +0 -0
  151. {hx_cli-0.2.1 → hx_cli-0.2.3}/src/hx/tui/views/transcript.py +0 -0
  152. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/__init__.py +0 -0
  153. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/agents/__init__.py +0 -0
  154. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/auth/__init__.py +0 -0
  155. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/auth/test_pkce.py +0 -0
  156. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/auth/test_resolve.py +0 -0
  157. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/auth/test_store.py +0 -0
  158. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/core/__init__.py +0 -0
  159. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/core/test_checkpoints.py +0 -0
  160. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/core/test_compaction.py +0 -0
  161. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/core/test_context.py +0 -0
  162. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/core/test_lateinject.py +0 -0
  163. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/core/test_rewind.py +0 -0
  164. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/core/test_session.py +0 -0
  165. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/core/test_settings_layers.py +0 -0
  166. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/core/test_settings_migration.py +0 -0
  167. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/core/test_steering.py +0 -0
  168. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/core/test_title.py +0 -0
  169. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/core/test_usage.py +0 -0
  170. {hx_cli-0.2.1/tests/mcp → hx_cli-0.2.3/tests/hooks}/__init__.py +0 -0
  171. {hx_cli-0.2.1/tests/permissions → hx_cli-0.2.3/tests/mcp}/__init__.py +0 -0
  172. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/mcp/fixtures/echo_server.py +0 -0
  173. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/mcp/test_manager.py +0 -0
  174. {hx_cli-0.2.1/tests/providers → hx_cli-0.2.3/tests/permissions}/__init__.py +0 -0
  175. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/permissions/test_engine.py +0 -0
  176. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/permissions/test_parser.py +0 -0
  177. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/permissions/test_sandbox.py +0 -0
  178. {hx_cli-0.2.1/tests/skills → hx_cli-0.2.3/tests/providers}/__init__.py +0 -0
  179. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/providers/test_codex.py +0 -0
  180. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/providers/test_codex_catalogue.py +0 -0
  181. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/providers/test_openrouter.py +0 -0
  182. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/providers/test_responses_codec.py +0 -0
  183. {hx_cli-0.2.1/tests/term → hx_cli-0.2.3/tests/skills}/__init__.py +0 -0
  184. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/skills/test_loader.py +0 -0
  185. {hx_cli-0.2.1/tests/tools → hx_cli-0.2.3/tests/term}/__init__.py +0 -0
  186. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/term/conftest.py +0 -0
  187. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/term/test_ansi.py +0 -0
  188. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/term/test_keydecode.py +0 -0
  189. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/term/test_markdown.py +0 -0
  190. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/term/test_primitives.py +0 -0
  191. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/term/test_sanitize.py +0 -0
  192. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/term/test_screen.py +0 -0
  193. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/term/test_width.py +0 -0
  194. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/test_docs.py +0 -0
  195. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/test_git.py +0 -0
  196. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/test_integration.py +0 -0
  197. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/test_live.py +0 -0
  198. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/test_live_tavily.py +0 -0
  199. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/test_net.py +0 -0
  200. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/test_paths.py +0 -0
  201. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/test_readme.py +0 -0
  202. {hx_cli-0.2.1/tests/tui → hx_cli-0.2.3/tests/tools}/__init__.py +0 -0
  203. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tools/test_background.py +0 -0
  204. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tools/test_bash.py +0 -0
  205. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tools/test_output.py +0 -0
  206. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tools/test_todo.py +0 -0
  207. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tools/test_websearch.py +0 -0
  208. {hx_cli-0.2.1/tests/tui/views → hx_cli-0.2.3/tests/tui}/__init__.py +0 -0
  209. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tui/support.py +0 -0
  210. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tui/test_autocomplete.py +0 -0
  211. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tui/test_clipboard.py +0 -0
  212. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tui/test_effort.py +0 -0
  213. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tui/test_keys.py +0 -0
  214. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tui/test_renderers.py +0 -0
  215. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tui/test_session.py +0 -0
  216. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tui/test_theme_json.py +0 -0
  217. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tui/views/test_blocks.py +0 -0
  218. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tui/views/test_permission.py +0 -0
  219. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tui/views/test_pickers.py +0 -0
  220. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tui/views/test_prompt.py +0 -0
  221. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tui/views/test_status.py +0 -0
  222. {hx_cli-0.2.1 → hx_cli-0.2.3}/tests/tui/views/test_transcript.py +0 -0
@@ -28,6 +28,79 @@ the project follows [semantic versioning](https://semver.org/).
28
28
 
29
29
  ---
30
30
 
31
+ ## [0.2.3] - 2026-09-18
32
+
33
+ ### Added
34
+
35
+ - Hooks: shell commands HX runs at `PreToolUse`, `PostToolUse`,
36
+ `UserPromptSubmit` and `Stop`. A hook can refuse an action (exit 2, or
37
+ `{"decision": "block"}`), rewrite a tool's arguments before the permission
38
+ engine sees them (`updatedInput`), or add a note for the model
39
+ (`additionalContext`). Configure them under `"hooks"` in
40
+ `~/.hx/settings.json`; `/hooks` lists what is loaded. Hooks declared in a
41
+ project's checked-in `.hx/settings.json` are ignored and reported, because
42
+ cloning a repository must not be enough to run commands on your machine.
43
+ - `Symbols`: outline a file, find where a name is defined, or find where it is
44
+ used, over a tree-sitter parse tree rather than a regex - so a match inside a
45
+ comment or a string never counts. Python, TypeScript, JavaScript, Go and
46
+ Rust. Needs the grammars: `uv tool install "hx-cli[symbols]"`. Without them
47
+ the tool is not registered at all.
48
+ - An example hook, `examples/hooks/check.sh`: after every `Edit`/`Write` it
49
+ runs the project's own checker (`ruff`, `tsc`, `go vet`, `cargo check`)
50
+ against just that file and hands the output back to the model, so a typo is
51
+ caught on the next turn instead of at test time.
52
+ - Line anchors. `Read` now labels each line with a short content hash, and
53
+ `Edit` accepts those anchors in a `hashline` argument to replace a span
54
+ without retyping it. An anchor that no longer resolves means the file moved,
55
+ so the patch is rejected instead of landing at the wrong lines. Set
56
+ `"tools": {"hashline": false}` to go back to plain `cat -n` reads.
57
+
58
+ ### Removed
59
+
60
+ - The `telemetry` setting, which was parsed and never read by anything.
61
+
62
+ ### Fixed
63
+
64
+ - The README's Status section said there were two model routes; Devin made it
65
+ three in 0.2.2.
66
+ - `MultiEdit` and `NotebookEdit` were listed as mutating tools and as edit
67
+ tools in the permission engine, but no such tools exist. A rule written
68
+ against either name silently matched nothing.
69
+
70
+ ---
71
+
72
+ ## [0.2.2] - 2026-09-17
73
+
74
+ ### Added
75
+
76
+ - Devin subscription as a third route: `hx auth login devin` (or `/login`)
77
+ signs in through the browser, and `devin/<model>` ids run against the plan -
78
+ every model the account includes, listed by `/model` and refreshed with
79
+ `/models refresh`. Reasoning depths Devin ships as separate models are folded
80
+ into one, so `/effort` picks the depth, and router models such as
81
+ `devin/adaptive` are assigned per turn. A session token in `DEVIN_API_KEY`
82
+ works where there is no browser. Devin Enterprise accounts sign in through
83
+ the same command by choosing "Log in with Devin for Enterprise".
84
+
85
+ ### Fixed
86
+
87
+ - The `/login` dialog no longer sits on "Starting…" for the whole sign-in: it
88
+ now updates as the flow moves on, and keeps the instructions on screen
89
+ alongside the link, progress and paste field instead of overwriting them.
90
+
91
+ - Resizing the terminal no longer crashes the session. A window manager sends
92
+ a burst of SIGWINCH for one drag of a corner, and a signal landing while the
93
+ previous frame was still writing re-entered stdout: `RuntimeError: reentrant
94
+ call inside <_io.BufferedWriter>`, and HX was gone. The redraw is handed to
95
+ the event loop now, and the burst collapses into one repaint.
96
+
97
+ - `/resume` and `hx resume` bring the whole conversation back, not only its
98
+ text: the reasoning, tool calls, their output and the plan are drawn again.
99
+ A resumed session used to show a blank tinted bar for each tool result and
100
+ nothing at all for the calls themselves.
101
+
102
+ ---
103
+
31
104
  ## [0.2.1] - 2026-09-14
32
105
 
33
106
  ### Changed
@@ -408,7 +481,9 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
408
481
  - `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
409
482
  self-update, and `install.sh` bootstrapping uv with a pinned Python.
410
483
 
411
- [Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.1...HEAD
484
+ [Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.3...HEAD
485
+ [0.2.3]: https://github.com/aletisunil/hx/compare/v0.2.2...v0.2.3
486
+ [0.2.2]: https://github.com/aletisunil/hx/compare/v0.2.1...v0.2.2
412
487
  [0.2.1]: https://github.com/aletisunil/hx/compare/v0.2.0...v0.2.1
413
488
  [0.2.0]: https://github.com/aletisunil/hx/compare/v0.1.9...v0.2.0
414
489
  [0.1.9]: https://github.com/aletisunil/hx/compare/v0.1.8...v0.1.9
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hx-cli
3
- Version: 0.2.1
4
- Summary: HX - a terminal coding agent
3
+ Version: 0.2.3
4
+ Summary: HX - an agent harness for the terminal
5
5
  Project-URL: Homepage, https://github.com/aletisunil/hx
6
6
  Project-URL: Issues, https://github.com/aletisunil/hx/issues
7
7
  Author: Sunil Aleti
@@ -14,7 +14,6 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
16
  Requires-Python: >=3.11
17
- Requires-Dist: click>=8.1
18
17
  Requires-Dist: httpx>=0.27
19
18
  Requires-Dist: markdown-it-py>=3.0
20
19
  Requires-Dist: pydantic>=2.7
@@ -31,12 +30,15 @@ Requires-Dist: pytest>=8.2; extra == 'dev'
31
30
  Requires-Dist: ruff>=0.6; extra == 'dev'
32
31
  Requires-Dist: types-pygments; extra == 'dev'
33
32
  Requires-Dist: types-pyyaml; extra == 'dev'
33
+ Provides-Extra: symbols
34
+ Requires-Dist: tree-sitter-language-pack>=0.7; extra == 'symbols'
35
+ Requires-Dist: tree-sitter>=0.23; extra == 'symbols'
34
36
  Description-Content-Type: text/markdown
35
37
 
36
38
  # HX
37
39
 
38
- A terminal coding agent. Python core, its own terminal renderer, OpenRouter for
39
- models.
40
+ An agent harness for the terminal. Python core, its own terminal renderer,
41
+ OpenRouter for models.
40
42
 
41
43
  HX runs in your project directory, reads and edits your code, runs commands in
42
44
  a sandboxed shell, and shows you what every turn costs.
@@ -85,13 +87,14 @@ Requires Python 3.11+. macOS and Linux. Update with `hx upgrade`.
85
87
 
86
88
  ### Credentials
87
89
 
88
- HX reaches models over two routes. Which one serves a turn is decided by the
90
+ HX reaches models over three routes. Which one serves a turn is decided by the
89
91
  model id alone, so "what paid for that" is always answerable by reading it:
90
92
 
91
93
  | Model id | Route | Billing |
92
94
  |---|---|---|
93
95
  | `anthropic/claude-sonnet-4.5`, `openai/gpt-5`, … | OpenRouter | per token, API key |
94
96
  | `openai-codex/gpt-5.6-terra` | ChatGPT Plus/Pro | your subscription |
97
+ | `devin/swe-1-6`, `devin/claude-opus-5`, … | Devin | your subscription |
95
98
 
96
99
  Credentials live in `~/.hx/auth.json`, mode 0600, one entry per route.
97
100
 
@@ -185,6 +188,38 @@ They join the `/model` picker on the next start. HX cannot ask how large an
185
188
  unknown model's context window is, so it assumes a conservative 200k — that
186
189
  costs an accurate context gauge, not the use of the model.
187
190
 
191
+ **Devin.** Sign in with the same browser flow the official `devin` CLI uses,
192
+ and run every model your Devin plan includes - SWE-1.6, and whatever frontier
193
+ models the plan offers - against the subscription:
194
+
195
+ ```sh
196
+ hx auth login devin # or /login inside the TUI
197
+ hx --model devin/swe-1-6
198
+ hx auth logout devin
199
+ ```
200
+
201
+ **Devin Enterprise** signs in the same way: on the page that opens, choose
202
+ "Log in with Devin for Enterprise", enter your company, and finish through its
203
+ identity provider. Your admin has to grant the "Use Devin CLI" permission first.
204
+ Usage counts toward the organization's allocation, and an enterprise tenant
205
+ served from its own API server is followed there for both turns and the model
206
+ list.
207
+
208
+ The browser opens to `app.devin.ai` and redirects to a loopback listener on
209
+ `127.0.0.1:59653`. Over SSH, paste the final redirect URL into the prompt
210
+ instead. A Devin login does not refresh: when it expires, HX says so and asks
211
+ you to sign in again. On a machine with no browser at all, a session token in
212
+ `HX_DEVIN_API_KEY` or `DEVIN_API_KEY` is used when nothing is saved.
213
+
214
+ Signing in fetches the account's own model list, and `/model` offers exactly
215
+ that; `/models refresh` asks again. Devin publishes each reasoning depth as a
216
+ model of its own - Claude Opus 5 High, Claude Opus 5 Max - and HX folds each
217
+ family into one model, so `/effort` picks the depth on Devin exactly as it does
218
+ on Codex. A fast lane or a 1M-context lane stays a separate model, since it
219
+ changes what a turn costs or how much fits. Router models such as
220
+ `devin/adaptive` are resolved to a concrete model on every turn by Devin itself.
221
+ If the list cannot be fetched, `devin/swe-1-6` and `devin/swe-1-6-fast` stand in.
222
+
188
223
  **Web search.** Optional, and the one credential that is not a model route.
189
224
  It powers two tools:
190
225
 
@@ -270,7 +305,7 @@ hx --system-prompt @p.md # replace the system prompt for one run
270
305
  hx --append-system-prompt "Always run the tests" # add to it; repeatable
271
306
  hx mcp list|add|remove # manage MCP servers
272
307
  hx auth [set|clear] # manage the OpenRouter key
273
- hx auth login [provider] # sign in (openrouter, openai-codex)
308
+ hx auth login [provider] # sign in (openrouter, openai-codex, devin)
274
309
  hx auth logout <provider> # forget a stored credential
275
310
  hx docs [section] # the manual; no argument lists its sections
276
311
  hx changelog [version] # what shipped in each version
@@ -405,6 +440,7 @@ than being silently resolved.
405
440
  | `/skills` | installed skills |
406
441
  | `/agents` | subagent types |
407
442
  | `/mcp` | server status |
443
+ | `/hooks` | configured hooks, and any that were refused |
408
444
  | `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
409
445
  | `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
410
446
  | `/copy` | copy the last reply to the clipboard |
@@ -522,7 +558,6 @@ along with the `.hx/.gitignore` it added to hide it.
522
558
  {
523
559
  "theme": "dark", // dark | light | ansi, or a file in ~/.hx/themes
524
560
  "quiet_startup": false, // skip the startup header
525
- "telemetry": false,
526
561
 
527
562
  "models": {
528
563
  "model": "anthropic/claude-sonnet-4.5",
@@ -565,10 +600,18 @@ along with the `.hx/.gitignore` it added to hide it.
565
600
  "tui": {
566
601
  "enterWhileBusy": "queue", // queue | steer - what enter does mid-turn
567
602
  "fullscreen": false // true takes the whole window (see /fullscreen)
603
+ },
604
+
605
+ "tools": {
606
+ "hashline": true // Read labels lines with content anchors Edit accepts
568
607
  }
569
608
  }
570
609
  ```
571
610
 
611
+ `hooks` is configured in the same files but is deliberately not part of this
612
+ merge: it is read per layer, and only from your own settings. See
613
+ [Extending it](#extending-it).
614
+
572
615
  Environment overrides: `HX_MODEL`, `HX_SUBAGENT_MODEL`, `HX_MAX_TOKENS`,
573
616
  `HX_PERMISSION_MODE`, `HX_SANDBOX`, `HX_COMPACT_AT`, `HX_GIT_NOTICES`,
574
617
  `HX_THEME`, `HX_QUIET_STARTUP`. Also `HX_HOME` to relocate user state, `HX_TAVILY_API_KEY`
@@ -729,6 +772,54 @@ rest to the session directory, and hands the model that path to grep.
729
772
 
730
773
  ---
731
774
 
775
+ ## Reading and editing code
776
+
777
+ **Line anchors.** `Read` labels every line with a short content hash:
778
+
779
+ ```
780
+ 1 a3f9 def handle(request):
781
+ 2 7c1c if request.method == "POST":
782
+ ```
783
+
784
+ The anchor is a hash of the line and its two neighbours, truncated to the
785
+ narrowest width that is unambiguous in that file. `Edit` takes those anchors
786
+ instead of retyped content:
787
+
788
+ ```json
789
+ { "file_path": "app.py", "hashline": [{ "start": "a3f9", "end": "7c1c", "new_string": "..." }] }
790
+ ```
791
+
792
+ The span is inclusive; omit `end` to replace one line, pass an empty
793
+ `new_string` to delete. Anchors are recomputed from disk when the edit runs, so
794
+ a file that moved under the model fails to resolve and the patch is rejected
795
+ rather than applied at stale coordinates. Exact-match `old_string` still works
796
+ and is unchanged; anchors are a second shape, not a replacement. Turn the whole
797
+ thing off with `"tools": {"hashline": false}` and reads go back to plain
798
+ `cat -n`.
799
+
800
+ **Symbols** answers structural questions with a parse tree rather than a
801
+ regex — three modes, `outline` for one file's shape, `definition` for where a
802
+ name is defined, `references` for where it is used:
803
+
804
+ ```
805
+ Symbols(mode="references", symbol="handle")
806
+ ```
807
+
808
+ Because it matches identifier nodes, the same word in a comment or a string
809
+ literal never matches. It cannot resolve types, so an unrelated symbol with the
810
+ same name in another file still does; it is tighter than `Grep`, not a language
811
+ server. Python, TypeScript, JavaScript, Go and Rust.
812
+
813
+ It needs grammars that are not installed by default, and the tool is simply not
814
+ registered without them — its schema never enters the cached prefix advertising
815
+ something that cannot run:
816
+
817
+ ```sh
818
+ uv tool install "hx-cli[symbols]"
819
+ ```
820
+
821
+ ---
822
+
732
823
  ## Extending it
733
824
 
734
825
  **Skills** are directories containing `SKILL.md` with YAML frontmatter:
@@ -785,6 +876,68 @@ Or `hx mcp add local python server.py`. Tools arrive namespaced
785
876
  with a per-server timeout; one that is broken or slow logs a warning and is
786
877
  dropped rather than taking the session with it.
787
878
 
879
+ **Hooks** are shell commands HX runs at named points in a turn. Four events:
880
+ `PreToolUse`, `PostToolUse`, `UserPromptSubmit` and `Stop`.
881
+
882
+ ```json
883
+ {
884
+ "hooks": {
885
+ "PreToolUse": [
886
+ {
887
+ "matcher": "Bash",
888
+ "hooks": [{ "type": "command", "command": "~/.hx/guard-bash.sh", "timeout": 10 }]
889
+ }
890
+ ],
891
+ "PostToolUse": [
892
+ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "ruff check --quiet" }] }
893
+ ]
894
+ }
895
+ }
896
+ ```
897
+
898
+ `matcher` is a regex matched against the whole tool name; omit it, or use `*`,
899
+ to match every tool. The hook reads a JSON event on stdin — `session_id`,
900
+ `cwd`, `hook_event_name`, and `tool_name`/`tool_input` where they apply — and
901
+ answers with its exit code:
902
+
903
+ | Exit | Means |
904
+ |---|---|
905
+ | `0` | allow |
906
+ | `2` | block. stderr becomes the reason the model is given |
907
+ | anything else | the hook is broken. Reported as a notice; the action proceeds |
908
+
909
+ That last row is deliberate: a typo in a shell command must not be able to
910
+ wedge a session. Only an explicit refusal stops anything.
911
+
912
+ A hook may also print JSON on stdout: `{"decision": "block", "reason": "..."}`
913
+ refuses, `{"updatedInput": {...}}` rewrites a tool's arguments before the
914
+ permission engine sees them, and `{"additionalContext": "..."}` adds a note for
915
+ the model to read. The format matches Claude Code's, so hooks already written
916
+ against that work here unchanged.
917
+
918
+ A worked example ships in the repository:
919
+ [`examples/hooks/check.sh`](https://github.com/aletisunil/hx/blob/main/examples/hooks/check.sh)
920
+ runs the checker a project already has — `ruff`, `tsc`, `go vet`, `cargo
921
+ check` — against the one file the model just edited, and returns whatever it
922
+ says as `additionalContext`. The model sees the errors appended to its own edit
923
+ result and fixes them on the next turn, rather than discovering them at test
924
+ time. It exits 0 either way: by `PostToolUse` the write has already landed, so
925
+ refusing it would be an answer to something that already happened.
926
+
927
+ ```sh
928
+ curl -o ~/.hx/check.sh https://raw.githubusercontent.com/aletisunil/hx/main/examples/hooks/check.sh
929
+ chmod +x ~/.hx/check.sh
930
+ ```
931
+
932
+ **Where hooks may be declared is a security boundary.** They are loaded from
933
+ `~/.hx/settings.json` and `~/.hx/projects/<slug>/settings.local.json` only.
934
+ Hooks in a project's checked-in `.hx/settings.json` are ignored and listed by
935
+ `/hooks`, because that file arrives with a clone and cloning a repository must
936
+ never be enough to run commands on the machine that cloned it. To adopt a
937
+ shared hook, copy it into your own settings — a deliberate act by the person
938
+ who will run it. Subagents inherit the parent's hooks, so a guard cannot be
939
+ bypassed by delegating through `Task`.
940
+
788
941
  ---
789
942
 
790
943
  ## Development
@@ -807,6 +960,10 @@ OPENROUTER_API_KEY=... uv run pytest -m live tests/test_live.py
807
960
  # the Codex route draws on your ChatGPT subscription instead
808
961
  hx auth login openai-codex
809
962
  uv run pytest -m live tests/test_live_codex.py
963
+
964
+ # and the Devin route on your Devin subscription
965
+ hx auth login devin
966
+ uv run pytest -m live tests/test_live_devin.py
810
967
  ```
811
968
 
812
969
  They are the only place the wire formats, streaming, tool use and a genuine
@@ -830,10 +987,11 @@ src/hx/
830
987
  cli.py config.py paths.py frontmatter.py git.py
831
988
  core/ loop, context assembly, compaction, late injection, sessions, usage
832
989
  auth/ credential store, OAuth flows, per-route resolution
833
- providers/ OpenRouter, Codex, the model catalogue, a scripted provider for tests
834
- tools/ Bash, Read, Write, Edit, Glob, Grep, TodoWrite, Task, WebSearch,
835
- WebFetch, output capping
990
+ providers/ OpenRouter, Codex, Devin, the model catalogue, a scripted provider for tests
991
+ tools/ Bash, Read, Write, Edit, Glob, Grep, Symbols, TodoWrite, Task,
992
+ WebSearch, WebFetch, line anchors, output capping
836
993
  permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
994
+ hooks/ the four events, the trust boundary, subprocess execution
837
995
  skills/ agents/ mcp/
838
996
  keys.py keybinding registry: ids, defaults, descriptions, user overrides
839
997
  term/ the renderer: raw mode, input decoding, the differ, the screen
@@ -896,12 +1054,13 @@ streaming with prefix caching and accurate cost accounting, session persistence
896
1054
  and resume, the tool suite, the permission engine and OS sandbox, late
897
1055
  injection, compaction, output capping, skills, subagents, MCP, and the TUI.
898
1056
 
899
- Two routes to a model: an OpenRouter API key, or a ChatGPT Plus/Pro
900
- subscription over OAuth. The model id decides which.
1057
+ Three routes to a model: an OpenRouter API key, a ChatGPT Plus/Pro
1058
+ subscription over OAuth, or a Devin subscription over OAuth. The model id
1059
+ decides which.
901
1060
 
902
1061
  Published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/), released
903
1062
  from CI on a tag.
904
1063
 
905
- The one thing still unproven in CI is a live call on either route: the `live`
906
- tests exist and cover both wire formats, tool use, reasoning replay and a
1064
+ The one thing still unproven in CI is a live call on any route: the `live`
1065
+ tests exist and cover all three wire formats, tool use, reasoning replay and a
907
1066
  genuine cache hit, but they need a credential and are deselected by default.
@@ -1,7 +1,7 @@
1
1
  # HX
2
2
 
3
- A terminal coding agent. Python core, its own terminal renderer, OpenRouter for
4
- models.
3
+ An agent harness for the terminal. Python core, its own terminal renderer,
4
+ OpenRouter for models.
5
5
 
6
6
  HX runs in your project directory, reads and edits your code, runs commands in
7
7
  a sandboxed shell, and shows you what every turn costs.
@@ -50,13 +50,14 @@ Requires Python 3.11+. macOS and Linux. Update with `hx upgrade`.
50
50
 
51
51
  ### Credentials
52
52
 
53
- HX reaches models over two routes. Which one serves a turn is decided by the
53
+ HX reaches models over three routes. Which one serves a turn is decided by the
54
54
  model id alone, so "what paid for that" is always answerable by reading it:
55
55
 
56
56
  | Model id | Route | Billing |
57
57
  |---|---|---|
58
58
  | `anthropic/claude-sonnet-4.5`, `openai/gpt-5`, … | OpenRouter | per token, API key |
59
59
  | `openai-codex/gpt-5.6-terra` | ChatGPT Plus/Pro | your subscription |
60
+ | `devin/swe-1-6`, `devin/claude-opus-5`, … | Devin | your subscription |
60
61
 
61
62
  Credentials live in `~/.hx/auth.json`, mode 0600, one entry per route.
62
63
 
@@ -150,6 +151,38 @@ They join the `/model` picker on the next start. HX cannot ask how large an
150
151
  unknown model's context window is, so it assumes a conservative 200k — that
151
152
  costs an accurate context gauge, not the use of the model.
152
153
 
154
+ **Devin.** Sign in with the same browser flow the official `devin` CLI uses,
155
+ and run every model your Devin plan includes - SWE-1.6, and whatever frontier
156
+ models the plan offers - against the subscription:
157
+
158
+ ```sh
159
+ hx auth login devin # or /login inside the TUI
160
+ hx --model devin/swe-1-6
161
+ hx auth logout devin
162
+ ```
163
+
164
+ **Devin Enterprise** signs in the same way: on the page that opens, choose
165
+ "Log in with Devin for Enterprise", enter your company, and finish through its
166
+ identity provider. Your admin has to grant the "Use Devin CLI" permission first.
167
+ Usage counts toward the organization's allocation, and an enterprise tenant
168
+ served from its own API server is followed there for both turns and the model
169
+ list.
170
+
171
+ The browser opens to `app.devin.ai` and redirects to a loopback listener on
172
+ `127.0.0.1:59653`. Over SSH, paste the final redirect URL into the prompt
173
+ instead. A Devin login does not refresh: when it expires, HX says so and asks
174
+ you to sign in again. On a machine with no browser at all, a session token in
175
+ `HX_DEVIN_API_KEY` or `DEVIN_API_KEY` is used when nothing is saved.
176
+
177
+ Signing in fetches the account's own model list, and `/model` offers exactly
178
+ that; `/models refresh` asks again. Devin publishes each reasoning depth as a
179
+ model of its own - Claude Opus 5 High, Claude Opus 5 Max - and HX folds each
180
+ family into one model, so `/effort` picks the depth on Devin exactly as it does
181
+ on Codex. A fast lane or a 1M-context lane stays a separate model, since it
182
+ changes what a turn costs or how much fits. Router models such as
183
+ `devin/adaptive` are resolved to a concrete model on every turn by Devin itself.
184
+ If the list cannot be fetched, `devin/swe-1-6` and `devin/swe-1-6-fast` stand in.
185
+
153
186
  **Web search.** Optional, and the one credential that is not a model route.
154
187
  It powers two tools:
155
188
 
@@ -235,7 +268,7 @@ hx --system-prompt @p.md # replace the system prompt for one run
235
268
  hx --append-system-prompt "Always run the tests" # add to it; repeatable
236
269
  hx mcp list|add|remove # manage MCP servers
237
270
  hx auth [set|clear] # manage the OpenRouter key
238
- hx auth login [provider] # sign in (openrouter, openai-codex)
271
+ hx auth login [provider] # sign in (openrouter, openai-codex, devin)
239
272
  hx auth logout <provider> # forget a stored credential
240
273
  hx docs [section] # the manual; no argument lists its sections
241
274
  hx changelog [version] # what shipped in each version
@@ -370,6 +403,7 @@ than being silently resolved.
370
403
  | `/skills` | installed skills |
371
404
  | `/agents` | subagent types |
372
405
  | `/mcp` | server status |
406
+ | `/hooks` | configured hooks, and any that were refused |
373
407
  | `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
374
408
  | `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
375
409
  | `/copy` | copy the last reply to the clipboard |
@@ -487,7 +521,6 @@ along with the `.hx/.gitignore` it added to hide it.
487
521
  {
488
522
  "theme": "dark", // dark | light | ansi, or a file in ~/.hx/themes
489
523
  "quiet_startup": false, // skip the startup header
490
- "telemetry": false,
491
524
 
492
525
  "models": {
493
526
  "model": "anthropic/claude-sonnet-4.5",
@@ -530,10 +563,18 @@ along with the `.hx/.gitignore` it added to hide it.
530
563
  "tui": {
531
564
  "enterWhileBusy": "queue", // queue | steer - what enter does mid-turn
532
565
  "fullscreen": false // true takes the whole window (see /fullscreen)
566
+ },
567
+
568
+ "tools": {
569
+ "hashline": true // Read labels lines with content anchors Edit accepts
533
570
  }
534
571
  }
535
572
  ```
536
573
 
574
+ `hooks` is configured in the same files but is deliberately not part of this
575
+ merge: it is read per layer, and only from your own settings. See
576
+ [Extending it](#extending-it).
577
+
537
578
  Environment overrides: `HX_MODEL`, `HX_SUBAGENT_MODEL`, `HX_MAX_TOKENS`,
538
579
  `HX_PERMISSION_MODE`, `HX_SANDBOX`, `HX_COMPACT_AT`, `HX_GIT_NOTICES`,
539
580
  `HX_THEME`, `HX_QUIET_STARTUP`. Also `HX_HOME` to relocate user state, `HX_TAVILY_API_KEY`
@@ -694,6 +735,54 @@ rest to the session directory, and hands the model that path to grep.
694
735
 
695
736
  ---
696
737
 
738
+ ## Reading and editing code
739
+
740
+ **Line anchors.** `Read` labels every line with a short content hash:
741
+
742
+ ```
743
+ 1 a3f9 def handle(request):
744
+ 2 7c1c if request.method == "POST":
745
+ ```
746
+
747
+ The anchor is a hash of the line and its two neighbours, truncated to the
748
+ narrowest width that is unambiguous in that file. `Edit` takes those anchors
749
+ instead of retyped content:
750
+
751
+ ```json
752
+ { "file_path": "app.py", "hashline": [{ "start": "a3f9", "end": "7c1c", "new_string": "..." }] }
753
+ ```
754
+
755
+ The span is inclusive; omit `end` to replace one line, pass an empty
756
+ `new_string` to delete. Anchors are recomputed from disk when the edit runs, so
757
+ a file that moved under the model fails to resolve and the patch is rejected
758
+ rather than applied at stale coordinates. Exact-match `old_string` still works
759
+ and is unchanged; anchors are a second shape, not a replacement. Turn the whole
760
+ thing off with `"tools": {"hashline": false}` and reads go back to plain
761
+ `cat -n`.
762
+
763
+ **Symbols** answers structural questions with a parse tree rather than a
764
+ regex — three modes, `outline` for one file's shape, `definition` for where a
765
+ name is defined, `references` for where it is used:
766
+
767
+ ```
768
+ Symbols(mode="references", symbol="handle")
769
+ ```
770
+
771
+ Because it matches identifier nodes, the same word in a comment or a string
772
+ literal never matches. It cannot resolve types, so an unrelated symbol with the
773
+ same name in another file still does; it is tighter than `Grep`, not a language
774
+ server. Python, TypeScript, JavaScript, Go and Rust.
775
+
776
+ It needs grammars that are not installed by default, and the tool is simply not
777
+ registered without them — its schema never enters the cached prefix advertising
778
+ something that cannot run:
779
+
780
+ ```sh
781
+ uv tool install "hx-cli[symbols]"
782
+ ```
783
+
784
+ ---
785
+
697
786
  ## Extending it
698
787
 
699
788
  **Skills** are directories containing `SKILL.md` with YAML frontmatter:
@@ -750,6 +839,68 @@ Or `hx mcp add local python server.py`. Tools arrive namespaced
750
839
  with a per-server timeout; one that is broken or slow logs a warning and is
751
840
  dropped rather than taking the session with it.
752
841
 
842
+ **Hooks** are shell commands HX runs at named points in a turn. Four events:
843
+ `PreToolUse`, `PostToolUse`, `UserPromptSubmit` and `Stop`.
844
+
845
+ ```json
846
+ {
847
+ "hooks": {
848
+ "PreToolUse": [
849
+ {
850
+ "matcher": "Bash",
851
+ "hooks": [{ "type": "command", "command": "~/.hx/guard-bash.sh", "timeout": 10 }]
852
+ }
853
+ ],
854
+ "PostToolUse": [
855
+ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "ruff check --quiet" }] }
856
+ ]
857
+ }
858
+ }
859
+ ```
860
+
861
+ `matcher` is a regex matched against the whole tool name; omit it, or use `*`,
862
+ to match every tool. The hook reads a JSON event on stdin — `session_id`,
863
+ `cwd`, `hook_event_name`, and `tool_name`/`tool_input` where they apply — and
864
+ answers with its exit code:
865
+
866
+ | Exit | Means |
867
+ |---|---|
868
+ | `0` | allow |
869
+ | `2` | block. stderr becomes the reason the model is given |
870
+ | anything else | the hook is broken. Reported as a notice; the action proceeds |
871
+
872
+ That last row is deliberate: a typo in a shell command must not be able to
873
+ wedge a session. Only an explicit refusal stops anything.
874
+
875
+ A hook may also print JSON on stdout: `{"decision": "block", "reason": "..."}`
876
+ refuses, `{"updatedInput": {...}}` rewrites a tool's arguments before the
877
+ permission engine sees them, and `{"additionalContext": "..."}` adds a note for
878
+ the model to read. The format matches Claude Code's, so hooks already written
879
+ against that work here unchanged.
880
+
881
+ A worked example ships in the repository:
882
+ [`examples/hooks/check.sh`](https://github.com/aletisunil/hx/blob/main/examples/hooks/check.sh)
883
+ runs the checker a project already has — `ruff`, `tsc`, `go vet`, `cargo
884
+ check` — against the one file the model just edited, and returns whatever it
885
+ says as `additionalContext`. The model sees the errors appended to its own edit
886
+ result and fixes them on the next turn, rather than discovering them at test
887
+ time. It exits 0 either way: by `PostToolUse` the write has already landed, so
888
+ refusing it would be an answer to something that already happened.
889
+
890
+ ```sh
891
+ curl -o ~/.hx/check.sh https://raw.githubusercontent.com/aletisunil/hx/main/examples/hooks/check.sh
892
+ chmod +x ~/.hx/check.sh
893
+ ```
894
+
895
+ **Where hooks may be declared is a security boundary.** They are loaded from
896
+ `~/.hx/settings.json` and `~/.hx/projects/<slug>/settings.local.json` only.
897
+ Hooks in a project's checked-in `.hx/settings.json` are ignored and listed by
898
+ `/hooks`, because that file arrives with a clone and cloning a repository must
899
+ never be enough to run commands on the machine that cloned it. To adopt a
900
+ shared hook, copy it into your own settings — a deliberate act by the person
901
+ who will run it. Subagents inherit the parent's hooks, so a guard cannot be
902
+ bypassed by delegating through `Task`.
903
+
753
904
  ---
754
905
 
755
906
  ## Development
@@ -772,6 +923,10 @@ OPENROUTER_API_KEY=... uv run pytest -m live tests/test_live.py
772
923
  # the Codex route draws on your ChatGPT subscription instead
773
924
  hx auth login openai-codex
774
925
  uv run pytest -m live tests/test_live_codex.py
926
+
927
+ # and the Devin route on your Devin subscription
928
+ hx auth login devin
929
+ uv run pytest -m live tests/test_live_devin.py
775
930
  ```
776
931
 
777
932
  They are the only place the wire formats, streaming, tool use and a genuine
@@ -795,10 +950,11 @@ src/hx/
795
950
  cli.py config.py paths.py frontmatter.py git.py
796
951
  core/ loop, context assembly, compaction, late injection, sessions, usage
797
952
  auth/ credential store, OAuth flows, per-route resolution
798
- providers/ OpenRouter, Codex, the model catalogue, a scripted provider for tests
799
- tools/ Bash, Read, Write, Edit, Glob, Grep, TodoWrite, Task, WebSearch,
800
- WebFetch, output capping
953
+ providers/ OpenRouter, Codex, Devin, the model catalogue, a scripted provider for tests
954
+ tools/ Bash, Read, Write, Edit, Glob, Grep, Symbols, TodoWrite, Task,
955
+ WebSearch, WebFetch, line anchors, output capping
801
956
  permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
957
+ hooks/ the four events, the trust boundary, subprocess execution
802
958
  skills/ agents/ mcp/
803
959
  keys.py keybinding registry: ids, defaults, descriptions, user overrides
804
960
  term/ the renderer: raw mode, input decoding, the differ, the screen
@@ -861,12 +1017,13 @@ streaming with prefix caching and accurate cost accounting, session persistence
861
1017
  and resume, the tool suite, the permission engine and OS sandbox, late
862
1018
  injection, compaction, output capping, skills, subagents, MCP, and the TUI.
863
1019
 
864
- Two routes to a model: an OpenRouter API key, or a ChatGPT Plus/Pro
865
- subscription over OAuth. The model id decides which.
1020
+ Three routes to a model: an OpenRouter API key, a ChatGPT Plus/Pro
1021
+ subscription over OAuth, or a Devin subscription over OAuth. The model id
1022
+ decides which.
866
1023
 
867
1024
  Published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/), released
868
1025
  from CI on a tag.
869
1026
 
870
- The one thing still unproven in CI is a live call on either route: the `live`
871
- tests exist and cover both wire formats, tool use, reasoning replay and a
1027
+ The one thing still unproven in CI is a live call on any route: the `live`
1028
+ tests exist and cover all three wire formats, tool use, reasoning replay and a
872
1029
  genuine cache hit, but they need a credential and are deselected by default.