hx-cli 0.2.3__tar.gz → 0.2.5__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.5/.gitignore +19 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/CHANGELOG.md +114 -1
- hx_cli-0.2.3/README.md → hx_cli-0.2.5/PKG-INFO +162 -0
- hx_cli-0.2.3/PKG-INFO → hx_cli-0.2.5/README.md +122 -37
- {hx_cli-0.2.3 → hx_cli-0.2.5}/pyproject.toml +4 -1
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/__init__.py +1 -1
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/oauth/callback.py +13 -1
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/store.py +16 -2
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/cli.py +120 -3
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/compaction.py +17 -2
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/context.py +25 -4
- hx_cli-0.2.5/src/hx/core/images.py +394 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/loop.py +98 -26
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/messages.py +80 -4
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/session.py +69 -5
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/keys.py +3 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/mcp/client.py +56 -9
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/mcp/manager.py +31 -1
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/permissions/engine.py +128 -11
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/permissions/sandbox.py +46 -6
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/codex_catalogue.py +12 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/devin.py +14 -1
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/devin_catalogue.py +4 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/devin_wire.py +21 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/models.py +36 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/openrouter.py +51 -3
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/responses_codec.py +29 -6
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/base.py +18 -9
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/bash.py +68 -19
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/read.py +36 -1
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/websearch.py +8 -0
- hx_cli-0.2.5/src/hx/trace.py +919 -0
- hx_cli-0.2.5/src/hx/tui/clipboard.py +308 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/commands.py +104 -1
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/glyphs.py +3 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/runtime.py +98 -20
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/blocks.py +27 -9
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/pickers.py +5 -2
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/prompt.py +108 -8
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/auth/test_codex_oauth.py +21 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/auth/test_store.py +58 -0
- hx_cli-0.2.5/tests/core/test_images.py +281 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_loop.py +206 -1
- hx_cli-0.2.5/tests/core/test_loop_images.py +181 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_session.py +70 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_steering.py +3 -3
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/mcp/fixtures/echo_server.py +47 -1
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/mcp/test_manager.py +54 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/permissions/test_engine.py +111 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/permissions/test_sandbox.py +63 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_catalogue.py +55 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_codex_catalogue.py +7 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_devin.py +36 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_devin_catalogue.py +23 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_openrouter.py +135 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_responses_codec.py +34 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_cli.py +49 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_readme.py +1 -0
- hx_cli-0.2.5/tests/test_trace.py +538 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/test_background.py +78 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/test_bash.py +48 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/test_files.py +33 -0
- hx_cli-0.2.5/tests/tui/test_images.py +319 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/test_session.py +22 -11
- hx_cli-0.2.5/tests/tui/test_trace_command.py +257 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/views/test_prompt.py +9 -9
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/views/test_runtime.py +1 -1
- {hx_cli-0.2.3 → hx_cli-0.2.5}/uv.lock +87 -0
- hx_cli-0.2.3/.gitignore +0 -16
- hx_cli-0.2.3/src/hx/tui/clipboard.py +0 -125
- {hx_cli-0.2.3 → hx_cli-0.2.5}/.github/workflows/ci.yml +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/examples/hooks/check.sh +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/install.sh +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/agents/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/agents/definitions.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/agents/subagent.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/oauth/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/oauth/browser.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/oauth/codex.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/oauth/devin.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/oauth/pkce.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/auth/resolve.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/config.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/checkpoints.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/events.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/lateinject.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/title.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/core/usage.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/docs.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/frontmatter.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/git.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/hooks/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/hooks/engine.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/hooks/spec.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/mcp/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/net.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/paths.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/permissions/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/permissions/parser.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/base.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/codex.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/fake.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/protowire.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/providers/registry.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/skills/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/skills/loader.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/skills/runtime.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/ansi.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/buffer.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/component.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/demo.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/editor.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/keydecode.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/loop.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/markdown.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/primitives.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/sanitize.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/screen.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/syntax.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/terminal.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/undo.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/width.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/term/word_nav.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/anchors.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/edit.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/glob.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/grep.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/output.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/registry.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/symbols.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/task.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/todo.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tools/write.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/app.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/format.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/fuzzy.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/killring.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/limits.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/paint.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/renderers.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/roles.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/theme.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/theme_json.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/themes/ansi.json +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/themes/dark.json +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/themes/light.json +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/dialog.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/login.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/permission.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/status.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/src/hx/tui/views/transcript.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/agents/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/agents/test_subagent.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/auth/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/auth/test_devin_oauth.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/auth/test_pkce.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/auth/test_resolve.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/conftest.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_checkpoints.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_compaction.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_context.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_lateinject.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_rewind.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_settings_layers.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_settings_migration.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_title.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/core/test_usage.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/hooks/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/hooks/test_engine.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/hooks/test_example_check.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/mcp/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/permissions/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/permissions/test_parser.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_codex.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_protowire.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/providers/test_registry.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/skills/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/skills/test_loader.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/conftest.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_ansi.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_keydecode.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_loop.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_markdown.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_primitives.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_sanitize.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_screen.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_terminal.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/term/test_width.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_docs.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_git.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_install.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_integration.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_live.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_live_codex.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_live_devin.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_live_tavily.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_net.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/test_paths.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/test_hashline.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/test_output.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/test_symbols.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/test_todo.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tools/test_websearch.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/support.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/test_autocomplete.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/test_clipboard.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/test_effort.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/test_keys.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/test_login.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/test_renderers.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/test_theme_json.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/views/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/views/test_blocks.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/views/test_permission.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/views/test_pickers.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/views/test_status.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.5}/tests/tui/views/test_transcript.py +0 -0
hx_cli-0.2.5/.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,117 @@ the project follows [semantic versioning](https://semver.org/).
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
+
## [0.2.5] - 2026-09-23
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- Images as input. `ctrl+v` attaches the image on the clipboard, dragging an
|
|
36
|
+
image file into the terminal attaches it, and `hx -p "..." --image PATH`
|
|
37
|
+
attaches one to a headless prompt. Each shows in the prompt as
|
|
38
|
+
`[Image #N]`.
|
|
39
|
+
- `Read` returns image files as images, so the model can look at a screenshot
|
|
40
|
+
or mockup it is pointed at with `@path`.
|
|
41
|
+
- Images returned by MCP tools reach the model instead of being dropped.
|
|
42
|
+
- Every route carries images: OpenRouter, the Codex subscription and the Devin
|
|
43
|
+
subscription. Large images are scaled and re-encoded to fit each route's
|
|
44
|
+
limits.
|
|
45
|
+
- A model that does not accept images is warned about when one is attached,
|
|
46
|
+
and is told the image was left out rather than sent a request that fails.
|
|
47
|
+
The `/model` picker marks the models that can see images.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- Background jobs are waited for, not only signalled, when a session closes.
|
|
52
|
+
An unwaited subprocess transport was collected after the event loop had
|
|
53
|
+
gone, which raised `Event loop is closed` as an unraisable warning on CI
|
|
54
|
+
runs that otherwise passed.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## [0.2.4] - 2026-09-19
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- `/trace [path]`: write the whole session to one self-contained HTML page and
|
|
63
|
+
open it. The system prompt, the project context, the skills index, every tool
|
|
64
|
+
schema, every prompt and reply, every thinking block, every tool call beside
|
|
65
|
+
the result it produced, the per-call token ledger and the files HX changed.
|
|
66
|
+
Late-injected reminders and compacted turns are included behind a toggle.
|
|
67
|
+
A session still running traces as it stands, including the turn that just
|
|
68
|
+
landed. The page follows your system's dark or light setting and a button
|
|
69
|
+
overrides it; nothing is fetched and nothing is uploaded.
|
|
70
|
+
- `hx trace [SESSION_ID] [PATH]`: the same page for any transcript on the
|
|
71
|
+
machine, without starting a session. Defaults to the last session in this
|
|
72
|
+
directory and prints only the path, so `open "$(hx trace)"` works. Either
|
|
73
|
+
argument may be given alone - a lone one that looks like a path is one.
|
|
74
|
+
- The system prompt and the tool schemas are now written to the transcript on a
|
|
75
|
+
session's first provider call, so a trace of a finished session can say what
|
|
76
|
+
the model was told and not only what it said.
|
|
77
|
+
|
|
78
|
+
### Security
|
|
79
|
+
|
|
80
|
+
- Permission rules are matched against the path a specifier resolves to, not
|
|
81
|
+
the text it was written as. `deny: Read(~/.ssh/**)` was got past by asking
|
|
82
|
+
for `../../.ssh/id_rsa`, or through a symlink planted in the project. The
|
|
83
|
+
same resolution stops an allow rule leaking: `allow: Edit(src/**)` used to
|
|
84
|
+
cover `src/../../etc/passwd`.
|
|
85
|
+
- `WebFetch` is matched against every URL it was given rather than the first,
|
|
86
|
+
so `deny: WebFetch(https://host/**)` is no longer a question of list order.
|
|
87
|
+
The approval prompt spells the URLs out too; it said `urls=[2 items]`, which
|
|
88
|
+
answered the only question that mattered with a number.
|
|
89
|
+
- The generated Seatbelt profile no longer grants write access to all of
|
|
90
|
+
`/private/var/folders` - every user's and every process's temp and cache.
|
|
91
|
+
`$TMPDIR` was resolved precisely to avoid that, and the profile handed it
|
|
92
|
+
back on the next line. This user's own container stays writable.
|
|
93
|
+
- `~/.hx/auth.json` is created with mode 0600 rather than narrowed to it after
|
|
94
|
+
the tokens are written, closing a window at a predictable path where the
|
|
95
|
+
credential was readable by any local account.
|
|
96
|
+
- The OAuth callback page escapes what the provider sent. An `error` parameter
|
|
97
|
+
carrying markup ran as HTML on a loopback origin that is otherwise the
|
|
98
|
+
user's own.
|
|
99
|
+
|
|
100
|
+
### Fixed
|
|
101
|
+
|
|
102
|
+
- Slash commands read the session that is in force rather than the one HX
|
|
103
|
+
opened with. After `/resume`, `/trace` wrote a page of zeros into the empty
|
|
104
|
+
startup session's directory and `/cost` reported nobody's usage: the command
|
|
105
|
+
context had captured the session object at startup, and `/resume` rebinds it.
|
|
106
|
+
- A session whose title was the trace template's own data placeholder had the
|
|
107
|
+
whole payload spliced into the page's `<title>`.
|
|
108
|
+
- A trace labelled an encrypted reasoning block "thinking (signed)", which read
|
|
109
|
+
as though the page had dropped the rest of it. It now says the text is the
|
|
110
|
+
provider's summary and the reasoning came back encrypted, and a block that
|
|
111
|
+
arrived with no summary at all says that rather than rendering an empty box.
|
|
112
|
+
- MCP servers survive a large response. One JSON-RPC message is one line and
|
|
113
|
+
asyncio's stream limit is 64 KiB, so a tool returning a file did not truncate
|
|
114
|
+
- it raised out of the read loop and killed the connection for the rest of
|
|
115
|
+
the session. The limit is now 16 MiB, and overshooting it says so.
|
|
116
|
+
- A tool call is no longer dropped when a stream ends without a `finish_reason`.
|
|
117
|
+
The arguments were buffered until that field arrived, so a complete call was
|
|
118
|
+
discarded and the turn reported `end_turn`: the model appeared to stop for no
|
|
119
|
+
reason, and the tokens were already paid for.
|
|
120
|
+
- The Linux sandbox starts again for anyone with credential *files*. Denied
|
|
121
|
+
paths were all shadowed with `--tmpfs`, which mounts a directory and aborts
|
|
122
|
+
bwrap when aimed at a file - and six of them are files, `~/.hx/auth.json`
|
|
123
|
+
among them. Files are shadowed with `/dev/null` instead. bwrap also starts in
|
|
124
|
+
the policy's directory rather than whatever `$PWD` happened to hold.
|
|
125
|
+
- `PersistentShell.cwd` follows the shell. It claimed to track it and was in
|
|
126
|
+
fact the launch directory forever, which meant a background job always
|
|
127
|
+
started where the session opened rather than where `cd` had left it.
|
|
128
|
+
- A background job no longer leaks its log file descriptor for the life of the
|
|
129
|
+
session.
|
|
130
|
+
- A record appended while the transcript was being written is no longer
|
|
131
|
+
dropped. The queue is taken in one step, and a failed write puts its records
|
|
132
|
+
back rather than losing them.
|
|
133
|
+
- An automatic compaction with nothing to summarise is no longer announced and
|
|
134
|
+
rerun every turn for the rest of the session. `/compact` still answers either
|
|
135
|
+
way.
|
|
136
|
+
- `/trace` takes `--no-open`, and never launches a *terminal* browser: with no
|
|
137
|
+
display, `webbrowser` falls through to lynx or w3m, which opens in the
|
|
138
|
+
terminal HX is drawing on and takes the session display with it.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
31
142
|
## [0.2.3] - 2026-09-18
|
|
32
143
|
|
|
33
144
|
### Added
|
|
@@ -481,7 +592,9 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
|
|
|
481
592
|
- `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
|
|
482
593
|
self-update, and `install.sh` bootstrapping uv with a pinned Python.
|
|
483
594
|
|
|
484
|
-
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.
|
|
595
|
+
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.5...HEAD
|
|
596
|
+
[0.2.5]: https://github.com/aletisunil/hx/compare/v0.2.4...v0.2.5
|
|
597
|
+
[0.2.4]: https://github.com/aletisunil/hx/compare/v0.2.3...v0.2.4
|
|
485
598
|
[0.2.3]: https://github.com/aletisunil/hx/compare/v0.2.2...v0.2.3
|
|
486
599
|
[0.2.2]: https://github.com/aletisunil/hx/compare/v0.2.1...v0.2.2
|
|
487
600
|
[0.2.1]: https://github.com/aletisunil/hx/compare/v0.2.0...v0.2.1
|
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hx-cli
|
|
3
|
+
Version: 0.2.5
|
|
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: pillow>=10.4
|
|
22
|
+
Requires-Dist: pydantic>=2.7
|
|
23
|
+
Requires-Dist: pygments>=2.17
|
|
24
|
+
Requires-Dist: pyyaml>=6.0
|
|
25
|
+
Requires-Dist: truststore>=0.10
|
|
26
|
+
Requires-Dist: wcwidth>=0.2.13
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
29
|
+
Requires-Dist: pyte>=0.8; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
34
|
+
Requires-Dist: types-pygments; extra == 'dev'
|
|
35
|
+
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
36
|
+
Provides-Extra: symbols
|
|
37
|
+
Requires-Dist: tree-sitter-language-pack>=0.7; extra == 'symbols'
|
|
38
|
+
Requires-Dist: tree-sitter>=0.23; extra == 'symbols'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
1
41
|
# HX
|
|
2
42
|
|
|
3
43
|
An agent harness for the terminal. Python core, its own terminal renderer,
|
|
@@ -257,9 +297,13 @@ available through OpenRouter.
|
|
|
257
297
|
```sh
|
|
258
298
|
hx # interactive TUI in the current directory
|
|
259
299
|
hx -p "explain this repo" # headless: streams to stdout, tool activity to stderr
|
|
300
|
+
hx -p "why?" --image s.png # attach an image to a headless prompt; repeatable
|
|
260
301
|
hx resume # resume the last session here
|
|
261
302
|
hx resume <session-id> # resume a specific one
|
|
262
303
|
hx prompt # print the system prompt this directory would use
|
|
304
|
+
hx trace # write the last session here to an HTML page
|
|
305
|
+
hx trace out.html # the same, to a path you choose
|
|
306
|
+
hx trace <id> out.html # a specific session, to a path you choose
|
|
263
307
|
hx --model openai/gpt-5 # override the model for one run
|
|
264
308
|
hx --mode plan # start read-only
|
|
265
309
|
hx --cwd ../other-project # run against a different directory
|
|
@@ -333,6 +377,7 @@ be rebound (see [Keybindings](#keybindings)).
|
|
|
333
377
|
| `pgdn` | scroll the transcript down (fullscreen; otherwise your terminal scrolls) |
|
|
334
378
|
| `ctrl+home` | jump to the start: the viewport in fullscreen, the first message otherwise |
|
|
335
379
|
| `ctrl+end` | jump back to the newest output |
|
|
380
|
+
| `ctrl+v` | attach the image on the clipboard (see [Images](#images)) |
|
|
336
381
|
| `@path` | complete a file path |
|
|
337
382
|
| `!command` | run a shell command directly, no model turn |
|
|
338
383
|
|
|
@@ -364,6 +409,44 @@ text](#selecting-and-copying-text) for the terminal's own selection.
|
|
|
364
409
|
HX does not emit OSC 133 prompt markers yet, so shell-integration features that
|
|
365
410
|
jump between prompts will not see HX's messages.
|
|
366
411
|
|
|
412
|
+
#### Images
|
|
413
|
+
|
|
414
|
+
Give the model a picture three ways, and each lands in the prompt as a token
|
|
415
|
+
such as `[Image #1]`:
|
|
416
|
+
|
|
417
|
+
- **`ctrl+v`** attaches whatever image is on the clipboard: a screenshot copied
|
|
418
|
+
with `cmd+ctrl+shift+4`, an image copied in a browser, or an image file
|
|
419
|
+
copied in Finder or a file manager. Terminals whose own paste sends nothing
|
|
420
|
+
when the clipboard holds a picture are caught too - an empty paste is read as
|
|
421
|
+
a request for the image.
|
|
422
|
+
- **Drag an image file** into the terminal. The terminal pastes its path, and a
|
|
423
|
+
paste that is nothing but paths to image files attaches them instead of
|
|
424
|
+
inserting the text. Quoted, backslash-escaped and `file://` paths all work.
|
|
425
|
+
- **Mention it.** `look at @docs/mockup.png` sends the path as text, and the
|
|
426
|
+
model opens it with `Read`, which returns images as images.
|
|
427
|
+
|
|
428
|
+
The token is the attachment: move it and the text refers to the picture in a
|
|
429
|
+
different place, delete it (one `backspace` takes the whole token) and the
|
|
430
|
+
image is not sent. Sent messages list their images underneath, with size and
|
|
431
|
+
weight. Numbers run for the whole session, so a message recalled from history
|
|
432
|
+
or restored by a rewind still carries its pictures.
|
|
433
|
+
|
|
434
|
+
PNG, JPEG, GIF, WebP, BMP and TIFF are accepted. An image within every route's
|
|
435
|
+
limits goes byte for byte; a larger one is scaled to 2000 px on its long side
|
|
436
|
+
and re-encoded to stay under 5 MB, which is what a retina screenshot usually
|
|
437
|
+
needs. Images an MCP tool returns - a browser screenshot, say - are handled the
|
|
438
|
+
same way rather than dropped.
|
|
439
|
+
|
|
440
|
+
Whether a model can see images comes from the catalogue: OpenRouter's input
|
|
441
|
+
modalities, the Codex catalogue, and Devin's model config. The `/model` picker
|
|
442
|
+
marks the ones that can with `images`. A model that cannot
|
|
443
|
+
is warned about when you attach, and is sent a line saying an image was left
|
|
444
|
+
out, rather than a request that fails. The transcript keeps the image either
|
|
445
|
+
way, so `/model` to one that can see it and ask again.
|
|
446
|
+
|
|
447
|
+
Over SSH the clipboard you copied into is on your own machine, so `ctrl+v`
|
|
448
|
+
says so; copy the file over and drag or paste its path instead.
|
|
449
|
+
|
|
367
450
|
#### Keybindings
|
|
368
451
|
|
|
369
452
|
Any key can be rebound in `~/.hx/keybindings.json`, keyed by the action ids in
|
|
@@ -407,6 +490,7 @@ than being silently resolved.
|
|
|
407
490
|
| `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
|
|
408
491
|
| `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
|
|
409
492
|
| `/copy` | copy the last reply to the clipboard |
|
|
493
|
+
| `/trace [path]` | write the whole session to a self-contained HTML page |
|
|
410
494
|
| `/fullscreen [on\|off]` | take the whole window, with the prompt pinned to the bottom |
|
|
411
495
|
| `/mouse [on\|off]` | mouse reporting, and with it your terminal's own text selection |
|
|
412
496
|
| `/init` | generate an `AGENTS.md` for the project |
|
|
@@ -652,6 +736,7 @@ files load here unchanged. `src/hx/tui/themes/dark.json` is the reference.
|
|
|
652
736
|
| `~/.hx/system-prompt.md` | your system prompt, replacing the built-in one |
|
|
653
737
|
| `~/.hx/system-prompt-append.md` | text appended to whichever prompt is in force |
|
|
654
738
|
| `~/.hx/sessions/` | transcripts, spilled tool output, subagent sessions |
|
|
739
|
+
| `~/.hx/sessions/<id>/trace.html` | where `/trace` writes, unless you name a path |
|
|
655
740
|
| `~/.hx/skills/`, `~/.hx/agents/` | your skills and agents |
|
|
656
741
|
| `~/.hx/projects/<project>/settings.local.json` | this machine's settings for one project - where "always allow" lands |
|
|
657
742
|
| `./.hx/settings.json` | project settings, checked in if you like — read by HX, never written |
|
|
@@ -735,6 +820,79 @@ rest to the session directory, and hands the model that path to grep.
|
|
|
735
820
|
|
|
736
821
|
---
|
|
737
822
|
|
|
823
|
+
## Tracing a session
|
|
824
|
+
|
|
825
|
+
`/trace` writes everything that happened to one self-contained HTML file and
|
|
826
|
+
opens it in a browser:
|
|
827
|
+
|
|
828
|
+
```
|
|
829
|
+
/trace # ~/.hx/sessions/<id>/trace.html
|
|
830
|
+
/trace ~/Desktop/bug.html # somewhere you choose
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
Everything means everything. The system prompt, the project context, the skills
|
|
834
|
+
index, every tool schema as the provider received it, every prompt, every
|
|
835
|
+
reply, every thinking block, every tool call next to the result it produced,
|
|
836
|
+
the per-call token ledger, and the files HX changed. Late-injected reminders
|
|
837
|
+
and compacted turns are in there too, behind a toggle that says how many there
|
|
838
|
+
are — a trace that hides the reminders and the superseded turns is not a trace
|
|
839
|
+
of the session that ran.
|
|
840
|
+
|
|
841
|
+
Nothing is summarised and nothing is prettified. Assistant text is shown as it
|
|
842
|
+
arrived, backticks and all, because a trace exists to answer "what actually
|
|
843
|
+
went through" and a rendered version of that is a different document. Long tool
|
|
844
|
+
results are folded rather than truncated, with the character count on the fold.
|
|
845
|
+
|
|
846
|
+
**Thinking, where the provider allows it.** How much of a model's reasoning HX
|
|
847
|
+
ever sees is the provider's decision, not HX's. Anthropic's models return the
|
|
848
|
+
thinking itself. The Responses API - the OpenAI and Codex routes - returns a
|
|
849
|
+
one-line summary in plaintext and the reasoning encrypted, to be replayed on
|
|
850
|
+
the next call and read by nobody, HX included. A trace labels which of the two
|
|
851
|
+
it is holding, because a one-line thinking block is otherwise indistinguishable
|
|
852
|
+
from a trace that dropped the rest.
|
|
853
|
+
|
|
854
|
+
A running session traces as it stands right now, including the turn that just
|
|
855
|
+
landed. The page says so, because a trace of a live session is a snapshot.
|
|
856
|
+
|
|
857
|
+
**Reading it.** Long tool results are folded and the reference panels - the
|
|
858
|
+
prompt, the schemas, the ledger - start shut, so the transcript is what you
|
|
859
|
+
land on. "Expand all" opens the whole page, panels included, and turns into
|
|
860
|
+
"Collapse all". A session with no turns yet opens its panels on its own, since
|
|
861
|
+
there is nothing else to read.
|
|
862
|
+
|
|
863
|
+
**Dark or light.** The page follows your system by default and a button in the
|
|
864
|
+
corner overrides it, remembered per browser. No stylesheet is fetched to do it.
|
|
865
|
+
|
|
866
|
+
**Nothing is uploaded and nothing is fetched.** The payload, the stylesheet and
|
|
867
|
+
the script all live in the file, so it opens offline, renders the same a year
|
|
868
|
+
later, and a trace of a private repository stays as private as the directory
|
|
869
|
+
you wrote it into. The embedded payload is also the machine-readable trace: it
|
|
870
|
+
is JSON, in a `<script id="trace-data">` element, and `/trace` is the only way
|
|
871
|
+
HX will ever put your transcript anywhere but `~/.hx`.
|
|
872
|
+
|
|
873
|
+
From outside a session, on any transcript on the machine:
|
|
874
|
+
|
|
875
|
+
```sh
|
|
876
|
+
hx trace # the last session in this directory
|
|
877
|
+
hx trace 20260918-214743-b5b8f9 # one by id
|
|
878
|
+
hx trace ~/somewhere.html # the last session, to a path you choose
|
|
879
|
+
hx trace <id> ~/somewhere.html # both, to a path or a directory
|
|
880
|
+
open "$(hx trace)" # the path is all that goes to stdout
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
`hx trace` reads the transcript and runs nothing, so it works on a session that
|
|
884
|
+
ended months ago and on one another terminal has open right now.
|
|
885
|
+
|
|
886
|
+
**What a trace can be missing.** The prompt and the tool schemas are written to
|
|
887
|
+
the transcript on the session's first provider call — the first moment the tool
|
|
888
|
+
registry has settled, because MCP servers connect after the loop is built. A
|
|
889
|
+
session traced before that call, or one recorded by a version of HX older than
|
|
890
|
+
this feature, has the transcript but not the prompt, and the page leaves those
|
|
891
|
+
panels out rather than guessing. `/trace` inside a live session fills them in
|
|
892
|
+
from the loop either way.
|
|
893
|
+
|
|
894
|
+
---
|
|
895
|
+
|
|
738
896
|
## Reading and editing code
|
|
739
897
|
|
|
740
898
|
**Line anchors.** `Read` labels every line with a short content hash:
|
|
@@ -760,6 +918,10 @@ and is unchanged; anchors are a second shape, not a replacement. Turn the whole
|
|
|
760
918
|
thing off with `"tools": {"hashline": false}` and reads go back to plain
|
|
761
919
|
`cat -n`.
|
|
762
920
|
|
|
921
|
+
**Images.** `Read` on a PNG, JPEG, GIF, WebP, BMP or TIFF - or on a file with
|
|
922
|
+
no extension whose bytes are one - returns the image itself for the model to
|
|
923
|
+
look at, normalised the way a pasted one is (see [Images](#images)).
|
|
924
|
+
|
|
763
925
|
**Symbols** answers structural questions with a parse tree rather than a
|
|
764
926
|
regex — three modes, `outline` for one file's shape, `definition` for where a
|
|
765
927
|
name is defined, `references` for where it is used:
|
|
@@ -1,40 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: hx-cli
|
|
3
|
-
Version: 0.2.3
|
|
4
|
-
Summary: HX - an agent harness for the terminal
|
|
5
|
-
Project-URL: Homepage, https://github.com/aletisunil/hx
|
|
6
|
-
Project-URL: Issues, https://github.com/aletisunil/hx/issues
|
|
7
|
-
Author: Sunil Aleti
|
|
8
|
-
License: MIT
|
|
9
|
-
Keywords: agent,cli,coding-agent,llm,openrouter,tui
|
|
10
|
-
Classifier: Development Status :: 3 - Alpha
|
|
11
|
-
Classifier: Environment :: Console
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
-
Requires-Python: >=3.11
|
|
17
|
-
Requires-Dist: httpx>=0.27
|
|
18
|
-
Requires-Dist: markdown-it-py>=3.0
|
|
19
|
-
Requires-Dist: pydantic>=2.7
|
|
20
|
-
Requires-Dist: pygments>=2.17
|
|
21
|
-
Requires-Dist: pyyaml>=6.0
|
|
22
|
-
Requires-Dist: truststore>=0.10
|
|
23
|
-
Requires-Dist: wcwidth>=0.2.13
|
|
24
|
-
Provides-Extra: dev
|
|
25
|
-
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
26
|
-
Requires-Dist: pyte>=0.8; extra == 'dev'
|
|
27
|
-
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
28
|
-
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
29
|
-
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
30
|
-
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
31
|
-
Requires-Dist: types-pygments; extra == 'dev'
|
|
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'
|
|
36
|
-
Description-Content-Type: text/markdown
|
|
37
|
-
|
|
38
1
|
# HX
|
|
39
2
|
|
|
40
3
|
An agent harness for the terminal. Python core, its own terminal renderer,
|
|
@@ -294,9 +257,13 @@ available through OpenRouter.
|
|
|
294
257
|
```sh
|
|
295
258
|
hx # interactive TUI in the current directory
|
|
296
259
|
hx -p "explain this repo" # headless: streams to stdout, tool activity to stderr
|
|
260
|
+
hx -p "why?" --image s.png # attach an image to a headless prompt; repeatable
|
|
297
261
|
hx resume # resume the last session here
|
|
298
262
|
hx resume <session-id> # resume a specific one
|
|
299
263
|
hx prompt # print the system prompt this directory would use
|
|
264
|
+
hx trace # write the last session here to an HTML page
|
|
265
|
+
hx trace out.html # the same, to a path you choose
|
|
266
|
+
hx trace <id> out.html # a specific session, to a path you choose
|
|
300
267
|
hx --model openai/gpt-5 # override the model for one run
|
|
301
268
|
hx --mode plan # start read-only
|
|
302
269
|
hx --cwd ../other-project # run against a different directory
|
|
@@ -370,6 +337,7 @@ be rebound (see [Keybindings](#keybindings)).
|
|
|
370
337
|
| `pgdn` | scroll the transcript down (fullscreen; otherwise your terminal scrolls) |
|
|
371
338
|
| `ctrl+home` | jump to the start: the viewport in fullscreen, the first message otherwise |
|
|
372
339
|
| `ctrl+end` | jump back to the newest output |
|
|
340
|
+
| `ctrl+v` | attach the image on the clipboard (see [Images](#images)) |
|
|
373
341
|
| `@path` | complete a file path |
|
|
374
342
|
| `!command` | run a shell command directly, no model turn |
|
|
375
343
|
|
|
@@ -401,6 +369,44 @@ text](#selecting-and-copying-text) for the terminal's own selection.
|
|
|
401
369
|
HX does not emit OSC 133 prompt markers yet, so shell-integration features that
|
|
402
370
|
jump between prompts will not see HX's messages.
|
|
403
371
|
|
|
372
|
+
#### Images
|
|
373
|
+
|
|
374
|
+
Give the model a picture three ways, and each lands in the prompt as a token
|
|
375
|
+
such as `[Image #1]`:
|
|
376
|
+
|
|
377
|
+
- **`ctrl+v`** attaches whatever image is on the clipboard: a screenshot copied
|
|
378
|
+
with `cmd+ctrl+shift+4`, an image copied in a browser, or an image file
|
|
379
|
+
copied in Finder or a file manager. Terminals whose own paste sends nothing
|
|
380
|
+
when the clipboard holds a picture are caught too - an empty paste is read as
|
|
381
|
+
a request for the image.
|
|
382
|
+
- **Drag an image file** into the terminal. The terminal pastes its path, and a
|
|
383
|
+
paste that is nothing but paths to image files attaches them instead of
|
|
384
|
+
inserting the text. Quoted, backslash-escaped and `file://` paths all work.
|
|
385
|
+
- **Mention it.** `look at @docs/mockup.png` sends the path as text, and the
|
|
386
|
+
model opens it with `Read`, which returns images as images.
|
|
387
|
+
|
|
388
|
+
The token is the attachment: move it and the text refers to the picture in a
|
|
389
|
+
different place, delete it (one `backspace` takes the whole token) and the
|
|
390
|
+
image is not sent. Sent messages list their images underneath, with size and
|
|
391
|
+
weight. Numbers run for the whole session, so a message recalled from history
|
|
392
|
+
or restored by a rewind still carries its pictures.
|
|
393
|
+
|
|
394
|
+
PNG, JPEG, GIF, WebP, BMP and TIFF are accepted. An image within every route's
|
|
395
|
+
limits goes byte for byte; a larger one is scaled to 2000 px on its long side
|
|
396
|
+
and re-encoded to stay under 5 MB, which is what a retina screenshot usually
|
|
397
|
+
needs. Images an MCP tool returns - a browser screenshot, say - are handled the
|
|
398
|
+
same way rather than dropped.
|
|
399
|
+
|
|
400
|
+
Whether a model can see images comes from the catalogue: OpenRouter's input
|
|
401
|
+
modalities, the Codex catalogue, and Devin's model config. The `/model` picker
|
|
402
|
+
marks the ones that can with `images`. A model that cannot
|
|
403
|
+
is warned about when you attach, and is sent a line saying an image was left
|
|
404
|
+
out, rather than a request that fails. The transcript keeps the image either
|
|
405
|
+
way, so `/model` to one that can see it and ask again.
|
|
406
|
+
|
|
407
|
+
Over SSH the clipboard you copied into is on your own machine, so `ctrl+v`
|
|
408
|
+
says so; copy the file over and drag or paste its path instead.
|
|
409
|
+
|
|
404
410
|
#### Keybindings
|
|
405
411
|
|
|
406
412
|
Any key can be rebound in `~/.hx/keybindings.json`, keyed by the action ids in
|
|
@@ -444,6 +450,7 @@ than being silently resolved.
|
|
|
444
450
|
| `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
|
|
445
451
|
| `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
|
|
446
452
|
| `/copy` | copy the last reply to the clipboard |
|
|
453
|
+
| `/trace [path]` | write the whole session to a self-contained HTML page |
|
|
447
454
|
| `/fullscreen [on\|off]` | take the whole window, with the prompt pinned to the bottom |
|
|
448
455
|
| `/mouse [on\|off]` | mouse reporting, and with it your terminal's own text selection |
|
|
449
456
|
| `/init` | generate an `AGENTS.md` for the project |
|
|
@@ -689,6 +696,7 @@ files load here unchanged. `src/hx/tui/themes/dark.json` is the reference.
|
|
|
689
696
|
| `~/.hx/system-prompt.md` | your system prompt, replacing the built-in one |
|
|
690
697
|
| `~/.hx/system-prompt-append.md` | text appended to whichever prompt is in force |
|
|
691
698
|
| `~/.hx/sessions/` | transcripts, spilled tool output, subagent sessions |
|
|
699
|
+
| `~/.hx/sessions/<id>/trace.html` | where `/trace` writes, unless you name a path |
|
|
692
700
|
| `~/.hx/skills/`, `~/.hx/agents/` | your skills and agents |
|
|
693
701
|
| `~/.hx/projects/<project>/settings.local.json` | this machine's settings for one project - where "always allow" lands |
|
|
694
702
|
| `./.hx/settings.json` | project settings, checked in if you like — read by HX, never written |
|
|
@@ -772,6 +780,79 @@ rest to the session directory, and hands the model that path to grep.
|
|
|
772
780
|
|
|
773
781
|
---
|
|
774
782
|
|
|
783
|
+
## Tracing a session
|
|
784
|
+
|
|
785
|
+
`/trace` writes everything that happened to one self-contained HTML file and
|
|
786
|
+
opens it in a browser:
|
|
787
|
+
|
|
788
|
+
```
|
|
789
|
+
/trace # ~/.hx/sessions/<id>/trace.html
|
|
790
|
+
/trace ~/Desktop/bug.html # somewhere you choose
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
Everything means everything. The system prompt, the project context, the skills
|
|
794
|
+
index, every tool schema as the provider received it, every prompt, every
|
|
795
|
+
reply, every thinking block, every tool call next to the result it produced,
|
|
796
|
+
the per-call token ledger, and the files HX changed. Late-injected reminders
|
|
797
|
+
and compacted turns are in there too, behind a toggle that says how many there
|
|
798
|
+
are — a trace that hides the reminders and the superseded turns is not a trace
|
|
799
|
+
of the session that ran.
|
|
800
|
+
|
|
801
|
+
Nothing is summarised and nothing is prettified. Assistant text is shown as it
|
|
802
|
+
arrived, backticks and all, because a trace exists to answer "what actually
|
|
803
|
+
went through" and a rendered version of that is a different document. Long tool
|
|
804
|
+
results are folded rather than truncated, with the character count on the fold.
|
|
805
|
+
|
|
806
|
+
**Thinking, where the provider allows it.** How much of a model's reasoning HX
|
|
807
|
+
ever sees is the provider's decision, not HX's. Anthropic's models return the
|
|
808
|
+
thinking itself. The Responses API - the OpenAI and Codex routes - returns a
|
|
809
|
+
one-line summary in plaintext and the reasoning encrypted, to be replayed on
|
|
810
|
+
the next call and read by nobody, HX included. A trace labels which of the two
|
|
811
|
+
it is holding, because a one-line thinking block is otherwise indistinguishable
|
|
812
|
+
from a trace that dropped the rest.
|
|
813
|
+
|
|
814
|
+
A running session traces as it stands right now, including the turn that just
|
|
815
|
+
landed. The page says so, because a trace of a live session is a snapshot.
|
|
816
|
+
|
|
817
|
+
**Reading it.** Long tool results are folded and the reference panels - the
|
|
818
|
+
prompt, the schemas, the ledger - start shut, so the transcript is what you
|
|
819
|
+
land on. "Expand all" opens the whole page, panels included, and turns into
|
|
820
|
+
"Collapse all". A session with no turns yet opens its panels on its own, since
|
|
821
|
+
there is nothing else to read.
|
|
822
|
+
|
|
823
|
+
**Dark or light.** The page follows your system by default and a button in the
|
|
824
|
+
corner overrides it, remembered per browser. No stylesheet is fetched to do it.
|
|
825
|
+
|
|
826
|
+
**Nothing is uploaded and nothing is fetched.** The payload, the stylesheet and
|
|
827
|
+
the script all live in the file, so it opens offline, renders the same a year
|
|
828
|
+
later, and a trace of a private repository stays as private as the directory
|
|
829
|
+
you wrote it into. The embedded payload is also the machine-readable trace: it
|
|
830
|
+
is JSON, in a `<script id="trace-data">` element, and `/trace` is the only way
|
|
831
|
+
HX will ever put your transcript anywhere but `~/.hx`.
|
|
832
|
+
|
|
833
|
+
From outside a session, on any transcript on the machine:
|
|
834
|
+
|
|
835
|
+
```sh
|
|
836
|
+
hx trace # the last session in this directory
|
|
837
|
+
hx trace 20260918-214743-b5b8f9 # one by id
|
|
838
|
+
hx trace ~/somewhere.html # the last session, to a path you choose
|
|
839
|
+
hx trace <id> ~/somewhere.html # both, to a path or a directory
|
|
840
|
+
open "$(hx trace)" # the path is all that goes to stdout
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
`hx trace` reads the transcript and runs nothing, so it works on a session that
|
|
844
|
+
ended months ago and on one another terminal has open right now.
|
|
845
|
+
|
|
846
|
+
**What a trace can be missing.** The prompt and the tool schemas are written to
|
|
847
|
+
the transcript on the session's first provider call — the first moment the tool
|
|
848
|
+
registry has settled, because MCP servers connect after the loop is built. A
|
|
849
|
+
session traced before that call, or one recorded by a version of HX older than
|
|
850
|
+
this feature, has the transcript but not the prompt, and the page leaves those
|
|
851
|
+
panels out rather than guessing. `/trace` inside a live session fills them in
|
|
852
|
+
from the loop either way.
|
|
853
|
+
|
|
854
|
+
---
|
|
855
|
+
|
|
775
856
|
## Reading and editing code
|
|
776
857
|
|
|
777
858
|
**Line anchors.** `Read` labels every line with a short content hash:
|
|
@@ -797,6 +878,10 @@ and is unchanged; anchors are a second shape, not a replacement. Turn the whole
|
|
|
797
878
|
thing off with `"tools": {"hashline": false}` and reads go back to plain
|
|
798
879
|
`cat -n`.
|
|
799
880
|
|
|
881
|
+
**Images.** `Read` on a PNG, JPEG, GIF, WebP, BMP or TIFF - or on a file with
|
|
882
|
+
no extension whose bytes are one - returns the image itself for the model to
|
|
883
|
+
look at, normalised the way a pasted one is (see [Images](#images)).
|
|
884
|
+
|
|
800
885
|
**Symbols** answers structural questions with a parse tree rather than a
|
|
801
886
|
regex — three modes, `outline` for one file's shape, `definition` for where a
|
|
802
887
|
name is defined, `references` for where it is used:
|
|
@@ -33,6 +33,7 @@ dependencies = [
|
|
|
33
33
|
"pygments>=2.17",
|
|
34
34
|
# CommonMark parsing. The renderer is ours; the parser is not worth writing.
|
|
35
35
|
"markdown-it-py>=3.0",
|
|
36
|
+
"pillow>=10.4",
|
|
36
37
|
]
|
|
37
38
|
|
|
38
39
|
[project.optional-dependencies]
|
|
@@ -60,7 +61,9 @@ dev = [
|
|
|
60
61
|
hx = "hx.cli:main"
|
|
61
62
|
|
|
62
63
|
[project.urls]
|
|
63
|
-
Homepage = "https://
|
|
64
|
+
Homepage = "https://hx.sunilaleti.dev"
|
|
65
|
+
Documentation = "https://hx.sunilaleti.dev"
|
|
66
|
+
Source = "https://github.com/aletisunil/hx"
|
|
64
67
|
Issues = "https://github.com/aletisunil/hx/issues"
|
|
65
68
|
|
|
66
69
|
# The version lives in the package, not here - three hand-maintained copies
|
|
@@ -12,6 +12,7 @@ where the browser reaches the host by another address.
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
import asyncio
|
|
15
|
+
import html
|
|
15
16
|
import os
|
|
16
17
|
import threading
|
|
17
18
|
from dataclasses import dataclass
|
|
@@ -49,7 +50,18 @@ p{{margin:0;color:#9aa4b2}}
|
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
def _page(title: str, body: str, *, ok: bool) -> bytes:
|
|
52
|
-
|
|
53
|
+
"""Render the one page this server serves.
|
|
54
|
+
|
|
55
|
+
Both fields are escaped: ``body`` carries the provider's ``error``
|
|
56
|
+
parameter, which arrives from the network and is under nobody's control
|
|
57
|
+
here. Interpolated raw, ``?error=<img src=x onerror=...>`` executed on an
|
|
58
|
+
origin that is otherwise this machine's own.
|
|
59
|
+
"""
|
|
60
|
+
return _PAGE.format(
|
|
61
|
+
title=html.escape(title),
|
|
62
|
+
body=html.escape(body),
|
|
63
|
+
colour="#7ee787" if ok else "#ff7b72",
|
|
64
|
+
).encode()
|
|
53
65
|
|
|
54
66
|
|
|
55
67
|
class _Handler(BaseHTTPRequestHandler):
|
|
@@ -21,6 +21,7 @@ from __future__ import annotations
|
|
|
21
21
|
|
|
22
22
|
import asyncio
|
|
23
23
|
import json
|
|
24
|
+
import os
|
|
24
25
|
import time
|
|
25
26
|
from collections.abc import Awaitable, Callable
|
|
26
27
|
from dataclasses import dataclass, field, replace
|
|
@@ -172,9 +173,22 @@ class AuthStore:
|
|
|
172
173
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
173
174
|
payload = {pid: cred.to_json() for pid, cred in sorted(data.items())}
|
|
174
175
|
|
|
176
|
+
# Created 0600, not chmod'd to it afterwards. The file is opened at
|
|
177
|
+
# whatever the umask allows and only narrowed once the tokens are
|
|
178
|
+
# already in it, which leaves a window - at a path anyone can predict -
|
|
179
|
+
# where the credential is world-readable. Passing the mode to `open`
|
|
180
|
+
# closes it: there is no instant at which the descriptor exists and the
|
|
181
|
+
# permissions are wrong.
|
|
175
182
|
tmp = path.with_suffix(".json.tmp")
|
|
176
|
-
|
|
177
|
-
|
|
183
|
+
descriptor = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
184
|
+
try:
|
|
185
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
186
|
+
handle.write(json.dumps(payload, indent=2))
|
|
187
|
+
except BaseException:
|
|
188
|
+
tmp.unlink(missing_ok=True)
|
|
189
|
+
raise
|
|
190
|
+
# O_CREAT leaves an existing file's mode alone, so a store written by
|
|
191
|
+
# an older HX is narrowed here rather than keeping its old permissions.
|
|
178
192
|
tmp.chmod(0o600)
|
|
179
193
|
tmp.replace(path)
|
|
180
194
|
|