hx-cli 0.2.2__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.
- {hx_cli-0.2.2 → hx_cli-0.2.3}/CHANGELOG.md +43 -1
- {hx_cli-0.2.2 → hx_cli-0.2.3}/PKG-INFO +131 -9
- {hx_cli-0.2.2 → hx_cli-0.2.3}/README.md +127 -7
- hx_cli-0.2.3/examples/hooks/check.sh +103 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/pyproject.toml +14 -1
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/__init__.py +1 -1
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/agents/subagent.py +6 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/cli.py +14 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/config.py +14 -2
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/loop.py +98 -7
- hx_cli-0.2.3/src/hx/hooks/__init__.py +6 -0
- hx_cli-0.2.3/src/hx/hooks/engine.py +227 -0
- hx_cli-0.2.3/src/hx/hooks/spec.py +80 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/permissions/engine.py +2 -2
- hx_cli-0.2.3/src/hx/tools/anchors.py +125 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/edit.py +124 -8
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/read.py +38 -5
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/registry.py +5 -0
- hx_cli-0.2.3/src/hx/tools/symbols.py +391 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/commands.py +33 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/renderers.py +17 -1
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/agents/test_subagent.py +4 -3
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_loop.py +128 -0
- hx_cli-0.2.3/tests/hooks/test_engine.py +205 -0
- hx_cli-0.2.3/tests/hooks/test_example_check.py +98 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tools/test_files.py +19 -3
- hx_cli-0.2.3/tests/tools/test_hashline.py +197 -0
- hx_cli-0.2.3/tests/tools/test_symbols.py +127 -0
- hx_cli-0.2.3/tests/tui/views/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/uv.lock +64 -12
- {hx_cli-0.2.2 → hx_cli-0.2.3}/.github/workflows/ci.yml +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/.gitignore +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/install.sh +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/agents/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/agents/definitions.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/oauth/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/oauth/browser.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/oauth/callback.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/oauth/codex.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/oauth/devin.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/oauth/pkce.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/resolve.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/store.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/checkpoints.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/compaction.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/context.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/events.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/lateinject.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/messages.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/session.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/title.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/usage.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/docs.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/frontmatter.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/git.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/keys.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/mcp/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/mcp/client.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/mcp/manager.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/net.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/paths.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/permissions/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/permissions/parser.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/permissions/sandbox.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/base.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/codex.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/codex_catalogue.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/devin.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/devin_catalogue.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/devin_wire.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/fake.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/models.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/openrouter.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/protowire.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/registry.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/responses_codec.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/skills/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/skills/loader.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/skills/runtime.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/ansi.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/buffer.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/component.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/demo.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/editor.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/keydecode.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/loop.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/markdown.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/primitives.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/sanitize.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/screen.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/syntax.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/terminal.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/undo.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/width.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/word_nav.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/base.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/bash.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/glob.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/grep.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/output.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/task.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/todo.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/websearch.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/write.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/app.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/clipboard.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/format.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/fuzzy.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/glyphs.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/killring.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/limits.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/paint.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/roles.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/runtime.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/theme.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/theme_json.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/themes/ansi.json +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/themes/dark.json +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/themes/light.json +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/blocks.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/dialog.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/login.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/permission.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/pickers.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/prompt.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/status.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/transcript.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/agents/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/auth/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/auth/test_codex_oauth.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/auth/test_devin_oauth.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/auth/test_pkce.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/auth/test_resolve.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/auth/test_store.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/conftest.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_checkpoints.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_compaction.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_context.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_lateinject.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_rewind.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_session.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_settings_layers.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_settings_migration.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_steering.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_title.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_usage.py +0 -0
- {hx_cli-0.2.2/tests/mcp → hx_cli-0.2.3/tests/hooks}/__init__.py +0 -0
- {hx_cli-0.2.2/tests/permissions → hx_cli-0.2.3/tests/mcp}/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/mcp/fixtures/echo_server.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/mcp/test_manager.py +0 -0
- {hx_cli-0.2.2/tests/providers → hx_cli-0.2.3/tests/permissions}/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/permissions/test_engine.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/permissions/test_parser.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/permissions/test_sandbox.py +0 -0
- {hx_cli-0.2.2/tests/skills → hx_cli-0.2.3/tests/providers}/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_catalogue.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_codex.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_codex_catalogue.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_devin.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_devin_catalogue.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_openrouter.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_protowire.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_registry.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_responses_codec.py +0 -0
- {hx_cli-0.2.2/tests/term → hx_cli-0.2.3/tests/skills}/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/skills/test_loader.py +0 -0
- {hx_cli-0.2.2/tests/tools → hx_cli-0.2.3/tests/term}/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/conftest.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_ansi.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_keydecode.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_loop.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_markdown.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_primitives.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_sanitize.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_screen.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_terminal.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_width.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_cli.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_docs.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_git.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_install.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_integration.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_live.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_live_codex.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_live_devin.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_live_tavily.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_net.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_paths.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_readme.py +0 -0
- {hx_cli-0.2.2/tests/tui → hx_cli-0.2.3/tests/tools}/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tools/test_background.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tools/test_bash.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tools/test_output.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tools/test_todo.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tools/test_websearch.py +0 -0
- {hx_cli-0.2.2/tests/tui/views → hx_cli-0.2.3/tests/tui}/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/support.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/test_autocomplete.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/test_clipboard.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/test_effort.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/test_keys.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/test_login.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/test_renderers.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/test_session.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/test_theme_json.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/views/test_blocks.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/views/test_permission.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/views/test_pickers.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/views/test_prompt.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/views/test_runtime.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/views/test_status.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/views/test_transcript.py +0 -0
|
@@ -28,6 +28,47 @@ 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
|
+
|
|
31
72
|
## [0.2.2] - 2026-09-17
|
|
32
73
|
|
|
33
74
|
### Added
|
|
@@ -440,7 +481,8 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
|
|
|
440
481
|
- `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
|
|
441
482
|
self-update, and `install.sh` bootstrapping uv with a pinned Python.
|
|
442
483
|
|
|
443
|
-
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.
|
|
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
|
|
444
486
|
[0.2.2]: https://github.com/aletisunil/hx/compare/v0.2.1...v0.2.2
|
|
445
487
|
[0.2.1]: https://github.com/aletisunil/hx/compare/v0.2.0...v0.2.1
|
|
446
488
|
[0.2.0]: https://github.com/aletisunil/hx/compare/v0.1.9...v0.2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hx-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
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
|
|
@@ -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,6 +30,9 @@ 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
|
|
@@ -438,6 +440,7 @@ than being silently resolved.
|
|
|
438
440
|
| `/skills` | installed skills |
|
|
439
441
|
| `/agents` | subagent types |
|
|
440
442
|
| `/mcp` | server status |
|
|
443
|
+
| `/hooks` | configured hooks, and any that were refused |
|
|
441
444
|
| `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
|
|
442
445
|
| `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
|
|
443
446
|
| `/copy` | copy the last reply to the clipboard |
|
|
@@ -555,7 +558,6 @@ along with the `.hx/.gitignore` it added to hide it.
|
|
|
555
558
|
{
|
|
556
559
|
"theme": "dark", // dark | light | ansi, or a file in ~/.hx/themes
|
|
557
560
|
"quiet_startup": false, // skip the startup header
|
|
558
|
-
"telemetry": false,
|
|
559
561
|
|
|
560
562
|
"models": {
|
|
561
563
|
"model": "anthropic/claude-sonnet-4.5",
|
|
@@ -598,10 +600,18 @@ along with the `.hx/.gitignore` it added to hide it.
|
|
|
598
600
|
"tui": {
|
|
599
601
|
"enterWhileBusy": "queue", // queue | steer - what enter does mid-turn
|
|
600
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
|
|
601
607
|
}
|
|
602
608
|
}
|
|
603
609
|
```
|
|
604
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
|
+
|
|
605
615
|
Environment overrides: `HX_MODEL`, `HX_SUBAGENT_MODEL`, `HX_MAX_TOKENS`,
|
|
606
616
|
`HX_PERMISSION_MODE`, `HX_SANDBOX`, `HX_COMPACT_AT`, `HX_GIT_NOTICES`,
|
|
607
617
|
`HX_THEME`, `HX_QUIET_STARTUP`. Also `HX_HOME` to relocate user state, `HX_TAVILY_API_KEY`
|
|
@@ -762,6 +772,54 @@ rest to the session directory, and hands the model that path to grep.
|
|
|
762
772
|
|
|
763
773
|
---
|
|
764
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
|
+
|
|
765
823
|
## Extending it
|
|
766
824
|
|
|
767
825
|
**Skills** are directories containing `SKILL.md` with YAML frontmatter:
|
|
@@ -818,6 +876,68 @@ Or `hx mcp add local python server.py`. Tools arrive namespaced
|
|
|
818
876
|
with a per-server timeout; one that is broken or slow logs a warning and is
|
|
819
877
|
dropped rather than taking the session with it.
|
|
820
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
|
+
|
|
821
941
|
---
|
|
822
942
|
|
|
823
943
|
## Development
|
|
@@ -868,9 +988,10 @@ src/hx/
|
|
|
868
988
|
core/ loop, context assembly, compaction, late injection, sessions, usage
|
|
869
989
|
auth/ credential store, OAuth flows, per-route resolution
|
|
870
990
|
providers/ OpenRouter, Codex, Devin, the model catalogue, a scripted provider for tests
|
|
871
|
-
tools/ Bash, Read, Write, Edit, Glob, Grep, TodoWrite, Task,
|
|
872
|
-
WebFetch, output capping
|
|
991
|
+
tools/ Bash, Read, Write, Edit, Glob, Grep, Symbols, TodoWrite, Task,
|
|
992
|
+
WebSearch, WebFetch, line anchors, output capping
|
|
873
993
|
permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
|
|
994
|
+
hooks/ the four events, the trust boundary, subprocess execution
|
|
874
995
|
skills/ agents/ mcp/
|
|
875
996
|
keys.py keybinding registry: ids, defaults, descriptions, user overrides
|
|
876
997
|
term/ the renderer: raw mode, input decoding, the differ, the screen
|
|
@@ -933,12 +1054,13 @@ streaming with prefix caching and accurate cost accounting, session persistence
|
|
|
933
1054
|
and resume, the tool suite, the permission engine and OS sandbox, late
|
|
934
1055
|
injection, compaction, output capping, skills, subagents, MCP, and the TUI.
|
|
935
1056
|
|
|
936
|
-
|
|
937
|
-
subscription over OAuth. The model id
|
|
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.
|
|
938
1060
|
|
|
939
1061
|
Published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/), released
|
|
940
1062
|
from CI on a tag.
|
|
941
1063
|
|
|
942
|
-
The one thing still unproven in CI is a live call on
|
|
943
|
-
tests exist and cover
|
|
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
|
|
944
1066
|
genuine cache hit, but they need a credential and are deselected by default.
|
|
@@ -403,6 +403,7 @@ than being silently resolved.
|
|
|
403
403
|
| `/skills` | installed skills |
|
|
404
404
|
| `/agents` | subagent types |
|
|
405
405
|
| `/mcp` | server status |
|
|
406
|
+
| `/hooks` | configured hooks, and any that were refused |
|
|
406
407
|
| `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
|
|
407
408
|
| `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
|
|
408
409
|
| `/copy` | copy the last reply to the clipboard |
|
|
@@ -520,7 +521,6 @@ along with the `.hx/.gitignore` it added to hide it.
|
|
|
520
521
|
{
|
|
521
522
|
"theme": "dark", // dark | light | ansi, or a file in ~/.hx/themes
|
|
522
523
|
"quiet_startup": false, // skip the startup header
|
|
523
|
-
"telemetry": false,
|
|
524
524
|
|
|
525
525
|
"models": {
|
|
526
526
|
"model": "anthropic/claude-sonnet-4.5",
|
|
@@ -563,10 +563,18 @@ along with the `.hx/.gitignore` it added to hide it.
|
|
|
563
563
|
"tui": {
|
|
564
564
|
"enterWhileBusy": "queue", // queue | steer - what enter does mid-turn
|
|
565
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
|
|
566
570
|
}
|
|
567
571
|
}
|
|
568
572
|
```
|
|
569
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
|
+
|
|
570
578
|
Environment overrides: `HX_MODEL`, `HX_SUBAGENT_MODEL`, `HX_MAX_TOKENS`,
|
|
571
579
|
`HX_PERMISSION_MODE`, `HX_SANDBOX`, `HX_COMPACT_AT`, `HX_GIT_NOTICES`,
|
|
572
580
|
`HX_THEME`, `HX_QUIET_STARTUP`. Also `HX_HOME` to relocate user state, `HX_TAVILY_API_KEY`
|
|
@@ -727,6 +735,54 @@ rest to the session directory, and hands the model that path to grep.
|
|
|
727
735
|
|
|
728
736
|
---
|
|
729
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
|
+
|
|
730
786
|
## Extending it
|
|
731
787
|
|
|
732
788
|
**Skills** are directories containing `SKILL.md` with YAML frontmatter:
|
|
@@ -783,6 +839,68 @@ Or `hx mcp add local python server.py`. Tools arrive namespaced
|
|
|
783
839
|
with a per-server timeout; one that is broken or slow logs a warning and is
|
|
784
840
|
dropped rather than taking the session with it.
|
|
785
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
|
+
|
|
786
904
|
---
|
|
787
905
|
|
|
788
906
|
## Development
|
|
@@ -833,9 +951,10 @@ src/hx/
|
|
|
833
951
|
core/ loop, context assembly, compaction, late injection, sessions, usage
|
|
834
952
|
auth/ credential store, OAuth flows, per-route resolution
|
|
835
953
|
providers/ OpenRouter, Codex, Devin, the model catalogue, a scripted provider for tests
|
|
836
|
-
tools/ Bash, Read, Write, Edit, Glob, Grep, TodoWrite, Task,
|
|
837
|
-
WebFetch, output capping
|
|
954
|
+
tools/ Bash, Read, Write, Edit, Glob, Grep, Symbols, TodoWrite, Task,
|
|
955
|
+
WebSearch, WebFetch, line anchors, output capping
|
|
838
956
|
permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
|
|
957
|
+
hooks/ the four events, the trust boundary, subprocess execution
|
|
839
958
|
skills/ agents/ mcp/
|
|
840
959
|
keys.py keybinding registry: ids, defaults, descriptions, user overrides
|
|
841
960
|
term/ the renderer: raw mode, input decoding, the differ, the screen
|
|
@@ -898,12 +1017,13 @@ streaming with prefix caching and accurate cost accounting, session persistence
|
|
|
898
1017
|
and resume, the tool suite, the permission engine and OS sandbox, late
|
|
899
1018
|
injection, compaction, output capping, skills, subagents, MCP, and the TUI.
|
|
900
1019
|
|
|
901
|
-
|
|
902
|
-
subscription over OAuth. The model id
|
|
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.
|
|
903
1023
|
|
|
904
1024
|
Published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/), released
|
|
905
1025
|
from CI on a tag.
|
|
906
1026
|
|
|
907
|
-
The one thing still unproven in CI is a live call on
|
|
908
|
-
tests exist and cover
|
|
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
|
|
909
1029
|
genuine cache hit, but they need a credential and are deselected by default.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# PostToolUse hook: check the file the model just edited, and hand whatever the
|
|
4
|
+
# checker says straight back to it.
|
|
5
|
+
#
|
|
6
|
+
# Without this, HX has no idea whether an edit compiles. The model writes a
|
|
7
|
+
# file, sees "Applied 1 edit(s)" and a diff, and finds out something broke two
|
|
8
|
+
# turns later when it runs the tests - or never. This closes that loop using
|
|
9
|
+
# the checkers the project already has, with no language server to supervise.
|
|
10
|
+
#
|
|
11
|
+
# Wire it up in ~/.hx/settings.json (hooks are loaded from your own settings
|
|
12
|
+
# only - see "Extending it" in the README):
|
|
13
|
+
#
|
|
14
|
+
# {
|
|
15
|
+
# "hooks": {
|
|
16
|
+
# "PostToolUse": [
|
|
17
|
+
# {
|
|
18
|
+
# "matcher": "Edit|Write",
|
|
19
|
+
# "hooks": [{"type": "command", "command": "~/.hx/check.sh", "timeout": 30}]
|
|
20
|
+
# }
|
|
21
|
+
# ]
|
|
22
|
+
# }
|
|
23
|
+
# }
|
|
24
|
+
#
|
|
25
|
+
# Exits 0 whether or not it finds anything. By PostToolUse the write has
|
|
26
|
+
# already landed, so exit 2 would refuse an edit that is on disk regardless -
|
|
27
|
+
# a confusing answer to something that already happened. Errors here are
|
|
28
|
+
# context for the next turn, not a veto.
|
|
29
|
+
|
|
30
|
+
set -uo pipefail
|
|
31
|
+
|
|
32
|
+
# Resolve a checker the way the project would run it. A bare `command -v ruff`
|
|
33
|
+
# finds nothing on the common setup where the tool lives in ./.venv and never
|
|
34
|
+
# on $PATH, and the hook then does nothing on every edit without saying why.
|
|
35
|
+
# Order: the project's venv, then $PATH, then `uv run` if this is a uv project.
|
|
36
|
+
find_tool() {
|
|
37
|
+
local name=$1
|
|
38
|
+
if [ -x "./.venv/bin/$name" ]; then
|
|
39
|
+
printf './.venv/bin/%s' "$name"
|
|
40
|
+
return 0
|
|
41
|
+
fi
|
|
42
|
+
if command -v "$name" >/dev/null 2>&1; then
|
|
43
|
+
printf '%s' "$name"
|
|
44
|
+
return 0
|
|
45
|
+
fi
|
|
46
|
+
if [ -f ./pyproject.toml ] && command -v uv >/dev/null 2>&1; then
|
|
47
|
+
printf 'uv run --quiet %s' "$name"
|
|
48
|
+
return 0
|
|
49
|
+
fi
|
|
50
|
+
return 1
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
event=$(cat)
|
|
54
|
+
|
|
55
|
+
path=$(
|
|
56
|
+
printf '%s' "$event" |
|
|
57
|
+
python3 -c 'import json,sys; print(json.load(sys.stdin).get("tool_input", {}).get("file_path", ""))' \
|
|
58
|
+
2>/dev/null
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# No path, or a file that no longer exists: nothing to check, and a hook that
|
|
62
|
+
# complains about its own inability to run is noise on every single edit.
|
|
63
|
+
[ -n "$path" ] && [ -f "$path" ] || exit 0
|
|
64
|
+
|
|
65
|
+
# One file, not the project. `ruff check .` on every edit is slow and buries
|
|
66
|
+
# the error the model just introduced under whatever was already failing.
|
|
67
|
+
case "$path" in
|
|
68
|
+
*.py)
|
|
69
|
+
ruff=$(find_tool ruff) || exit 0
|
|
70
|
+
# shellcheck disable=SC2086 # find_tool may return "uv run --quiet ruff"
|
|
71
|
+
output=$($ruff check --quiet "$path" 2>&1)
|
|
72
|
+
;;
|
|
73
|
+
*.ts | *.tsx)
|
|
74
|
+
command -v npx >/dev/null || exit 0
|
|
75
|
+
# Single-file tsc ignores the project graph, so imports it cannot resolve
|
|
76
|
+
# are reported as errors. Real ones still surface; treat it as a smoke
|
|
77
|
+
# test rather than a substitute for `tsc -p .`.
|
|
78
|
+
output=$(npx --no-install tsc --noEmit "$path" 2>&1)
|
|
79
|
+
;;
|
|
80
|
+
*.go)
|
|
81
|
+
command -v go >/dev/null || exit 0
|
|
82
|
+
output=$(go vet "$(dirname "$path")" 2>&1)
|
|
83
|
+
;;
|
|
84
|
+
*.rs)
|
|
85
|
+
command -v cargo >/dev/null || exit 0
|
|
86
|
+
output=$(cargo check --quiet --message-format short 2>&1)
|
|
87
|
+
;;
|
|
88
|
+
*)
|
|
89
|
+
# An extension with no checker is silence, not a complaint. A hook that
|
|
90
|
+
# says something on every Markdown edit gets switched off within a day.
|
|
91
|
+
exit 0
|
|
92
|
+
;;
|
|
93
|
+
esac
|
|
94
|
+
|
|
95
|
+
[ -z "$output" ] && exit 0
|
|
96
|
+
|
|
97
|
+
printf '%s' "$output" |
|
|
98
|
+
python3 -c '
|
|
99
|
+
import json, sys
|
|
100
|
+
print(json.dumps({
|
|
101
|
+
"additionalContext": "Checker output for the file you just edited:\n" + sys.stdin.read().strip()
|
|
102
|
+
}))
|
|
103
|
+
'
|
|
@@ -23,7 +23,6 @@ dependencies = [
|
|
|
23
23
|
"httpx>=0.27",
|
|
24
24
|
"pydantic>=2.7",
|
|
25
25
|
"pyyaml>=6.0",
|
|
26
|
-
"click>=8.1",
|
|
27
26
|
# System trust store, so a corporate proxy CA the machine already trusts is
|
|
28
27
|
# enough - certifi alone is why `hx` fails on networks where browsers work.
|
|
29
28
|
"truststore>=0.10",
|
|
@@ -37,6 +36,13 @@ dependencies = [
|
|
|
37
36
|
]
|
|
38
37
|
|
|
39
38
|
[project.optional-dependencies]
|
|
39
|
+
# Tree-sitter grammars for the Symbols tool. Optional because the wheels are
|
|
40
|
+
# large and the rest of HX works without them - the tool is simply not
|
|
41
|
+
# registered when the import fails.
|
|
42
|
+
symbols = [
|
|
43
|
+
"tree-sitter>=0.23",
|
|
44
|
+
"tree-sitter-language-pack>=0.7",
|
|
45
|
+
]
|
|
40
46
|
dev = [
|
|
41
47
|
"pytest>=8.2",
|
|
42
48
|
"pytest-asyncio>=0.23",
|
|
@@ -99,6 +105,13 @@ plugins = []
|
|
|
99
105
|
module = ["pyte.*", "wcwidth.*", "markdown_it.*"]
|
|
100
106
|
ignore_missing_imports = true
|
|
101
107
|
|
|
108
|
+
# The Symbols grammars are an optional extra, so the lint job does not install
|
|
109
|
+
# them. Type-checking must pass in the configuration users actually get - the
|
|
110
|
+
# one where the import fails and the tool is never registered.
|
|
111
|
+
[[tool.mypy.overrides]]
|
|
112
|
+
module = ["tree_sitter.*", "tree_sitter_language_pack.*"]
|
|
113
|
+
ignore_missing_imports = true
|
|
114
|
+
|
|
102
115
|
[tool.pytest.ini_options]
|
|
103
116
|
testpaths = ["tests"]
|
|
104
117
|
asyncio_mode = "auto"
|
|
@@ -21,6 +21,7 @@ from hx.core.events import SubagentFinished, SubagentStarted
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
22
22
|
from hx.config import Settings
|
|
23
23
|
from hx.core.events import EventBus
|
|
24
|
+
from hx.hooks.engine import HookEngine
|
|
24
25
|
from hx.permissions.engine import PermissionEngine
|
|
25
26
|
from hx.providers.base import Provider
|
|
26
27
|
from hx.providers.models import ModelRegistry
|
|
@@ -57,6 +58,7 @@ class SubagentRunner:
|
|
|
57
58
|
models: ModelRegistry | None = None,
|
|
58
59
|
parent_session_id: str | None = None,
|
|
59
60
|
parent_usage: object | None = None,
|
|
61
|
+
hooks: HookEngine | None = None,
|
|
60
62
|
) -> None:
|
|
61
63
|
self.definitions = definitions
|
|
62
64
|
self.provider = provider
|
|
@@ -67,6 +69,9 @@ class SubagentRunner:
|
|
|
67
69
|
self.models = models
|
|
68
70
|
self.parent_session_id = parent_session_id
|
|
69
71
|
self.parent_usage = parent_usage
|
|
72
|
+
self.hooks = hooks
|
|
73
|
+
"""Shared with the parent: a hook that guards Bash must guard a subagent's
|
|
74
|
+
Bash too, or the guard is one delegation away from being bypassed."""
|
|
70
75
|
self._active: dict[str, str] = {}
|
|
71
76
|
|
|
72
77
|
async def run(self, agent_type: str, prompt: str, description: str) -> SubagentResult:
|
|
@@ -155,6 +160,7 @@ class SubagentRunner:
|
|
|
155
160
|
bus=self.bus,
|
|
156
161
|
settings=self.settings,
|
|
157
162
|
model_info=self.models.get_or_default(model) if self.models else None,
|
|
163
|
+
hooks=self.hooks,
|
|
158
164
|
)
|
|
159
165
|
loop.origin = f"{definition.name} subagent"
|
|
160
166
|
return loop
|