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.
- hx_cli-0.2.4/.gitignore +19 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/CHANGELOG.md +128 -1
- hx_cli-0.2.2/README.md → hx_cli-0.2.4/PKG-INFO +244 -7
- hx_cli-0.2.2/PKG-INFO → hx_cli-0.2.4/README.md +205 -42
- hx_cli-0.2.4/examples/hooks/check.sh +103 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/pyproject.toml +17 -2
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/__init__.py +1 -1
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/agents/subagent.py +6 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/oauth/callback.py +13 -1
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/store.py +16 -2
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/cli.py +95 -1
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/config.py +14 -2
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/loop.py +171 -22
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/session.py +69 -5
- hx_cli-0.2.4/src/hx/hooks/__init__.py +6 -0
- hx_cli-0.2.4/src/hx/hooks/engine.py +227 -0
- hx_cli-0.2.4/src/hx/hooks/spec.py +80 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/mcp/client.py +28 -1
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/permissions/engine.py +130 -13
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/permissions/sandbox.py +46 -6
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/openrouter.py +16 -2
- hx_cli-0.2.4/src/hx/tools/anchors.py +125 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/base.py +12 -8
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/bash.py +54 -17
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/edit.py +124 -8
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/read.py +38 -5
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/registry.py +5 -0
- hx_cli-0.2.4/src/hx/tools/symbols.py +391 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/websearch.py +8 -0
- hx_cli-0.2.4/src/hx/trace.py +887 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/commands.py +135 -1
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/renderers.py +17 -1
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/runtime.py +1 -1
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/agents/test_subagent.py +4 -3
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/auth/test_codex_oauth.py +21 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/auth/test_store.py +58 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_loop.py +334 -1
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_session.py +70 -0
- hx_cli-0.2.4/tests/hooks/test_engine.py +205 -0
- hx_cli-0.2.4/tests/hooks/test_example_check.py +98 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/mcp/fixtures/echo_server.py +22 -1
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/mcp/test_manager.py +34 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/permissions/test_engine.py +111 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/permissions/test_sandbox.py +63 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_openrouter.py +65 -0
- hx_cli-0.2.4/tests/test_trace.py +515 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tools/test_background.py +64 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tools/test_bash.py +48 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tools/test_files.py +19 -3
- hx_cli-0.2.4/tests/tools/test_hashline.py +197 -0
- hx_cli-0.2.4/tests/tools/test_symbols.py +127 -0
- hx_cli-0.2.4/tests/tui/test_trace_command.py +257 -0
- hx_cli-0.2.4/tests/tui/views/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/uv.lock +64 -12
- hx_cli-0.2.2/.gitignore +0 -16
- {hx_cli-0.2.2 → hx_cli-0.2.4}/.github/workflows/ci.yml +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/install.sh +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/agents/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/agents/definitions.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/oauth/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/oauth/browser.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/oauth/codex.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/oauth/devin.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/oauth/pkce.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/auth/resolve.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/checkpoints.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/compaction.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/context.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/events.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/lateinject.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/messages.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/title.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/core/usage.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/docs.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/frontmatter.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/git.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/keys.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/mcp/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/mcp/manager.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/net.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/paths.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/permissions/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/permissions/parser.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/base.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/codex.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/codex_catalogue.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/devin.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/devin_catalogue.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/devin_wire.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/fake.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/models.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/protowire.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/registry.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/providers/responses_codec.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/skills/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/skills/loader.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/skills/runtime.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/ansi.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/buffer.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/component.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/demo.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/editor.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/keydecode.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/loop.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/markdown.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/primitives.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/sanitize.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/screen.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/syntax.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/terminal.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/undo.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/width.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/term/word_nav.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/glob.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/grep.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/output.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/task.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/todo.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tools/write.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/app.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/clipboard.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/format.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/fuzzy.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/glyphs.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/killring.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/limits.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/paint.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/roles.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/theme.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/theme_json.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/themes/ansi.json +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/themes/dark.json +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/themes/light.json +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/blocks.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/dialog.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/login.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/permission.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/pickers.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/prompt.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/status.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/src/hx/tui/views/transcript.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/agents/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/auth/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/auth/test_devin_oauth.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/auth/test_pkce.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/auth/test_resolve.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/conftest.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_checkpoints.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_compaction.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_context.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_lateinject.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_rewind.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_settings_layers.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_settings_migration.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_steering.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_title.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/core/test_usage.py +0 -0
- {hx_cli-0.2.2/tests/mcp → hx_cli-0.2.4/tests/hooks}/__init__.py +0 -0
- {hx_cli-0.2.2/tests/permissions → hx_cli-0.2.4/tests/mcp}/__init__.py +0 -0
- {hx_cli-0.2.2/tests/providers → hx_cli-0.2.4/tests/permissions}/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/permissions/test_parser.py +0 -0
- {hx_cli-0.2.2/tests/skills → hx_cli-0.2.4/tests/providers}/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_catalogue.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_codex.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_codex_catalogue.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_devin.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_devin_catalogue.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_protowire.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_registry.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/providers/test_responses_codec.py +0 -0
- {hx_cli-0.2.2/tests/term → hx_cli-0.2.4/tests/skills}/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/skills/test_loader.py +0 -0
- {hx_cli-0.2.2/tests/tools → hx_cli-0.2.4/tests/term}/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/conftest.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_ansi.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_keydecode.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_loop.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_markdown.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_primitives.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_sanitize.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_screen.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_terminal.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/term/test_width.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_cli.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_docs.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_git.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_install.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_integration.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_live.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_live_codex.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_live_devin.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_live_tavily.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_net.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_paths.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/test_readme.py +0 -0
- {hx_cli-0.2.2/tests/tui → hx_cli-0.2.4/tests/tools}/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tools/test_output.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tools/test_todo.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tools/test_websearch.py +0 -0
- {hx_cli-0.2.2/tests/tui/views → hx_cli-0.2.4/tests/tui}/__init__.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/support.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/test_autocomplete.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/test_clipboard.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/test_effort.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/test_keys.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/test_login.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/test_renderers.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/test_session.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/test_theme_json.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/views/test_blocks.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/views/test_permission.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/views/test_pickers.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/views/test_prompt.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/views/test_runtime.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/views/test_status.py +0 -0
- {hx_cli-0.2.2 → hx_cli-0.2.4}/tests/tui/views/test_transcript.py +0 -0
hx_cli-0.2.4/.gitignore
ADDED
|
@@ -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.
|
|
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,
|
|
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
|
-
|
|
902
|
-
subscription over OAuth. The model id
|
|
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
|
|
908
|
-
tests exist and cover
|
|
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.
|