hx-cli 0.2.3__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hx_cli-0.2.4/.gitignore +19 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/CHANGELOG.md +86 -1
- hx_cli-0.2.3/README.md → hx_cli-0.2.4/PKG-INFO +117 -0
- hx_cli-0.2.3/PKG-INFO → hx_cli-0.2.4/README.md +78 -37
- {hx_cli-0.2.3 → hx_cli-0.2.4}/pyproject.toml +3 -1
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/__init__.py +1 -1
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/auth/oauth/callback.py +13 -1
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/auth/store.py +16 -2
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/cli.py +81 -1
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/core/loop.py +73 -15
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/core/session.py +69 -5
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/mcp/client.py +28 -1
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/permissions/engine.py +128 -11
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/permissions/sandbox.py +46 -6
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/providers/openrouter.py +16 -2
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tools/base.py +12 -8
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tools/bash.py +54 -17
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tools/websearch.py +8 -0
- hx_cli-0.2.4/src/hx/trace.py +887 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/commands.py +102 -1
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/runtime.py +1 -1
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/auth/test_codex_oauth.py +21 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/auth/test_store.py +58 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/core/test_loop.py +206 -1
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/core/test_session.py +70 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/mcp/fixtures/echo_server.py +22 -1
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/mcp/test_manager.py +34 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/permissions/test_engine.py +111 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/permissions/test_sandbox.py +63 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/providers/test_openrouter.py +65 -0
- hx_cli-0.2.4/tests/test_trace.py +515 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tools/test_background.py +64 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tools/test_bash.py +48 -0
- hx_cli-0.2.4/tests/tui/test_trace_command.py +257 -0
- hx_cli-0.2.3/.gitignore +0 -16
- {hx_cli-0.2.3 → hx_cli-0.2.4}/.github/workflows/ci.yml +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/examples/hooks/check.sh +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/install.sh +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/agents/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/agents/definitions.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/agents/subagent.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/auth/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/auth/oauth/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/auth/oauth/browser.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/auth/oauth/codex.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/auth/oauth/devin.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/auth/oauth/pkce.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/auth/resolve.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/config.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/core/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/core/checkpoints.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/core/compaction.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/core/context.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/core/events.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/core/lateinject.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/core/messages.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/core/title.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/core/usage.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/docs.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/frontmatter.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/git.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/hooks/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/hooks/engine.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/hooks/spec.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/keys.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/mcp/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/mcp/manager.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/net.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/paths.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/permissions/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/permissions/parser.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/providers/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/providers/base.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/providers/codex.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/providers/codex_catalogue.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/providers/devin.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/providers/devin_catalogue.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/providers/devin_wire.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/providers/fake.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/providers/models.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/providers/protowire.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/providers/registry.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/providers/responses_codec.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/skills/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/skills/loader.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/skills/runtime.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/ansi.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/buffer.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/component.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/demo.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/editor.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/keydecode.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/loop.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/markdown.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/primitives.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/sanitize.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/screen.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/syntax.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/terminal.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/undo.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/width.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/term/word_nav.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tools/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tools/anchors.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tools/edit.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tools/glob.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tools/grep.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tools/output.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tools/read.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tools/registry.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tools/symbols.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tools/task.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tools/todo.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tools/write.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/app.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/clipboard.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/format.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/fuzzy.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/glyphs.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/killring.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/limits.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/paint.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/renderers.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/roles.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/theme.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/theme_json.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/themes/ansi.json +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/themes/dark.json +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/themes/light.json +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/views/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/views/blocks.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/views/dialog.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/views/login.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/views/permission.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/views/pickers.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/views/prompt.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/views/status.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/src/hx/tui/views/transcript.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/agents/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/agents/test_subagent.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/auth/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/auth/test_devin_oauth.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/auth/test_pkce.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/auth/test_resolve.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/conftest.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/core/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/core/test_checkpoints.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/core/test_compaction.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/core/test_context.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/core/test_lateinject.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/core/test_rewind.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/core/test_settings_layers.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/core/test_settings_migration.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/core/test_steering.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/core/test_title.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/core/test_usage.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/hooks/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/hooks/test_engine.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/hooks/test_example_check.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/mcp/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/permissions/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/permissions/test_parser.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/providers/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/providers/test_catalogue.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/providers/test_codex.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/providers/test_codex_catalogue.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/providers/test_devin.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/providers/test_devin_catalogue.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/providers/test_protowire.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/providers/test_registry.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/providers/test_responses_codec.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/skills/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/skills/test_loader.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/term/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/term/conftest.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/term/test_ansi.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/term/test_keydecode.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/term/test_loop.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/term/test_markdown.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/term/test_primitives.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/term/test_sanitize.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/term/test_screen.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/term/test_terminal.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/term/test_width.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/test_cli.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/test_docs.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/test_git.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/test_install.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/test_integration.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/test_live.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/test_live_codex.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/test_live_devin.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/test_live_tavily.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/test_net.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/test_paths.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/test_readme.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tools/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tools/test_files.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tools/test_hashline.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tools/test_output.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tools/test_symbols.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tools/test_todo.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tools/test_websearch.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/support.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/test_autocomplete.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/test_clipboard.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/test_effort.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/test_keys.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/test_login.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/test_renderers.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/test_session.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/test_theme_json.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/views/__init__.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/views/test_blocks.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/views/test_permission.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/views/test_pickers.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/views/test_prompt.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/views/test_runtime.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/views/test_status.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/tests/tui/views/test_transcript.py +0 -0
- {hx_cli-0.2.3 → hx_cli-0.2.4}/uv.lock +0 -0
hx_cli-0.2.4/.gitignore
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.venv/
|
|
4
|
+
venv/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.mypy_cache/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
htmlcov/
|
|
13
|
+
.DS_Store
|
|
14
|
+
.hx/sessions/
|
|
15
|
+
# Not .hx/settings.json: that file is the project's *shared* configuration and
|
|
16
|
+
# is meant to be checked in. What HX writes on your behalf - permission grants
|
|
17
|
+
# and the rest - lives under $HX_HOME/projects/<slug>, outside the checkout
|
|
18
|
+
# entirely, so there is nothing here left to hide.
|
|
19
|
+
snapshot_report.html
|
|
@@ -28,6 +28,90 @@ the project follows [semantic versioning](https://semver.org/).
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
+
## [0.2.4] - 2026-09-19
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- `/trace [path]`: write the whole session to one self-contained HTML page and
|
|
36
|
+
open it. The system prompt, the project context, the skills index, every tool
|
|
37
|
+
schema, every prompt and reply, every thinking block, every tool call beside
|
|
38
|
+
the result it produced, the per-call token ledger and the files HX changed.
|
|
39
|
+
Late-injected reminders and compacted turns are included behind a toggle.
|
|
40
|
+
A session still running traces as it stands, including the turn that just
|
|
41
|
+
landed. The page follows your system's dark or light setting and a button
|
|
42
|
+
overrides it; nothing is fetched and nothing is uploaded.
|
|
43
|
+
- `hx trace [SESSION_ID] [PATH]`: the same page for any transcript on the
|
|
44
|
+
machine, without starting a session. Defaults to the last session in this
|
|
45
|
+
directory and prints only the path, so `open "$(hx trace)"` works. Either
|
|
46
|
+
argument may be given alone - a lone one that looks like a path is one.
|
|
47
|
+
- The system prompt and the tool schemas are now written to the transcript on a
|
|
48
|
+
session's first provider call, so a trace of a finished session can say what
|
|
49
|
+
the model was told and not only what it said.
|
|
50
|
+
|
|
51
|
+
### Security
|
|
52
|
+
|
|
53
|
+
- Permission rules are matched against the path a specifier resolves to, not
|
|
54
|
+
the text it was written as. `deny: Read(~/.ssh/**)` was got past by asking
|
|
55
|
+
for `../../.ssh/id_rsa`, or through a symlink planted in the project. The
|
|
56
|
+
same resolution stops an allow rule leaking: `allow: Edit(src/**)` used to
|
|
57
|
+
cover `src/../../etc/passwd`.
|
|
58
|
+
- `WebFetch` is matched against every URL it was given rather than the first,
|
|
59
|
+
so `deny: WebFetch(https://host/**)` is no longer a question of list order.
|
|
60
|
+
The approval prompt spells the URLs out too; it said `urls=[2 items]`, which
|
|
61
|
+
answered the only question that mattered with a number.
|
|
62
|
+
- The generated Seatbelt profile no longer grants write access to all of
|
|
63
|
+
`/private/var/folders` - every user's and every process's temp and cache.
|
|
64
|
+
`$TMPDIR` was resolved precisely to avoid that, and the profile handed it
|
|
65
|
+
back on the next line. This user's own container stays writable.
|
|
66
|
+
- `~/.hx/auth.json` is created with mode 0600 rather than narrowed to it after
|
|
67
|
+
the tokens are written, closing a window at a predictable path where the
|
|
68
|
+
credential was readable by any local account.
|
|
69
|
+
- The OAuth callback page escapes what the provider sent. An `error` parameter
|
|
70
|
+
carrying markup ran as HTML on a loopback origin that is otherwise the
|
|
71
|
+
user's own.
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
|
|
75
|
+
- Slash commands read the session that is in force rather than the one HX
|
|
76
|
+
opened with. After `/resume`, `/trace` wrote a page of zeros into the empty
|
|
77
|
+
startup session's directory and `/cost` reported nobody's usage: the command
|
|
78
|
+
context had captured the session object at startup, and `/resume` rebinds it.
|
|
79
|
+
- A session whose title was the trace template's own data placeholder had the
|
|
80
|
+
whole payload spliced into the page's `<title>`.
|
|
81
|
+
- A trace labelled an encrypted reasoning block "thinking (signed)", which read
|
|
82
|
+
as though the page had dropped the rest of it. It now says the text is the
|
|
83
|
+
provider's summary and the reasoning came back encrypted, and a block that
|
|
84
|
+
arrived with no summary at all says that rather than rendering an empty box.
|
|
85
|
+
- MCP servers survive a large response. One JSON-RPC message is one line and
|
|
86
|
+
asyncio's stream limit is 64 KiB, so a tool returning a file did not truncate
|
|
87
|
+
- it raised out of the read loop and killed the connection for the rest of
|
|
88
|
+
the session. The limit is now 16 MiB, and overshooting it says so.
|
|
89
|
+
- A tool call is no longer dropped when a stream ends without a `finish_reason`.
|
|
90
|
+
The arguments were buffered until that field arrived, so a complete call was
|
|
91
|
+
discarded and the turn reported `end_turn`: the model appeared to stop for no
|
|
92
|
+
reason, and the tokens were already paid for.
|
|
93
|
+
- The Linux sandbox starts again for anyone with credential *files*. Denied
|
|
94
|
+
paths were all shadowed with `--tmpfs`, which mounts a directory and aborts
|
|
95
|
+
bwrap when aimed at a file - and six of them are files, `~/.hx/auth.json`
|
|
96
|
+
among them. Files are shadowed with `/dev/null` instead. bwrap also starts in
|
|
97
|
+
the policy's directory rather than whatever `$PWD` happened to hold.
|
|
98
|
+
- `PersistentShell.cwd` follows the shell. It claimed to track it and was in
|
|
99
|
+
fact the launch directory forever, which meant a background job always
|
|
100
|
+
started where the session opened rather than where `cd` had left it.
|
|
101
|
+
- A background job no longer leaks its log file descriptor for the life of the
|
|
102
|
+
session.
|
|
103
|
+
- A record appended while the transcript was being written is no longer
|
|
104
|
+
dropped. The queue is taken in one step, and a failed write puts its records
|
|
105
|
+
back rather than losing them.
|
|
106
|
+
- An automatic compaction with nothing to summarise is no longer announced and
|
|
107
|
+
rerun every turn for the rest of the session. `/compact` still answers either
|
|
108
|
+
way.
|
|
109
|
+
- `/trace` takes `--no-open`, and never launches a *terminal* browser: with no
|
|
110
|
+
display, `webbrowser` falls through to lynx or w3m, which opens in the
|
|
111
|
+
terminal HX is drawing on and takes the session display with it.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
31
115
|
## [0.2.3] - 2026-09-18
|
|
32
116
|
|
|
33
117
|
### Added
|
|
@@ -481,7 +565,8 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
|
|
|
481
565
|
- `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
|
|
482
566
|
self-update, and `install.sh` bootstrapping uv with a pinned Python.
|
|
483
567
|
|
|
484
|
-
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.
|
|
568
|
+
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.4...HEAD
|
|
569
|
+
[0.2.4]: https://github.com/aletisunil/hx/compare/v0.2.3...v0.2.4
|
|
485
570
|
[0.2.3]: https://github.com/aletisunil/hx/compare/v0.2.2...v0.2.3
|
|
486
571
|
[0.2.2]: https://github.com/aletisunil/hx/compare/v0.2.1...v0.2.2
|
|
487
572
|
[0.2.1]: https://github.com/aletisunil/hx/compare/v0.2.0...v0.2.1
|
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hx-cli
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: HX - an agent harness for the terminal
|
|
5
|
+
Project-URL: Homepage, https://hx.sunilaleti.dev
|
|
6
|
+
Project-URL: Documentation, https://hx.sunilaleti.dev
|
|
7
|
+
Project-URL: Source, https://github.com/aletisunil/hx
|
|
8
|
+
Project-URL: Issues, https://github.com/aletisunil/hx/issues
|
|
9
|
+
Author: Sunil Aleti
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: agent,cli,coding-agent,llm,openrouter,tui
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: httpx>=0.27
|
|
20
|
+
Requires-Dist: markdown-it-py>=3.0
|
|
21
|
+
Requires-Dist: pydantic>=2.7
|
|
22
|
+
Requires-Dist: pygments>=2.17
|
|
23
|
+
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Requires-Dist: truststore>=0.10
|
|
25
|
+
Requires-Dist: wcwidth>=0.2.13
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
28
|
+
Requires-Dist: pyte>=0.8; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
33
|
+
Requires-Dist: types-pygments; extra == 'dev'
|
|
34
|
+
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
35
|
+
Provides-Extra: symbols
|
|
36
|
+
Requires-Dist: tree-sitter-language-pack>=0.7; extra == 'symbols'
|
|
37
|
+
Requires-Dist: tree-sitter>=0.23; extra == 'symbols'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
1
40
|
# HX
|
|
2
41
|
|
|
3
42
|
An agent harness for the terminal. Python core, its own terminal renderer,
|
|
@@ -260,6 +299,9 @@ hx -p "explain this repo" # headless: streams to stdout, tool activity to stde
|
|
|
260
299
|
hx resume # resume the last session here
|
|
261
300
|
hx resume <session-id> # resume a specific one
|
|
262
301
|
hx prompt # print the system prompt this directory would use
|
|
302
|
+
hx trace # write the last session here to an HTML page
|
|
303
|
+
hx trace out.html # the same, to a path you choose
|
|
304
|
+
hx trace <id> out.html # a specific session, to a path you choose
|
|
263
305
|
hx --model openai/gpt-5 # override the model for one run
|
|
264
306
|
hx --mode plan # start read-only
|
|
265
307
|
hx --cwd ../other-project # run against a different directory
|
|
@@ -407,6 +449,7 @@ than being silently resolved.
|
|
|
407
449
|
| `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
|
|
408
450
|
| `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
|
|
409
451
|
| `/copy` | copy the last reply to the clipboard |
|
|
452
|
+
| `/trace [path]` | write the whole session to a self-contained HTML page |
|
|
410
453
|
| `/fullscreen [on\|off]` | take the whole window, with the prompt pinned to the bottom |
|
|
411
454
|
| `/mouse [on\|off]` | mouse reporting, and with it your terminal's own text selection |
|
|
412
455
|
| `/init` | generate an `AGENTS.md` for the project |
|
|
@@ -652,6 +695,7 @@ files load here unchanged. `src/hx/tui/themes/dark.json` is the reference.
|
|
|
652
695
|
| `~/.hx/system-prompt.md` | your system prompt, replacing the built-in one |
|
|
653
696
|
| `~/.hx/system-prompt-append.md` | text appended to whichever prompt is in force |
|
|
654
697
|
| `~/.hx/sessions/` | transcripts, spilled tool output, subagent sessions |
|
|
698
|
+
| `~/.hx/sessions/<id>/trace.html` | where `/trace` writes, unless you name a path |
|
|
655
699
|
| `~/.hx/skills/`, `~/.hx/agents/` | your skills and agents |
|
|
656
700
|
| `~/.hx/projects/<project>/settings.local.json` | this machine's settings for one project - where "always allow" lands |
|
|
657
701
|
| `./.hx/settings.json` | project settings, checked in if you like — read by HX, never written |
|
|
@@ -735,6 +779,79 @@ rest to the session directory, and hands the model that path to grep.
|
|
|
735
779
|
|
|
736
780
|
---
|
|
737
781
|
|
|
782
|
+
## Tracing a session
|
|
783
|
+
|
|
784
|
+
`/trace` writes everything that happened to one self-contained HTML file and
|
|
785
|
+
opens it in a browser:
|
|
786
|
+
|
|
787
|
+
```
|
|
788
|
+
/trace # ~/.hx/sessions/<id>/trace.html
|
|
789
|
+
/trace ~/Desktop/bug.html # somewhere you choose
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
Everything means everything. The system prompt, the project context, the skills
|
|
793
|
+
index, every tool schema as the provider received it, every prompt, every
|
|
794
|
+
reply, every thinking block, every tool call next to the result it produced,
|
|
795
|
+
the per-call token ledger, and the files HX changed. Late-injected reminders
|
|
796
|
+
and compacted turns are in there too, behind a toggle that says how many there
|
|
797
|
+
are — a trace that hides the reminders and the superseded turns is not a trace
|
|
798
|
+
of the session that ran.
|
|
799
|
+
|
|
800
|
+
Nothing is summarised and nothing is prettified. Assistant text is shown as it
|
|
801
|
+
arrived, backticks and all, because a trace exists to answer "what actually
|
|
802
|
+
went through" and a rendered version of that is a different document. Long tool
|
|
803
|
+
results are folded rather than truncated, with the character count on the fold.
|
|
804
|
+
|
|
805
|
+
**Thinking, where the provider allows it.** How much of a model's reasoning HX
|
|
806
|
+
ever sees is the provider's decision, not HX's. Anthropic's models return the
|
|
807
|
+
thinking itself. The Responses API - the OpenAI and Codex routes - returns a
|
|
808
|
+
one-line summary in plaintext and the reasoning encrypted, to be replayed on
|
|
809
|
+
the next call and read by nobody, HX included. A trace labels which of the two
|
|
810
|
+
it is holding, because a one-line thinking block is otherwise indistinguishable
|
|
811
|
+
from a trace that dropped the rest.
|
|
812
|
+
|
|
813
|
+
A running session traces as it stands right now, including the turn that just
|
|
814
|
+
landed. The page says so, because a trace of a live session is a snapshot.
|
|
815
|
+
|
|
816
|
+
**Reading it.** Long tool results are folded and the reference panels - the
|
|
817
|
+
prompt, the schemas, the ledger - start shut, so the transcript is what you
|
|
818
|
+
land on. "Expand all" opens the whole page, panels included, and turns into
|
|
819
|
+
"Collapse all". A session with no turns yet opens its panels on its own, since
|
|
820
|
+
there is nothing else to read.
|
|
821
|
+
|
|
822
|
+
**Dark or light.** The page follows your system by default and a button in the
|
|
823
|
+
corner overrides it, remembered per browser. No stylesheet is fetched to do it.
|
|
824
|
+
|
|
825
|
+
**Nothing is uploaded and nothing is fetched.** The payload, the stylesheet and
|
|
826
|
+
the script all live in the file, so it opens offline, renders the same a year
|
|
827
|
+
later, and a trace of a private repository stays as private as the directory
|
|
828
|
+
you wrote it into. The embedded payload is also the machine-readable trace: it
|
|
829
|
+
is JSON, in a `<script id="trace-data">` element, and `/trace` is the only way
|
|
830
|
+
HX will ever put your transcript anywhere but `~/.hx`.
|
|
831
|
+
|
|
832
|
+
From outside a session, on any transcript on the machine:
|
|
833
|
+
|
|
834
|
+
```sh
|
|
835
|
+
hx trace # the last session in this directory
|
|
836
|
+
hx trace 20260918-214743-b5b8f9 # one by id
|
|
837
|
+
hx trace ~/somewhere.html # the last session, to a path you choose
|
|
838
|
+
hx trace <id> ~/somewhere.html # both, to a path or a directory
|
|
839
|
+
open "$(hx trace)" # the path is all that goes to stdout
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
`hx trace` reads the transcript and runs nothing, so it works on a session that
|
|
843
|
+
ended months ago and on one another terminal has open right now.
|
|
844
|
+
|
|
845
|
+
**What a trace can be missing.** The prompt and the tool schemas are written to
|
|
846
|
+
the transcript on the session's first provider call — the first moment the tool
|
|
847
|
+
registry has settled, because MCP servers connect after the loop is built. A
|
|
848
|
+
session traced before that call, or one recorded by a version of HX older than
|
|
849
|
+
this feature, has the transcript but not the prompt, and the page leaves those
|
|
850
|
+
panels out rather than guessing. `/trace` inside a live session fills them in
|
|
851
|
+
from the loop either way.
|
|
852
|
+
|
|
853
|
+
---
|
|
854
|
+
|
|
738
855
|
## Reading and editing code
|
|
739
856
|
|
|
740
857
|
**Line anchors.** `Read` labels every line with a short content hash:
|
|
@@ -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,
|
|
@@ -297,6 +260,9 @@ hx -p "explain this repo" # headless: streams to stdout, tool activity to stde
|
|
|
297
260
|
hx resume # resume the last session here
|
|
298
261
|
hx resume <session-id> # resume a specific one
|
|
299
262
|
hx prompt # print the system prompt this directory would use
|
|
263
|
+
hx trace # write the last session here to an HTML page
|
|
264
|
+
hx trace out.html # the same, to a path you choose
|
|
265
|
+
hx trace <id> out.html # a specific session, to a path you choose
|
|
300
266
|
hx --model openai/gpt-5 # override the model for one run
|
|
301
267
|
hx --mode plan # start read-only
|
|
302
268
|
hx --cwd ../other-project # run against a different directory
|
|
@@ -444,6 +410,7 @@ than being silently resolved.
|
|
|
444
410
|
| `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
|
|
445
411
|
| `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
|
|
446
412
|
| `/copy` | copy the last reply to the clipboard |
|
|
413
|
+
| `/trace [path]` | write the whole session to a self-contained HTML page |
|
|
447
414
|
| `/fullscreen [on\|off]` | take the whole window, with the prompt pinned to the bottom |
|
|
448
415
|
| `/mouse [on\|off]` | mouse reporting, and with it your terminal's own text selection |
|
|
449
416
|
| `/init` | generate an `AGENTS.md` for the project |
|
|
@@ -689,6 +656,7 @@ files load here unchanged. `src/hx/tui/themes/dark.json` is the reference.
|
|
|
689
656
|
| `~/.hx/system-prompt.md` | your system prompt, replacing the built-in one |
|
|
690
657
|
| `~/.hx/system-prompt-append.md` | text appended to whichever prompt is in force |
|
|
691
658
|
| `~/.hx/sessions/` | transcripts, spilled tool output, subagent sessions |
|
|
659
|
+
| `~/.hx/sessions/<id>/trace.html` | where `/trace` writes, unless you name a path |
|
|
692
660
|
| `~/.hx/skills/`, `~/.hx/agents/` | your skills and agents |
|
|
693
661
|
| `~/.hx/projects/<project>/settings.local.json` | this machine's settings for one project - where "always allow" lands |
|
|
694
662
|
| `./.hx/settings.json` | project settings, checked in if you like — read by HX, never written |
|
|
@@ -772,6 +740,79 @@ rest to the session directory, and hands the model that path to grep.
|
|
|
772
740
|
|
|
773
741
|
---
|
|
774
742
|
|
|
743
|
+
## Tracing a session
|
|
744
|
+
|
|
745
|
+
`/trace` writes everything that happened to one self-contained HTML file and
|
|
746
|
+
opens it in a browser:
|
|
747
|
+
|
|
748
|
+
```
|
|
749
|
+
/trace # ~/.hx/sessions/<id>/trace.html
|
|
750
|
+
/trace ~/Desktop/bug.html # somewhere you choose
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
Everything means everything. The system prompt, the project context, the skills
|
|
754
|
+
index, every tool schema as the provider received it, every prompt, every
|
|
755
|
+
reply, every thinking block, every tool call next to the result it produced,
|
|
756
|
+
the per-call token ledger, and the files HX changed. Late-injected reminders
|
|
757
|
+
and compacted turns are in there too, behind a toggle that says how many there
|
|
758
|
+
are — a trace that hides the reminders and the superseded turns is not a trace
|
|
759
|
+
of the session that ran.
|
|
760
|
+
|
|
761
|
+
Nothing is summarised and nothing is prettified. Assistant text is shown as it
|
|
762
|
+
arrived, backticks and all, because a trace exists to answer "what actually
|
|
763
|
+
went through" and a rendered version of that is a different document. Long tool
|
|
764
|
+
results are folded rather than truncated, with the character count on the fold.
|
|
765
|
+
|
|
766
|
+
**Thinking, where the provider allows it.** How much of a model's reasoning HX
|
|
767
|
+
ever sees is the provider's decision, not HX's. Anthropic's models return the
|
|
768
|
+
thinking itself. The Responses API - the OpenAI and Codex routes - returns a
|
|
769
|
+
one-line summary in plaintext and the reasoning encrypted, to be replayed on
|
|
770
|
+
the next call and read by nobody, HX included. A trace labels which of the two
|
|
771
|
+
it is holding, because a one-line thinking block is otherwise indistinguishable
|
|
772
|
+
from a trace that dropped the rest.
|
|
773
|
+
|
|
774
|
+
A running session traces as it stands right now, including the turn that just
|
|
775
|
+
landed. The page says so, because a trace of a live session is a snapshot.
|
|
776
|
+
|
|
777
|
+
**Reading it.** Long tool results are folded and the reference panels - the
|
|
778
|
+
prompt, the schemas, the ledger - start shut, so the transcript is what you
|
|
779
|
+
land on. "Expand all" opens the whole page, panels included, and turns into
|
|
780
|
+
"Collapse all". A session with no turns yet opens its panels on its own, since
|
|
781
|
+
there is nothing else to read.
|
|
782
|
+
|
|
783
|
+
**Dark or light.** The page follows your system by default and a button in the
|
|
784
|
+
corner overrides it, remembered per browser. No stylesheet is fetched to do it.
|
|
785
|
+
|
|
786
|
+
**Nothing is uploaded and nothing is fetched.** The payload, the stylesheet and
|
|
787
|
+
the script all live in the file, so it opens offline, renders the same a year
|
|
788
|
+
later, and a trace of a private repository stays as private as the directory
|
|
789
|
+
you wrote it into. The embedded payload is also the machine-readable trace: it
|
|
790
|
+
is JSON, in a `<script id="trace-data">` element, and `/trace` is the only way
|
|
791
|
+
HX will ever put your transcript anywhere but `~/.hx`.
|
|
792
|
+
|
|
793
|
+
From outside a session, on any transcript on the machine:
|
|
794
|
+
|
|
795
|
+
```sh
|
|
796
|
+
hx trace # the last session in this directory
|
|
797
|
+
hx trace 20260918-214743-b5b8f9 # one by id
|
|
798
|
+
hx trace ~/somewhere.html # the last session, to a path you choose
|
|
799
|
+
hx trace <id> ~/somewhere.html # both, to a path or a directory
|
|
800
|
+
open "$(hx trace)" # the path is all that goes to stdout
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
`hx trace` reads the transcript and runs nothing, so it works on a session that
|
|
804
|
+
ended months ago and on one another terminal has open right now.
|
|
805
|
+
|
|
806
|
+
**What a trace can be missing.** The prompt and the tool schemas are written to
|
|
807
|
+
the transcript on the session's first provider call — the first moment the tool
|
|
808
|
+
registry has settled, because MCP servers connect after the loop is built. A
|
|
809
|
+
session traced before that call, or one recorded by a version of HX older than
|
|
810
|
+
this feature, has the transcript but not the prompt, and the page leaves those
|
|
811
|
+
panels out rather than guessing. `/trace` inside a live session fills them in
|
|
812
|
+
from the loop either way.
|
|
813
|
+
|
|
814
|
+
---
|
|
815
|
+
|
|
775
816
|
## Reading and editing code
|
|
776
817
|
|
|
777
818
|
**Line anchors.** `Read` labels every line with a short content hash:
|
|
@@ -60,7 +60,9 @@ dev = [
|
|
|
60
60
|
hx = "hx.cli:main"
|
|
61
61
|
|
|
62
62
|
[project.urls]
|
|
63
|
-
Homepage = "https://
|
|
63
|
+
Homepage = "https://hx.sunilaleti.dev"
|
|
64
|
+
Documentation = "https://hx.sunilaleti.dev"
|
|
65
|
+
Source = "https://github.com/aletisunil/hx"
|
|
64
66
|
Issues = "https://github.com/aletisunil/hx/issues"
|
|
65
67
|
|
|
66
68
|
# 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
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
``hx -p "..."`` print mode: run one prompt headless, stream to stdout
|
|
5
5
|
``hx resume [id]`` resume a session
|
|
6
6
|
``hx prompt`` print the resolved system prompt
|
|
7
|
+
``hx trace [id]`` write a session to a self-contained HTML page
|
|
7
8
|
``hx mcp ...`` manage MCP servers
|
|
8
9
|
``hx docs [section]`` print the shipped manual
|
|
9
10
|
``hx changelog [ver]`` print what shipped in each version
|
|
@@ -35,6 +36,8 @@ Usage:
|
|
|
35
36
|
hx -p, --print PROMPT Run one prompt headlessly and print the result
|
|
36
37
|
hx resume [SESSION_ID] Resume a previous session
|
|
37
38
|
hx prompt Print the system prompt this directory would use
|
|
39
|
+
hx trace [SESSION_ID] [PATH]
|
|
40
|
+
Write a session to a self-contained HTML trace
|
|
38
41
|
hx mcp list|add|remove Manage MCP servers
|
|
39
42
|
hx auth [set|clear] Show or change the OpenRouter API key
|
|
40
43
|
hx docs [SECTION|--all] Print the manual, or one section of it
|
|
@@ -97,6 +100,8 @@ def _dispatch(args: list[str]) -> int:
|
|
|
97
100
|
return run_auth_command(list(parsed.rest))
|
|
98
101
|
if parsed.command == "prompt":
|
|
99
102
|
return run_prompt_command(parsed)
|
|
103
|
+
if parsed.command == "trace":
|
|
104
|
+
return run_trace_command(parsed)
|
|
100
105
|
if parsed.command == "docs":
|
|
101
106
|
return run_docs_command(list(parsed.rest))
|
|
102
107
|
if parsed.command == "changelog":
|
|
@@ -162,7 +167,7 @@ def parse_args(args: list[str]) -> ParsedArgs:
|
|
|
162
167
|
|
|
163
168
|
if positional:
|
|
164
169
|
head, *tail = positional
|
|
165
|
-
if head in {"resume", "prompt", "mcp", "upgrade", "auth"}:
|
|
170
|
+
if head in {"resume", "prompt", "trace", "mcp", "upgrade", "auth"}:
|
|
166
171
|
parsed.command = head
|
|
167
172
|
parsed.rest = tuple(tail)
|
|
168
173
|
if head == "resume" and tail:
|
|
@@ -704,6 +709,81 @@ def run_prompt_command(parsed: ParsedArgs) -> int:
|
|
|
704
709
|
return 0
|
|
705
710
|
|
|
706
711
|
|
|
712
|
+
def _looks_like_a_path(arg: str) -> bool:
|
|
713
|
+
"""Whether a lone ``hx trace`` argument is a destination, not a session id.
|
|
714
|
+
|
|
715
|
+
Session ids are ``20260918-215601-7608a6b0``: no separator, no extension,
|
|
716
|
+
no leading ``~`` or ``.``. Anything wearing one of those is somewhere the
|
|
717
|
+
user wants the file, and reading it as an id produces ``no session
|
|
718
|
+
'/Users/you/bug.html'`` - an error about the wrong thing entirely.
|
|
719
|
+
"""
|
|
720
|
+
return (
|
|
721
|
+
"/" in arg or os.sep in arg or arg.startswith(("~", ".")) or arg.lower().endswith(".html")
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def run_trace_command(parsed: ParsedArgs) -> int:
|
|
726
|
+
"""``hx trace [SESSION_ID] [PATH]`` - write a session out as one HTML page.
|
|
727
|
+
|
|
728
|
+
Reads the transcript rather than running anything, so it works on a session
|
|
729
|
+
that ended months ago and on one another terminal has open right now. A
|
|
730
|
+
live session traced this way is current as of its last flush, which is
|
|
731
|
+
every message; ``/trace`` inside that session is the one that also catches
|
|
732
|
+
the usage of a turn still in flight.
|
|
733
|
+
|
|
734
|
+
Either argument may be given alone. A lone one that looks like a path is
|
|
735
|
+
one, so ``hx trace ~/bug.html`` means what ``/trace ~/bug.html`` means
|
|
736
|
+
inside a session rather than hunting for a session by that name.
|
|
737
|
+
"""
|
|
738
|
+
from hx.core.session import SessionNotFound, latest_session, load_session
|
|
739
|
+
from hx.trace import TRACE_FILENAME, build_trace, default_trace_path, write_trace
|
|
740
|
+
|
|
741
|
+
rest = list(parsed.rest)
|
|
742
|
+
cwd = (parsed.cwd or Path.cwd()).resolve()
|
|
743
|
+
|
|
744
|
+
target: str | None = None
|
|
745
|
+
session_id: str | None = None
|
|
746
|
+
if len(rest) > 1:
|
|
747
|
+
session_id, target = rest[0], rest[1]
|
|
748
|
+
elif rest:
|
|
749
|
+
if _looks_like_a_path(rest[0]):
|
|
750
|
+
target = rest[0]
|
|
751
|
+
else:
|
|
752
|
+
session_id = rest[0]
|
|
753
|
+
|
|
754
|
+
if session_id is None:
|
|
755
|
+
latest = latest_session(cwd)
|
|
756
|
+
if latest is None:
|
|
757
|
+
print(f"error: no sessions recorded for {cwd}", file=sys.stderr)
|
|
758
|
+
return 1
|
|
759
|
+
session_id = latest.session_id
|
|
760
|
+
|
|
761
|
+
try:
|
|
762
|
+
session = load_session(session_id)
|
|
763
|
+
except SessionNotFound:
|
|
764
|
+
print(f"error: no session {session_id!r}", file=sys.stderr)
|
|
765
|
+
return 1
|
|
766
|
+
|
|
767
|
+
destination = default_trace_path(session)
|
|
768
|
+
if target is not None:
|
|
769
|
+
# Relative paths stay relative to the shell, not to ``--cwd``: that flag
|
|
770
|
+
# picks the project to trace, and a shell user typing ``out.html`` means
|
|
771
|
+
# the directory they are standing in.
|
|
772
|
+
destination = Path(target).expanduser()
|
|
773
|
+
if destination.is_dir():
|
|
774
|
+
destination = destination / TRACE_FILENAME
|
|
775
|
+
|
|
776
|
+
try:
|
|
777
|
+
written = write_trace(build_trace(session), destination)
|
|
778
|
+
except OSError as exc:
|
|
779
|
+
print(f"error: could not write the trace: {exc}", file=sys.stderr)
|
|
780
|
+
return 1
|
|
781
|
+
|
|
782
|
+
# The path on stdout alone, so `open "$(hx trace)"` works.
|
|
783
|
+
print(written)
|
|
784
|
+
return 0
|
|
785
|
+
|
|
786
|
+
|
|
707
787
|
def run_docs_command(args: list[str]) -> int:
|
|
708
788
|
"""``hx docs [SECTION|--all]`` - the manual that shipped with this version.
|
|
709
789
|
|