hx-cli 0.2.2__tar.gz → 0.2.4__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 (225) hide show
  1. hx_cli-0.2.4/.gitignore +19 -0
  2. {hx_cli-0.2.2 → hx_cli-0.2.4}/CHANGELOG.md +128 -1
  3. hx_cli-0.2.2/README.md → hx_cli-0.2.4/PKG-INFO +244 -7
  4. hx_cli-0.2.2/PKG-INFO → hx_cli-0.2.4/README.md +205 -42
  5. hx_cli-0.2.4/examples/hooks/check.sh +103 -0
  6. {hx_cli-0.2.2 → hx_cli-0.2.4}/pyproject.toml +17 -2
  7. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/__init__.py +1 -1
  8. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/agents/subagent.py +6 -0
  9. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/oauth/callback.py +13 -1
  10. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/store.py +16 -2
  11. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/cli.py +95 -1
  12. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/config.py +14 -2
  13. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/loop.py +171 -22
  14. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/session.py +69 -5
  15. hx_cli-0.2.4/src/hx/hooks/__init__.py +6 -0
  16. hx_cli-0.2.4/src/hx/hooks/engine.py +227 -0
  17. hx_cli-0.2.4/src/hx/hooks/spec.py +80 -0
  18. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/mcp/client.py +28 -1
  19. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/permissions/engine.py +130 -13
  20. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/permissions/sandbox.py +46 -6
  21. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/openrouter.py +16 -2
  22. hx_cli-0.2.4/src/hx/tools/anchors.py +125 -0
  23. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/base.py +12 -8
  24. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/bash.py +54 -17
  25. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/edit.py +124 -8
  26. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/read.py +38 -5
  27. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/registry.py +5 -0
  28. hx_cli-0.2.4/src/hx/tools/symbols.py +391 -0
  29. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/websearch.py +8 -0
  30. hx_cli-0.2.4/src/hx/trace.py +887 -0
  31. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/commands.py +135 -1
  32. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/renderers.py +17 -1
  33. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/runtime.py +1 -1
  34. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/agents/test_subagent.py +4 -3
  35. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/auth/test_codex_oauth.py +21 -0
  36. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/auth/test_store.py +58 -0
  37. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_loop.py +334 -1
  38. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_session.py +70 -0
  39. hx_cli-0.2.4/tests/hooks/test_engine.py +205 -0
  40. hx_cli-0.2.4/tests/hooks/test_example_check.py +98 -0
  41. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/mcp/fixtures/echo_server.py +22 -1
  42. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/mcp/test_manager.py +34 -0
  43. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/permissions/test_engine.py +111 -0
  44. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/permissions/test_sandbox.py +63 -0
  45. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_openrouter.py +65 -0
  46. hx_cli-0.2.4/tests/test_trace.py +515 -0
  47. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tools/test_background.py +64 -0
  48. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tools/test_bash.py +48 -0
  49. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tools/test_files.py +19 -3
  50. hx_cli-0.2.4/tests/tools/test_hashline.py +197 -0
  51. hx_cli-0.2.4/tests/tools/test_symbols.py +127 -0
  52. hx_cli-0.2.4/tests/tui/test_trace_command.py +257 -0
  53. hx_cli-0.2.4/tests/tui/views/__init__.py +0 -0
  54. {hx_cli-0.2.2 → hx_cli-0.2.4}/uv.lock +64 -12
  55. hx_cli-0.2.2/.gitignore +0 -16
  56. {hx_cli-0.2.2 → hx_cli-0.2.4}/.github/workflows/ci.yml +0 -0
  57. {hx_cli-0.2.2 → hx_cli-0.2.4}/install.sh +0 -0
  58. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/agents/__init__.py +0 -0
  59. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/agents/definitions.py +0 -0
  60. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/__init__.py +0 -0
  61. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/oauth/__init__.py +0 -0
  62. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/oauth/browser.py +0 -0
  63. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/oauth/codex.py +0 -0
  64. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/oauth/devin.py +0 -0
  65. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/oauth/pkce.py +0 -0
  66. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/resolve.py +0 -0
  67. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/__init__.py +0 -0
  68. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/checkpoints.py +0 -0
  69. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/compaction.py +0 -0
  70. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/context.py +0 -0
  71. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/events.py +0 -0
  72. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/lateinject.py +0 -0
  73. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/messages.py +0 -0
  74. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/title.py +0 -0
  75. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/usage.py +0 -0
  76. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/docs.py +0 -0
  77. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/frontmatter.py +0 -0
  78. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/git.py +0 -0
  79. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/keys.py +0 -0
  80. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/mcp/__init__.py +0 -0
  81. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/mcp/manager.py +0 -0
  82. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/net.py +0 -0
  83. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/paths.py +0 -0
  84. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/permissions/__init__.py +0 -0
  85. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/permissions/parser.py +0 -0
  86. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/__init__.py +0 -0
  87. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/base.py +0 -0
  88. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/codex.py +0 -0
  89. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/codex_catalogue.py +0 -0
  90. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/devin.py +0 -0
  91. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/devin_catalogue.py +0 -0
  92. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/devin_wire.py +0 -0
  93. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/fake.py +0 -0
  94. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/models.py +0 -0
  95. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/protowire.py +0 -0
  96. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/registry.py +0 -0
  97. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/responses_codec.py +0 -0
  98. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/skills/__init__.py +0 -0
  99. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/skills/loader.py +0 -0
  100. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/skills/runtime.py +0 -0
  101. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/__init__.py +0 -0
  102. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/ansi.py +0 -0
  103. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/buffer.py +0 -0
  104. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/component.py +0 -0
  105. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/demo.py +0 -0
  106. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/editor.py +0 -0
  107. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/keydecode.py +0 -0
  108. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/loop.py +0 -0
  109. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/markdown.py +0 -0
  110. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/primitives.py +0 -0
  111. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/sanitize.py +0 -0
  112. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/screen.py +0 -0
  113. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/syntax.py +0 -0
  114. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/terminal.py +0 -0
  115. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/undo.py +0 -0
  116. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/width.py +0 -0
  117. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/word_nav.py +0 -0
  118. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/__init__.py +0 -0
  119. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/glob.py +0 -0
  120. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/grep.py +0 -0
  121. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/output.py +0 -0
  122. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/task.py +0 -0
  123. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/todo.py +0 -0
  124. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/write.py +0 -0
  125. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/__init__.py +0 -0
  126. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/app.py +0 -0
  127. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/clipboard.py +0 -0
  128. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/format.py +0 -0
  129. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/fuzzy.py +0 -0
  130. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/glyphs.py +0 -0
  131. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/killring.py +0 -0
  132. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/limits.py +0 -0
  133. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/paint.py +0 -0
  134. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/roles.py +0 -0
  135. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/theme.py +0 -0
  136. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/theme_json.py +0 -0
  137. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/themes/ansi.json +0 -0
  138. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/themes/dark.json +0 -0
  139. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/themes/light.json +0 -0
  140. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/__init__.py +0 -0
  141. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/blocks.py +0 -0
  142. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/dialog.py +0 -0
  143. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/login.py +0 -0
  144. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/permission.py +0 -0
  145. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/pickers.py +0 -0
  146. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/prompt.py +0 -0
  147. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/status.py +0 -0
  148. {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/transcript.py +0 -0
  149. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/__init__.py +0 -0
  150. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/agents/__init__.py +0 -0
  151. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/auth/__init__.py +0 -0
  152. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/auth/test_devin_oauth.py +0 -0
  153. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/auth/test_pkce.py +0 -0
  154. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/auth/test_resolve.py +0 -0
  155. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/conftest.py +0 -0
  156. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/__init__.py +0 -0
  157. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_checkpoints.py +0 -0
  158. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_compaction.py +0 -0
  159. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_context.py +0 -0
  160. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_lateinject.py +0 -0
  161. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_rewind.py +0 -0
  162. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_settings_layers.py +0 -0
  163. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_settings_migration.py +0 -0
  164. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_steering.py +0 -0
  165. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_title.py +0 -0
  166. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_usage.py +0 -0
  167. {hx_cli-0.2.2/tests/mcp → hx_cli-0.2.4/tests/hooks}/__init__.py +0 -0
  168. {hx_cli-0.2.2/tests/permissions → hx_cli-0.2.4/tests/mcp}/__init__.py +0 -0
  169. {hx_cli-0.2.2/tests/providers → hx_cli-0.2.4/tests/permissions}/__init__.py +0 -0
  170. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/permissions/test_parser.py +0 -0
  171. {hx_cli-0.2.2/tests/skills → hx_cli-0.2.4/tests/providers}/__init__.py +0 -0
  172. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_catalogue.py +0 -0
  173. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_codex.py +0 -0
  174. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_codex_catalogue.py +0 -0
  175. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_devin.py +0 -0
  176. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_devin_catalogue.py +0 -0
  177. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_protowire.py +0 -0
  178. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_registry.py +0 -0
  179. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_responses_codec.py +0 -0
  180. {hx_cli-0.2.2/tests/term → hx_cli-0.2.4/tests/skills}/__init__.py +0 -0
  181. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/skills/test_loader.py +0 -0
  182. {hx_cli-0.2.2/tests/tools → hx_cli-0.2.4/tests/term}/__init__.py +0 -0
  183. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/conftest.py +0 -0
  184. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_ansi.py +0 -0
  185. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_keydecode.py +0 -0
  186. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_loop.py +0 -0
  187. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_markdown.py +0 -0
  188. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_primitives.py +0 -0
  189. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_sanitize.py +0 -0
  190. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_screen.py +0 -0
  191. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_terminal.py +0 -0
  192. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_width.py +0 -0
  193. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_cli.py +0 -0
  194. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_docs.py +0 -0
  195. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_git.py +0 -0
  196. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_install.py +0 -0
  197. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_integration.py +0 -0
  198. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_live.py +0 -0
  199. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_live_codex.py +0 -0
  200. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_live_devin.py +0 -0
  201. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_live_tavily.py +0 -0
  202. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_net.py +0 -0
  203. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_paths.py +0 -0
  204. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_readme.py +0 -0
  205. {hx_cli-0.2.2/tests/tui → hx_cli-0.2.4/tests/tools}/__init__.py +0 -0
  206. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tools/test_output.py +0 -0
  207. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tools/test_todo.py +0 -0
  208. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tools/test_websearch.py +0 -0
  209. {hx_cli-0.2.2/tests/tui/views → hx_cli-0.2.4/tests/tui}/__init__.py +0 -0
  210. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/support.py +0 -0
  211. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/test_autocomplete.py +0 -0
  212. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/test_clipboard.py +0 -0
  213. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/test_effort.py +0 -0
  214. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/test_keys.py +0 -0
  215. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/test_login.py +0 -0
  216. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/test_renderers.py +0 -0
  217. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/test_session.py +0 -0
  218. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/test_theme_json.py +0 -0
  219. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/views/test_blocks.py +0 -0
  220. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/views/test_permission.py +0 -0
  221. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/views/test_pickers.py +0 -0
  222. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/views/test_prompt.py +0 -0
  223. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/views/test_runtime.py +0 -0
  224. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/views/test_status.py +0 -0
  225. {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/views/test_transcript.py +0 -0
@@ -0,0 +1,19 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+ .DS_Store
14
+ .hx/sessions/
15
+ # Not .hx/settings.json: that file is the project's *shared* configuration and
16
+ # is meant to be checked in. What HX writes on your behalf - permission grants
17
+ # and the rest - lives under $HX_HOME/projects/<slug>, outside the checkout
18
+ # entirely, so there is nothing here left to hide.
19
+ snapshot_report.html
@@ -28,6 +28,131 @@ the project follows [semantic versioning](https://semver.org/).
28
28
 
29
29
  ---
30
30
 
31
+ ## [0.2.4] - 2026-09-19
32
+
33
+ ### Added
34
+
35
+ - `/trace [path]`: write the whole session to one self-contained HTML page and
36
+ open it. The system prompt, the project context, the skills index, every tool
37
+ schema, every prompt and reply, every thinking block, every tool call beside
38
+ the result it produced, the per-call token ledger and the files HX changed.
39
+ Late-injected reminders and compacted turns are included behind a toggle.
40
+ A session still running traces as it stands, including the turn that just
41
+ landed. The page follows your system's dark or light setting and a button
42
+ overrides it; nothing is fetched and nothing is uploaded.
43
+ - `hx trace [SESSION_ID] [PATH]`: the same page for any transcript on the
44
+ machine, without starting a session. Defaults to the last session in this
45
+ directory and prints only the path, so `open "$(hx trace)"` works. Either
46
+ argument may be given alone - a lone one that looks like a path is one.
47
+ - The system prompt and the tool schemas are now written to the transcript on a
48
+ session's first provider call, so a trace of a finished session can say what
49
+ the model was told and not only what it said.
50
+
51
+ ### Security
52
+
53
+ - Permission rules are matched against the path a specifier resolves to, not
54
+ the text it was written as. `deny: Read(~/.ssh/**)` was got past by asking
55
+ for `../../.ssh/id_rsa`, or through a symlink planted in the project. The
56
+ same resolution stops an allow rule leaking: `allow: Edit(src/**)` used to
57
+ cover `src/../../etc/passwd`.
58
+ - `WebFetch` is matched against every URL it was given rather than the first,
59
+ so `deny: WebFetch(https://host/**)` is no longer a question of list order.
60
+ The approval prompt spells the URLs out too; it said `urls=[2 items]`, which
61
+ answered the only question that mattered with a number.
62
+ - The generated Seatbelt profile no longer grants write access to all of
63
+ `/private/var/folders` - every user's and every process's temp and cache.
64
+ `$TMPDIR` was resolved precisely to avoid that, and the profile handed it
65
+ back on the next line. This user's own container stays writable.
66
+ - `~/.hx/auth.json` is created with mode 0600 rather than narrowed to it after
67
+ the tokens are written, closing a window at a predictable path where the
68
+ credential was readable by any local account.
69
+ - The OAuth callback page escapes what the provider sent. An `error` parameter
70
+ carrying markup ran as HTML on a loopback origin that is otherwise the
71
+ user's own.
72
+
73
+ ### Fixed
74
+
75
+ - Slash commands read the session that is in force rather than the one HX
76
+ opened with. After `/resume`, `/trace` wrote a page of zeros into the empty
77
+ startup session's directory and `/cost` reported nobody's usage: the command
78
+ context had captured the session object at startup, and `/resume` rebinds it.
79
+ - A session whose title was the trace template's own data placeholder had the
80
+ whole payload spliced into the page's `<title>`.
81
+ - A trace labelled an encrypted reasoning block "thinking (signed)", which read
82
+ as though the page had dropped the rest of it. It now says the text is the
83
+ provider's summary and the reasoning came back encrypted, and a block that
84
+ arrived with no summary at all says that rather than rendering an empty box.
85
+ - MCP servers survive a large response. One JSON-RPC message is one line and
86
+ asyncio's stream limit is 64 KiB, so a tool returning a file did not truncate
87
+ - it raised out of the read loop and killed the connection for the rest of
88
+ the session. The limit is now 16 MiB, and overshooting it says so.
89
+ - A tool call is no longer dropped when a stream ends without a `finish_reason`.
90
+ The arguments were buffered until that field arrived, so a complete call was
91
+ discarded and the turn reported `end_turn`: the model appeared to stop for no
92
+ reason, and the tokens were already paid for.
93
+ - The Linux sandbox starts again for anyone with credential *files*. Denied
94
+ paths were all shadowed with `--tmpfs`, which mounts a directory and aborts
95
+ bwrap when aimed at a file - and six of them are files, `~/.hx/auth.json`
96
+ among them. Files are shadowed with `/dev/null` instead. bwrap also starts in
97
+ the policy's directory rather than whatever `$PWD` happened to hold.
98
+ - `PersistentShell.cwd` follows the shell. It claimed to track it and was in
99
+ fact the launch directory forever, which meant a background job always
100
+ started where the session opened rather than where `cd` had left it.
101
+ - A background job no longer leaks its log file descriptor for the life of the
102
+ session.
103
+ - A record appended while the transcript was being written is no longer
104
+ dropped. The queue is taken in one step, and a failed write puts its records
105
+ back rather than losing them.
106
+ - An automatic compaction with nothing to summarise is no longer announced and
107
+ rerun every turn for the rest of the session. `/compact` still answers either
108
+ way.
109
+ - `/trace` takes `--no-open`, and never launches a *terminal* browser: with no
110
+ display, `webbrowser` falls through to lynx or w3m, which opens in the
111
+ terminal HX is drawing on and takes the session display with it.
112
+
113
+ ---
114
+
115
+ ## [0.2.3] - 2026-09-18
116
+
117
+ ### Added
118
+
119
+ - Hooks: shell commands HX runs at `PreToolUse`, `PostToolUse`,
120
+ `UserPromptSubmit` and `Stop`. A hook can refuse an action (exit 2, or
121
+ `{"decision": "block"}`), rewrite a tool's arguments before the permission
122
+ engine sees them (`updatedInput`), or add a note for the model
123
+ (`additionalContext`). Configure them under `"hooks"` in
124
+ `~/.hx/settings.json`; `/hooks` lists what is loaded. Hooks declared in a
125
+ project's checked-in `.hx/settings.json` are ignored and reported, because
126
+ cloning a repository must not be enough to run commands on your machine.
127
+ - `Symbols`: outline a file, find where a name is defined, or find where it is
128
+ used, over a tree-sitter parse tree rather than a regex - so a match inside a
129
+ comment or a string never counts. Python, TypeScript, JavaScript, Go and
130
+ Rust. Needs the grammars: `uv tool install "hx-cli[symbols]"`. Without them
131
+ the tool is not registered at all.
132
+ - An example hook, `examples/hooks/check.sh`: after every `Edit`/`Write` it
133
+ runs the project's own checker (`ruff`, `tsc`, `go vet`, `cargo check`)
134
+ against just that file and hands the output back to the model, so a typo is
135
+ caught on the next turn instead of at test time.
136
+ - Line anchors. `Read` now labels each line with a short content hash, and
137
+ `Edit` accepts those anchors in a `hashline` argument to replace a span
138
+ without retyping it. An anchor that no longer resolves means the file moved,
139
+ so the patch is rejected instead of landing at the wrong lines. Set
140
+ `"tools": {"hashline": false}` to go back to plain `cat -n` reads.
141
+
142
+ ### Removed
143
+
144
+ - The `telemetry` setting, which was parsed and never read by anything.
145
+
146
+ ### Fixed
147
+
148
+ - The README's Status section said there were two model routes; Devin made it
149
+ three in 0.2.2.
150
+ - `MultiEdit` and `NotebookEdit` were listed as mutating tools and as edit
151
+ tools in the permission engine, but no such tools exist. A rule written
152
+ against either name silently matched nothing.
153
+
154
+ ---
155
+
31
156
  ## [0.2.2] - 2026-09-17
32
157
 
33
158
  ### Added
@@ -440,7 +565,9 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
440
565
  - `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
441
566
  self-update, and `install.sh` bootstrapping uv with a pinned Python.
442
567
 
443
- [Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.2...HEAD
568
+ [Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.4...HEAD
569
+ [0.2.4]: https://github.com/aletisunil/hx/compare/v0.2.3...v0.2.4
570
+ [0.2.3]: https://github.com/aletisunil/hx/compare/v0.2.2...v0.2.3
444
571
  [0.2.2]: https://github.com/aletisunil/hx/compare/v0.2.1...v0.2.2
445
572
  [0.2.1]: https://github.com/aletisunil/hx/compare/v0.2.0...v0.2.1
446
573
  [0.2.0]: https://github.com/aletisunil/hx/compare/v0.1.9...v0.2.0
@@ -1,3 +1,42 @@
1
+ Metadata-Version: 2.5
2
+ Name: hx-cli
3
+ Version: 0.2.4
4
+ Summary: HX - an agent harness for the terminal
5
+ Project-URL: Homepage, https://hx.sunilaleti.dev
6
+ Project-URL: Documentation, https://hx.sunilaleti.dev
7
+ Project-URL: Source, https://github.com/aletisunil/hx
8
+ Project-URL: Issues, https://github.com/aletisunil/hx/issues
9
+ Author: Sunil Aleti
10
+ License: MIT
11
+ Keywords: agent,cli,coding-agent,llm,openrouter,tui
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: httpx>=0.27
20
+ Requires-Dist: markdown-it-py>=3.0
21
+ Requires-Dist: pydantic>=2.7
22
+ Requires-Dist: pygments>=2.17
23
+ Requires-Dist: pyyaml>=6.0
24
+ Requires-Dist: truststore>=0.10
25
+ Requires-Dist: wcwidth>=0.2.13
26
+ Provides-Extra: dev
27
+ Requires-Dist: mypy>=1.11; extra == 'dev'
28
+ Requires-Dist: pyte>=0.8; extra == 'dev'
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
30
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
31
+ Requires-Dist: pytest>=8.2; extra == 'dev'
32
+ Requires-Dist: ruff>=0.6; extra == 'dev'
33
+ Requires-Dist: types-pygments; extra == 'dev'
34
+ Requires-Dist: types-pyyaml; extra == 'dev'
35
+ Provides-Extra: symbols
36
+ Requires-Dist: tree-sitter-language-pack>=0.7; extra == 'symbols'
37
+ Requires-Dist: tree-sitter>=0.23; extra == 'symbols'
38
+ Description-Content-Type: text/markdown
39
+
1
40
  # HX
2
41
 
3
42
  An agent harness for the terminal. Python core, its own terminal renderer,
@@ -260,6 +299,9 @@ hx -p "explain this repo" # headless: streams to stdout, tool activity to stde
260
299
  hx resume # resume the last session here
261
300
  hx resume <session-id> # resume a specific one
262
301
  hx prompt # print the system prompt this directory would use
302
+ hx trace # write the last session here to an HTML page
303
+ hx trace out.html # the same, to a path you choose
304
+ hx trace <id> out.html # a specific session, to a path you choose
263
305
  hx --model openai/gpt-5 # override the model for one run
264
306
  hx --mode plan # start read-only
265
307
  hx --cwd ../other-project # run against a different directory
@@ -403,9 +445,11 @@ than being silently resolved.
403
445
  | `/skills` | installed skills |
404
446
  | `/agents` | subagent types |
405
447
  | `/mcp` | server status |
448
+ | `/hooks` | configured hooks, and any that were refused |
406
449
  | `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
407
450
  | `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
408
451
  | `/copy` | copy the last reply to the clipboard |
452
+ | `/trace [path]` | write the whole session to a self-contained HTML page |
409
453
  | `/fullscreen [on\|off]` | take the whole window, with the prompt pinned to the bottom |
410
454
  | `/mouse [on\|off]` | mouse reporting, and with it your terminal's own text selection |
411
455
  | `/init` | generate an `AGENTS.md` for the project |
@@ -520,7 +564,6 @@ along with the `.hx/.gitignore` it added to hide it.
520
564
  {
521
565
  "theme": "dark", // dark | light | ansi, or a file in ~/.hx/themes
522
566
  "quiet_startup": false, // skip the startup header
523
- "telemetry": false,
524
567
 
525
568
  "models": {
526
569
  "model": "anthropic/claude-sonnet-4.5",
@@ -563,10 +606,18 @@ along with the `.hx/.gitignore` it added to hide it.
563
606
  "tui": {
564
607
  "enterWhileBusy": "queue", // queue | steer - what enter does mid-turn
565
608
  "fullscreen": false // true takes the whole window (see /fullscreen)
609
+ },
610
+
611
+ "tools": {
612
+ "hashline": true // Read labels lines with content anchors Edit accepts
566
613
  }
567
614
  }
568
615
  ```
569
616
 
617
+ `hooks` is configured in the same files but is deliberately not part of this
618
+ merge: it is read per layer, and only from your own settings. See
619
+ [Extending it](#extending-it).
620
+
570
621
  Environment overrides: `HX_MODEL`, `HX_SUBAGENT_MODEL`, `HX_MAX_TOKENS`,
571
622
  `HX_PERMISSION_MODE`, `HX_SANDBOX`, `HX_COMPACT_AT`, `HX_GIT_NOTICES`,
572
623
  `HX_THEME`, `HX_QUIET_STARTUP`. Also `HX_HOME` to relocate user state, `HX_TAVILY_API_KEY`
@@ -644,6 +695,7 @@ files load here unchanged. `src/hx/tui/themes/dark.json` is the reference.
644
695
  | `~/.hx/system-prompt.md` | your system prompt, replacing the built-in one |
645
696
  | `~/.hx/system-prompt-append.md` | text appended to whichever prompt is in force |
646
697
  | `~/.hx/sessions/` | transcripts, spilled tool output, subagent sessions |
698
+ | `~/.hx/sessions/<id>/trace.html` | where `/trace` writes, unless you name a path |
647
699
  | `~/.hx/skills/`, `~/.hx/agents/` | your skills and agents |
648
700
  | `~/.hx/projects/<project>/settings.local.json` | this machine's settings for one project - where "always allow" lands |
649
701
  | `./.hx/settings.json` | project settings, checked in if you like — read by HX, never written |
@@ -727,6 +779,127 @@ rest to the session directory, and hands the model that path to grep.
727
779
 
728
780
  ---
729
781
 
782
+ ## Tracing a session
783
+
784
+ `/trace` writes everything that happened to one self-contained HTML file and
785
+ opens it in a browser:
786
+
787
+ ```
788
+ /trace # ~/.hx/sessions/<id>/trace.html
789
+ /trace ~/Desktop/bug.html # somewhere you choose
790
+ ```
791
+
792
+ Everything means everything. The system prompt, the project context, the skills
793
+ index, every tool schema as the provider received it, every prompt, every
794
+ reply, every thinking block, every tool call next to the result it produced,
795
+ the per-call token ledger, and the files HX changed. Late-injected reminders
796
+ and compacted turns are in there too, behind a toggle that says how many there
797
+ are — a trace that hides the reminders and the superseded turns is not a trace
798
+ of the session that ran.
799
+
800
+ Nothing is summarised and nothing is prettified. Assistant text is shown as it
801
+ arrived, backticks and all, because a trace exists to answer "what actually
802
+ went through" and a rendered version of that is a different document. Long tool
803
+ results are folded rather than truncated, with the character count on the fold.
804
+
805
+ **Thinking, where the provider allows it.** How much of a model's reasoning HX
806
+ ever sees is the provider's decision, not HX's. Anthropic's models return the
807
+ thinking itself. The Responses API - the OpenAI and Codex routes - returns a
808
+ one-line summary in plaintext and the reasoning encrypted, to be replayed on
809
+ the next call and read by nobody, HX included. A trace labels which of the two
810
+ it is holding, because a one-line thinking block is otherwise indistinguishable
811
+ from a trace that dropped the rest.
812
+
813
+ A running session traces as it stands right now, including the turn that just
814
+ landed. The page says so, because a trace of a live session is a snapshot.
815
+
816
+ **Reading it.** Long tool results are folded and the reference panels - the
817
+ prompt, the schemas, the ledger - start shut, so the transcript is what you
818
+ land on. "Expand all" opens the whole page, panels included, and turns into
819
+ "Collapse all". A session with no turns yet opens its panels on its own, since
820
+ there is nothing else to read.
821
+
822
+ **Dark or light.** The page follows your system by default and a button in the
823
+ corner overrides it, remembered per browser. No stylesheet is fetched to do it.
824
+
825
+ **Nothing is uploaded and nothing is fetched.** The payload, the stylesheet and
826
+ the script all live in the file, so it opens offline, renders the same a year
827
+ later, and a trace of a private repository stays as private as the directory
828
+ you wrote it into. The embedded payload is also the machine-readable trace: it
829
+ is JSON, in a `<script id="trace-data">` element, and `/trace` is the only way
830
+ HX will ever put your transcript anywhere but `~/.hx`.
831
+
832
+ From outside a session, on any transcript on the machine:
833
+
834
+ ```sh
835
+ hx trace # the last session in this directory
836
+ hx trace 20260918-214743-b5b8f9 # one by id
837
+ hx trace ~/somewhere.html # the last session, to a path you choose
838
+ hx trace <id> ~/somewhere.html # both, to a path or a directory
839
+ open "$(hx trace)" # the path is all that goes to stdout
840
+ ```
841
+
842
+ `hx trace` reads the transcript and runs nothing, so it works on a session that
843
+ ended months ago and on one another terminal has open right now.
844
+
845
+ **What a trace can be missing.** The prompt and the tool schemas are written to
846
+ the transcript on the session's first provider call — the first moment the tool
847
+ registry has settled, because MCP servers connect after the loop is built. A
848
+ session traced before that call, or one recorded by a version of HX older than
849
+ this feature, has the transcript but not the prompt, and the page leaves those
850
+ panels out rather than guessing. `/trace` inside a live session fills them in
851
+ from the loop either way.
852
+
853
+ ---
854
+
855
+ ## Reading and editing code
856
+
857
+ **Line anchors.** `Read` labels every line with a short content hash:
858
+
859
+ ```
860
+ 1 a3f9 def handle(request):
861
+ 2 7c1c if request.method == "POST":
862
+ ```
863
+
864
+ The anchor is a hash of the line and its two neighbours, truncated to the
865
+ narrowest width that is unambiguous in that file. `Edit` takes those anchors
866
+ instead of retyped content:
867
+
868
+ ```json
869
+ { "file_path": "app.py", "hashline": [{ "start": "a3f9", "end": "7c1c", "new_string": "..." }] }
870
+ ```
871
+
872
+ The span is inclusive; omit `end` to replace one line, pass an empty
873
+ `new_string` to delete. Anchors are recomputed from disk when the edit runs, so
874
+ a file that moved under the model fails to resolve and the patch is rejected
875
+ rather than applied at stale coordinates. Exact-match `old_string` still works
876
+ and is unchanged; anchors are a second shape, not a replacement. Turn the whole
877
+ thing off with `"tools": {"hashline": false}` and reads go back to plain
878
+ `cat -n`.
879
+
880
+ **Symbols** answers structural questions with a parse tree rather than a
881
+ regex — three modes, `outline` for one file's shape, `definition` for where a
882
+ name is defined, `references` for where it is used:
883
+
884
+ ```
885
+ Symbols(mode="references", symbol="handle")
886
+ ```
887
+
888
+ Because it matches identifier nodes, the same word in a comment or a string
889
+ literal never matches. It cannot resolve types, so an unrelated symbol with the
890
+ same name in another file still does; it is tighter than `Grep`, not a language
891
+ server. Python, TypeScript, JavaScript, Go and Rust.
892
+
893
+ It needs grammars that are not installed by default, and the tool is simply not
894
+ registered without them — its schema never enters the cached prefix advertising
895
+ something that cannot run:
896
+
897
+ ```sh
898
+ uv tool install "hx-cli[symbols]"
899
+ ```
900
+
901
+ ---
902
+
730
903
  ## Extending it
731
904
 
732
905
  **Skills** are directories containing `SKILL.md` with YAML frontmatter:
@@ -783,6 +956,68 @@ Or `hx mcp add local python server.py`. Tools arrive namespaced
783
956
  with a per-server timeout; one that is broken or slow logs a warning and is
784
957
  dropped rather than taking the session with it.
785
958
 
959
+ **Hooks** are shell commands HX runs at named points in a turn. Four events:
960
+ `PreToolUse`, `PostToolUse`, `UserPromptSubmit` and `Stop`.
961
+
962
+ ```json
963
+ {
964
+ "hooks": {
965
+ "PreToolUse": [
966
+ {
967
+ "matcher": "Bash",
968
+ "hooks": [{ "type": "command", "command": "~/.hx/guard-bash.sh", "timeout": 10 }]
969
+ }
970
+ ],
971
+ "PostToolUse": [
972
+ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "ruff check --quiet" }] }
973
+ ]
974
+ }
975
+ }
976
+ ```
977
+
978
+ `matcher` is a regex matched against the whole tool name; omit it, or use `*`,
979
+ to match every tool. The hook reads a JSON event on stdin — `session_id`,
980
+ `cwd`, `hook_event_name`, and `tool_name`/`tool_input` where they apply — and
981
+ answers with its exit code:
982
+
983
+ | Exit | Means |
984
+ |---|---|
985
+ | `0` | allow |
986
+ | `2` | block. stderr becomes the reason the model is given |
987
+ | anything else | the hook is broken. Reported as a notice; the action proceeds |
988
+
989
+ That last row is deliberate: a typo in a shell command must not be able to
990
+ wedge a session. Only an explicit refusal stops anything.
991
+
992
+ A hook may also print JSON on stdout: `{"decision": "block", "reason": "..."}`
993
+ refuses, `{"updatedInput": {...}}` rewrites a tool's arguments before the
994
+ permission engine sees them, and `{"additionalContext": "..."}` adds a note for
995
+ the model to read. The format matches Claude Code's, so hooks already written
996
+ against that work here unchanged.
997
+
998
+ A worked example ships in the repository:
999
+ [`examples/hooks/check.sh`](https://github.com/aletisunil/hx/blob/main/examples/hooks/check.sh)
1000
+ runs the checker a project already has — `ruff`, `tsc`, `go vet`, `cargo
1001
+ check` — against the one file the model just edited, and returns whatever it
1002
+ says as `additionalContext`. The model sees the errors appended to its own edit
1003
+ result and fixes them on the next turn, rather than discovering them at test
1004
+ time. It exits 0 either way: by `PostToolUse` the write has already landed, so
1005
+ refusing it would be an answer to something that already happened.
1006
+
1007
+ ```sh
1008
+ curl -o ~/.hx/check.sh https://raw.githubusercontent.com/aletisunil/hx/main/examples/hooks/check.sh
1009
+ chmod +x ~/.hx/check.sh
1010
+ ```
1011
+
1012
+ **Where hooks may be declared is a security boundary.** They are loaded from
1013
+ `~/.hx/settings.json` and `~/.hx/projects/<slug>/settings.local.json` only.
1014
+ Hooks in a project's checked-in `.hx/settings.json` are ignored and listed by
1015
+ `/hooks`, because that file arrives with a clone and cloning a repository must
1016
+ never be enough to run commands on the machine that cloned it. To adopt a
1017
+ shared hook, copy it into your own settings — a deliberate act by the person
1018
+ who will run it. Subagents inherit the parent's hooks, so a guard cannot be
1019
+ bypassed by delegating through `Task`.
1020
+
786
1021
  ---
787
1022
 
788
1023
  ## Development
@@ -833,9 +1068,10 @@ src/hx/
833
1068
  core/ loop, context assembly, compaction, late injection, sessions, usage
834
1069
  auth/ credential store, OAuth flows, per-route resolution
835
1070
  providers/ OpenRouter, Codex, Devin, the model catalogue, a scripted provider for tests
836
- tools/ Bash, Read, Write, Edit, Glob, Grep, TodoWrite, Task, WebSearch,
837
- WebFetch, output capping
1071
+ tools/ Bash, Read, Write, Edit, Glob, Grep, Symbols, TodoWrite, Task,
1072
+ WebSearch, WebFetch, line anchors, output capping
838
1073
  permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
1074
+ hooks/ the four events, the trust boundary, subprocess execution
839
1075
  skills/ agents/ mcp/
840
1076
  keys.py keybinding registry: ids, defaults, descriptions, user overrides
841
1077
  term/ the renderer: raw mode, input decoding, the differ, the screen
@@ -898,12 +1134,13 @@ streaming with prefix caching and accurate cost accounting, session persistence
898
1134
  and resume, the tool suite, the permission engine and OS sandbox, late
899
1135
  injection, compaction, output capping, skills, subagents, MCP, and the TUI.
900
1136
 
901
- Two routes to a model: an OpenRouter API key, or a ChatGPT Plus/Pro
902
- subscription over OAuth. The model id decides which.
1137
+ Three routes to a model: an OpenRouter API key, a ChatGPT Plus/Pro
1138
+ subscription over OAuth, or a Devin subscription over OAuth. The model id
1139
+ decides which.
903
1140
 
904
1141
  Published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/), released
905
1142
  from CI on a tag.
906
1143
 
907
- The one thing still unproven in CI is a live call on either route: the `live`
908
- tests exist and cover both wire formats, tool use, reasoning replay and a
1144
+ The one thing still unproven in CI is a live call on any route: the `live`
1145
+ tests exist and cover all three wire formats, tool use, reasoning replay and a
909
1146
  genuine cache hit, but they need a credential and are deselected by default.