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