hx-cli 0.2.5__tar.gz → 0.2.6__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 → hx_cli-0.2.6}/CHANGELOG.md +49 -1
- {hx_cli-0.2.5 → hx_cli-0.2.6}/PKG-INFO +46 -6
- {hx_cli-0.2.5 → hx_cli-0.2.6}/README.md +45 -5
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/__init__.py +1 -1
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/cli.py +218 -12
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/session.py +59 -9
- hx_cli-0.2.6/src/hx/mcp/client.py +662 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/mcp/manager.py +208 -22
- hx_cli-0.2.6/src/hx/mcp/oauth.py +766 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/screen.py +38 -7
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/registry.py +4 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/app.py +4 -3
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/commands.py +101 -8
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/format.py +22 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/renderers.py +69 -16
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/runtime.py +27 -11
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/login.py +3 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/pickers.py +79 -15
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_session.py +51 -0
- hx_cli-0.2.6/tests/mcp/fixtures/oauth_server.py +438 -0
- hx_cli-0.2.6/tests/mcp/test_oauth.py +670 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_screen.py +27 -1
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_cli.py +145 -0
- hx_cli-0.2.6/tests/tui/test_mcp_login.py +102 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_renderers.py +74 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/views/test_runtime.py +170 -0
- hx_cli-0.2.5/src/hx/mcp/client.py +0 -367
- {hx_cli-0.2.5 → hx_cli-0.2.6}/.github/workflows/ci.yml +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/.gitignore +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/examples/hooks/check.sh +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/install.sh +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/pyproject.toml +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/agents/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/agents/definitions.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/agents/subagent.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/oauth/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/oauth/browser.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/oauth/callback.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/oauth/codex.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/oauth/devin.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/oauth/pkce.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/resolve.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/auth/store.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/config.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/checkpoints.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/compaction.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/context.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/events.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/images.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/lateinject.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/loop.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/messages.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/title.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/core/usage.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/docs.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/frontmatter.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/git.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/hooks/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/hooks/engine.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/hooks/spec.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/keys.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/mcp/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/net.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/paths.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/permissions/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/permissions/engine.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/permissions/parser.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/permissions/sandbox.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/base.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/codex.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/codex_catalogue.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/devin.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/devin_catalogue.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/devin_wire.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/fake.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/models.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/openrouter.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/protowire.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/registry.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/providers/responses_codec.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/skills/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/skills/loader.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/skills/runtime.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/ansi.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/buffer.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/component.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/demo.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/editor.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/keydecode.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/loop.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/markdown.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/primitives.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/sanitize.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/syntax.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/terminal.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/undo.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/width.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/term/word_nav.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/anchors.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/base.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/bash.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/edit.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/glob.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/grep.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/output.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/read.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/symbols.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/task.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/todo.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/websearch.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tools/write.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/trace.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/clipboard.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/fuzzy.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/glyphs.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/killring.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/limits.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/paint.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/roles.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/theme.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/theme_json.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/themes/ansi.json +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/themes/dark.json +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/themes/light.json +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/blocks.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/dialog.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/permission.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/prompt.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/status.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/src/hx/tui/views/transcript.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/agents/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/agents/test_subagent.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/auth/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/auth/test_codex_oauth.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/auth/test_devin_oauth.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/auth/test_pkce.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/auth/test_resolve.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/auth/test_store.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/conftest.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_checkpoints.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_compaction.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_context.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_images.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_lateinject.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_loop.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_loop_images.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_rewind.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_settings_layers.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_settings_migration.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_steering.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_title.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/core/test_usage.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/hooks/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/hooks/test_engine.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/hooks/test_example_check.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/mcp/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/mcp/fixtures/echo_server.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/mcp/test_manager.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/permissions/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/permissions/test_engine.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/permissions/test_parser.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/permissions/test_sandbox.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_catalogue.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_codex.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_codex_catalogue.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_devin.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_devin_catalogue.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_openrouter.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_protowire.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_registry.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/providers/test_responses_codec.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/skills/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/skills/test_loader.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/conftest.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_ansi.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_keydecode.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_loop.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_markdown.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_primitives.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_sanitize.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_terminal.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/term/test_width.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_docs.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_git.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_install.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_integration.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_live.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_live_codex.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_live_devin.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_live_tavily.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_net.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_paths.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_readme.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/test_trace.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/test_background.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/test_bash.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/test_files.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/test_hashline.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/test_output.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/test_symbols.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/test_todo.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tools/test_websearch.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/support.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_autocomplete.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_clipboard.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_effort.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_images.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_keys.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_login.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_session.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_theme_json.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/test_trace_command.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/views/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/views/test_blocks.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/views/test_permission.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/views/test_pickers.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/views/test_prompt.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/views/test_status.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/tests/tui/views/test_transcript.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.6}/uv.lock +0 -0
|
@@ -28,6 +28,53 @@ the project follows [semantic versioning](https://semver.org/).
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
+
## [0.2.6] - 2026-09-23
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- Sign in to remote MCP servers over OAuth. A server that asks for it -
|
|
36
|
+
Atlassian's `https://mcp.atlassian.com/v2/mcp`, for one - shows as needing
|
|
37
|
+
sign-in; `/mcp login <server>` or `hx mcp login <server>` opens the browser
|
|
38
|
+
and the tools arrive without a restart. Tokens refresh on their own;
|
|
39
|
+
`/mcp logout <server>` forgets them. `"oauth": {"clientId": ...}` in
|
|
40
|
+
`mcp.json` covers servers without client registration.
|
|
41
|
+
- MCP servers that change their tool list mid-session have the new list
|
|
42
|
+
swapped in, whether they announce it in a reply or on their own stream.
|
|
43
|
+
- `hx mcp add NAME --url URL --header "Name: value"` saves headers, such as an
|
|
44
|
+
API token, with the server.
|
|
45
|
+
- `/mcp reconnect <server>` restarts one server without restarting HX.
|
|
46
|
+
- `/resume` always offers a list to switch to: this directory's sessions
|
|
47
|
+
first, then every other directory's, each row naming where it lives.
|
|
48
|
+
Picking one from elsewhere restarts HX in that directory on it, with the
|
|
49
|
+
same `--mode` and `--model`. The session already open is no longer listed,
|
|
50
|
+
and the list is no longer cut at 20.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- `/resume` said "No previous sessions in this directory" in a fresh
|
|
55
|
+
directory, or inside a session resumed from another one, instead of
|
|
56
|
+
listing anything.
|
|
57
|
+
- `hx resume <id>` ran the session in whatever directory you typed it in,
|
|
58
|
+
not the one it was recorded in, so its paths and tool calls pointed at the
|
|
59
|
+
wrong project.
|
|
60
|
+
- `hx mcp add NAME --url URL`, `--user`, and server arguments like `npx -y`
|
|
61
|
+
were refused as unknown options, so none of them could be run.
|
|
62
|
+
- `hx mcp add` dropped headers from the entry it wrote.
|
|
63
|
+
- A remote MCP server that expires its session is initialized again instead
|
|
64
|
+
of failing every call until HX restarts.
|
|
65
|
+
- A multi-line shell command no longer breaks the screen. Its approval shows
|
|
66
|
+
one row per line, the tool call shows the first line and `+N lines`
|
|
67
|
+
(`ctrl+o` shows the rest), and the approval options and prompt are no longer
|
|
68
|
+
drawn several times over.
|
|
69
|
+
- `/resume` from inside a resumed session showed the resumed conversation
|
|
70
|
+
pushed off the top of the screen, leaving only the "Resumed" notice.
|
|
71
|
+
- A session title with a line break in it no longer splits its row in the
|
|
72
|
+
`/resume` picker.
|
|
73
|
+
- Started in your home directory, the status bar named it `~/.`; it now reads
|
|
74
|
+
`~`.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
31
78
|
## [0.2.5] - 2026-09-23
|
|
32
79
|
|
|
33
80
|
### Added
|
|
@@ -592,7 +639,8 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
|
|
|
592
639
|
- `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
|
|
593
640
|
self-update, and `install.sh` bootstrapping uv with a pinned Python.
|
|
594
641
|
|
|
595
|
-
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.
|
|
642
|
+
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.6...HEAD
|
|
643
|
+
[0.2.6]: https://github.com/aletisunil/hx/compare/v0.2.5...v0.2.6
|
|
596
644
|
[0.2.5]: https://github.com/aletisunil/hx/compare/v0.2.4...v0.2.5
|
|
597
645
|
[0.2.4]: https://github.com/aletisunil/hx/compare/v0.2.3...v0.2.4
|
|
598
646
|
[0.2.3]: https://github.com/aletisunil/hx/compare/v0.2.2...v0.2.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hx-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: HX - an agent harness for the terminal
|
|
5
5
|
Project-URL: Homepage, https://hx.sunilaleti.dev
|
|
6
6
|
Project-URL: Documentation, https://hx.sunilaleti.dev
|
|
@@ -299,7 +299,7 @@ hx # interactive TUI in the current directory
|
|
|
299
299
|
hx -p "explain this repo" # headless: streams to stdout, tool activity to stderr
|
|
300
300
|
hx -p "why?" --image s.png # attach an image to a headless prompt; repeatable
|
|
301
301
|
hx resume # resume the last session here
|
|
302
|
-
hx resume <session-id> # resume a specific one
|
|
302
|
+
hx resume <session-id> # resume a specific one, in the directory it was recorded in
|
|
303
303
|
hx prompt # print the system prompt this directory would use
|
|
304
304
|
hx trace # write the last session here to an HTML page
|
|
305
305
|
hx trace out.html # the same, to a path you choose
|
|
@@ -311,6 +311,8 @@ hx --no-sandbox # disable OS sandboxing (rules still apply)
|
|
|
311
311
|
hx --system-prompt @p.md # replace the system prompt for one run
|
|
312
312
|
hx --append-system-prompt "Always run the tests" # add to it; repeatable
|
|
313
313
|
hx mcp list|add|remove # manage MCP servers
|
|
314
|
+
hx mcp login <server> # sign in to a remote MCP server in the browser
|
|
315
|
+
hx mcp logout <server> # forget a remote MCP server's sign-in
|
|
314
316
|
hx auth [set|clear] # manage the OpenRouter key
|
|
315
317
|
hx auth login [provider] # sign in (openrouter, openai-codex, devin)
|
|
316
318
|
hx auth logout <provider> # forget a stored credential
|
|
@@ -478,14 +480,14 @@ than being silently resolved.
|
|
|
478
480
|
| `/cost` | tokens, cache savings, spend |
|
|
479
481
|
| `/compact [focus]` | summarise older turns now |
|
|
480
482
|
| `/clear` | fresh session, same directory |
|
|
481
|
-
| `/resume` |
|
|
483
|
+
| `/resume` | switch to another session - this directory's first, then the rest |
|
|
482
484
|
| `/rewind` | go back to an earlier prompt, restoring the files HX changed |
|
|
483
485
|
| `/title [text]` | show or set this session's name |
|
|
484
486
|
| `/prompt` | the system prompt this session is running with |
|
|
485
487
|
| `/todos` | show the current plan |
|
|
486
488
|
| `/skills` | installed skills |
|
|
487
489
|
| `/agents` | subagent types |
|
|
488
|
-
| `/mcp` | server status |
|
|
490
|
+
| `/mcp [login\|logout\|reconnect <server>]` | server status, and signing in to remote servers |
|
|
489
491
|
| `/hooks` | configured hooks, and any that were refused |
|
|
490
492
|
| `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
|
|
491
493
|
| `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
|
|
@@ -701,6 +703,13 @@ if you want a cheaper model for it — and it is counted in `/cost` like any oth
|
|
|
701
703
|
If the call fails the session is still named, from your first message. `/title
|
|
702
704
|
<text>` renames it.
|
|
703
705
|
|
|
706
|
+
`/resume` lists every session except the one you are in: this directory's
|
|
707
|
+
first, then other directories', each row naming where it lives. A session from
|
|
708
|
+
this directory is swapped in place. One from elsewhere belongs to that
|
|
709
|
+
directory - its sandbox, permission rules, prompt and tools - so HX restarts
|
|
710
|
+
itself there with `hx resume <id>`, keeping options like `--mode` and
|
|
711
|
+
`--model`.
|
|
712
|
+
|
|
704
713
|
### Themes
|
|
705
714
|
|
|
706
715
|
A theme is a JSON file: a `vars` block of raw colours, and a `colors` block
|
|
@@ -996,10 +1005,41 @@ construction.
|
|
|
996
1005
|
}
|
|
997
1006
|
```
|
|
998
1007
|
|
|
999
|
-
Or `hx mcp add local python server.py
|
|
1008
|
+
Or `hx mcp add local python server.py`, or
|
|
1009
|
+
`hx mcp add remote --url https://example.com/mcp`. Tools arrive namespaced
|
|
1000
1010
|
`mcp__<server>__<tool>` in a deterministic order. Servers connect concurrently
|
|
1001
1011
|
with a per-server timeout; one that is broken or slow logs a warning and is
|
|
1002
|
-
dropped rather than taking the session with it.
|
|
1012
|
+
dropped rather than taking the session with it. A server that changes its tool
|
|
1013
|
+
list mid-session (`notifications/tools/list_changed`) has the new list swapped
|
|
1014
|
+
in without a restart.
|
|
1015
|
+
|
|
1016
|
+
A remote server that asks for sign-in - Atlassian's, for one - is signed in to
|
|
1017
|
+
over OAuth, with nothing to configure:
|
|
1018
|
+
|
|
1019
|
+
```json
|
|
1020
|
+
{ "mcpServers": { "atlassian": { "url": "https://mcp.atlassian.com/v2/mcp" } } }
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
It shows as needing sign-in until you run `/mcp login atlassian` in a session
|
|
1024
|
+
or `hx mcp login atlassian` in a shell. That opens the browser, and the tools
|
|
1025
|
+
arrive as soon as you approve - no restart. HX follows the MCP authorization
|
|
1026
|
+
spec: it finds the authorization server from the server's metadata, registers
|
|
1027
|
+
itself as a client, signs in with PKCE, and keeps the token in `~/.hx/auth.json`
|
|
1028
|
+
(mode 0600), refreshing it before it expires and again if the server rejects
|
|
1029
|
+
it. Over SSH, paste the final redirect URL into the prompt instead.
|
|
1030
|
+
`/mcp logout atlassian` forgets it.
|
|
1031
|
+
|
|
1032
|
+
A server that does not offer client registration needs a client you register
|
|
1033
|
+
with it yourself, and anything else about the sign-in can be pinned the same
|
|
1034
|
+
way:
|
|
1035
|
+
|
|
1036
|
+
```json
|
|
1037
|
+
"oauth": { "clientId": "...", "clientSecret": "...", "scope": "read write", "callbackPort": 33418 }
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
To send a token of your own instead - an API key - give the server an
|
|
1041
|
+
`Authorization` header, and OAuth stays out of the way:
|
|
1042
|
+
`hx mcp add jira --url https://mcp.atlassian.com/v2/mcp --header "Authorization: Basic <base64 email:token>"`.
|
|
1003
1043
|
|
|
1004
1044
|
**Hooks** are shell commands HX runs at named points in a turn. Four events:
|
|
1005
1045
|
`PreToolUse`, `PostToolUse`, `UserPromptSubmit` and `Stop`.
|
|
@@ -259,7 +259,7 @@ hx # interactive TUI in the current directory
|
|
|
259
259
|
hx -p "explain this repo" # headless: streams to stdout, tool activity to stderr
|
|
260
260
|
hx -p "why?" --image s.png # attach an image to a headless prompt; repeatable
|
|
261
261
|
hx resume # resume the last session here
|
|
262
|
-
hx resume <session-id> # resume a specific one
|
|
262
|
+
hx resume <session-id> # resume a specific one, in the directory it was recorded in
|
|
263
263
|
hx prompt # print the system prompt this directory would use
|
|
264
264
|
hx trace # write the last session here to an HTML page
|
|
265
265
|
hx trace out.html # the same, to a path you choose
|
|
@@ -271,6 +271,8 @@ hx --no-sandbox # disable OS sandboxing (rules still apply)
|
|
|
271
271
|
hx --system-prompt @p.md # replace the system prompt for one run
|
|
272
272
|
hx --append-system-prompt "Always run the tests" # add to it; repeatable
|
|
273
273
|
hx mcp list|add|remove # manage MCP servers
|
|
274
|
+
hx mcp login <server> # sign in to a remote MCP server in the browser
|
|
275
|
+
hx mcp logout <server> # forget a remote MCP server's sign-in
|
|
274
276
|
hx auth [set|clear] # manage the OpenRouter key
|
|
275
277
|
hx auth login [provider] # sign in (openrouter, openai-codex, devin)
|
|
276
278
|
hx auth logout <provider> # forget a stored credential
|
|
@@ -438,14 +440,14 @@ than being silently resolved.
|
|
|
438
440
|
| `/cost` | tokens, cache savings, spend |
|
|
439
441
|
| `/compact [focus]` | summarise older turns now |
|
|
440
442
|
| `/clear` | fresh session, same directory |
|
|
441
|
-
| `/resume` |
|
|
443
|
+
| `/resume` | switch to another session - this directory's first, then the rest |
|
|
442
444
|
| `/rewind` | go back to an earlier prompt, restoring the files HX changed |
|
|
443
445
|
| `/title [text]` | show or set this session's name |
|
|
444
446
|
| `/prompt` | the system prompt this session is running with |
|
|
445
447
|
| `/todos` | show the current plan |
|
|
446
448
|
| `/skills` | installed skills |
|
|
447
449
|
| `/agents` | subagent types |
|
|
448
|
-
| `/mcp` | server status |
|
|
450
|
+
| `/mcp [login\|logout\|reconnect <server>]` | server status, and signing in to remote servers |
|
|
449
451
|
| `/hooks` | configured hooks, and any that were refused |
|
|
450
452
|
| `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
|
|
451
453
|
| `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
|
|
@@ -661,6 +663,13 @@ if you want a cheaper model for it — and it is counted in `/cost` like any oth
|
|
|
661
663
|
If the call fails the session is still named, from your first message. `/title
|
|
662
664
|
<text>` renames it.
|
|
663
665
|
|
|
666
|
+
`/resume` lists every session except the one you are in: this directory's
|
|
667
|
+
first, then other directories', each row naming where it lives. A session from
|
|
668
|
+
this directory is swapped in place. One from elsewhere belongs to that
|
|
669
|
+
directory - its sandbox, permission rules, prompt and tools - so HX restarts
|
|
670
|
+
itself there with `hx resume <id>`, keeping options like `--mode` and
|
|
671
|
+
`--model`.
|
|
672
|
+
|
|
664
673
|
### Themes
|
|
665
674
|
|
|
666
675
|
A theme is a JSON file: a `vars` block of raw colours, and a `colors` block
|
|
@@ -956,10 +965,41 @@ construction.
|
|
|
956
965
|
}
|
|
957
966
|
```
|
|
958
967
|
|
|
959
|
-
Or `hx mcp add local python server.py
|
|
968
|
+
Or `hx mcp add local python server.py`, or
|
|
969
|
+
`hx mcp add remote --url https://example.com/mcp`. Tools arrive namespaced
|
|
960
970
|
`mcp__<server>__<tool>` in a deterministic order. Servers connect concurrently
|
|
961
971
|
with a per-server timeout; one that is broken or slow logs a warning and is
|
|
962
|
-
dropped rather than taking the session with it.
|
|
972
|
+
dropped rather than taking the session with it. A server that changes its tool
|
|
973
|
+
list mid-session (`notifications/tools/list_changed`) has the new list swapped
|
|
974
|
+
in without a restart.
|
|
975
|
+
|
|
976
|
+
A remote server that asks for sign-in - Atlassian's, for one - is signed in to
|
|
977
|
+
over OAuth, with nothing to configure:
|
|
978
|
+
|
|
979
|
+
```json
|
|
980
|
+
{ "mcpServers": { "atlassian": { "url": "https://mcp.atlassian.com/v2/mcp" } } }
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
It shows as needing sign-in until you run `/mcp login atlassian` in a session
|
|
984
|
+
or `hx mcp login atlassian` in a shell. That opens the browser, and the tools
|
|
985
|
+
arrive as soon as you approve - no restart. HX follows the MCP authorization
|
|
986
|
+
spec: it finds the authorization server from the server's metadata, registers
|
|
987
|
+
itself as a client, signs in with PKCE, and keeps the token in `~/.hx/auth.json`
|
|
988
|
+
(mode 0600), refreshing it before it expires and again if the server rejects
|
|
989
|
+
it. Over SSH, paste the final redirect URL into the prompt instead.
|
|
990
|
+
`/mcp logout atlassian` forgets it.
|
|
991
|
+
|
|
992
|
+
A server that does not offer client registration needs a client you register
|
|
993
|
+
with it yourself, and anything else about the sign-in can be pinned the same
|
|
994
|
+
way:
|
|
995
|
+
|
|
996
|
+
```json
|
|
997
|
+
"oauth": { "clientId": "...", "clientSecret": "...", "scope": "read write", "callbackPort": 33418 }
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
To send a token of your own instead - an API key - give the server an
|
|
1001
|
+
`Authorization` header, and OAuth stays out of the way:
|
|
1002
|
+
`hx mcp add jira --url https://mcp.atlassian.com/v2/mcp --header "Authorization: Basic <base64 email:token>"`.
|
|
963
1003
|
|
|
964
1004
|
**Hooks** are shell commands HX runs at named points in a turn. Four events:
|
|
965
1005
|
`PreToolUse`, `PostToolUse`, `UserPromptSubmit` and `Stop`.
|
|
@@ -38,7 +38,8 @@ Usage:
|
|
|
38
38
|
hx prompt Print the system prompt this directory would use
|
|
39
39
|
hx trace [SESSION_ID] [PATH]
|
|
40
40
|
Write a session to a self-contained HTML trace
|
|
41
|
-
hx mcp list|add|remove
|
|
41
|
+
hx mcp list|add|remove|login|logout
|
|
42
|
+
Manage MCP servers, and sign in to remote ones
|
|
42
43
|
hx auth [set|clear] Show or change the OpenRouter API key
|
|
43
44
|
hx docs [SECTION|--all] Print the manual, or one section of it
|
|
44
45
|
hx changelog [VERSION] Print what shipped in each version
|
|
@@ -61,6 +62,14 @@ Options:
|
|
|
61
62
|
DOC_COMMANDS = frozenset({"docs", "changelog"})
|
|
62
63
|
"""Commands that print a shipped document and exit, taking no run options."""
|
|
63
64
|
|
|
65
|
+
VERBATIM_COMMANDS = DOC_COMMANDS | {"mcp", "auth"}
|
|
66
|
+
"""Commands with flags of their own, so their arguments are taken as typed.
|
|
67
|
+
|
|
68
|
+
`hx mcp add jira --url URL` and `hx mcp add fs npx -y server` are the
|
|
69
|
+
documented forms; read as run options, `--url` and `-y` were refused as
|
|
70
|
+
unknown before the command ever saw them.
|
|
71
|
+
"""
|
|
72
|
+
|
|
64
73
|
|
|
65
74
|
@dataclass(slots=True)
|
|
66
75
|
class ParsedArgs:
|
|
@@ -76,6 +85,10 @@ class ParsedArgs:
|
|
|
76
85
|
def main(argv: list[str] | None = None) -> int:
|
|
77
86
|
"""Parse arguments and dispatch. Returns a process exit code."""
|
|
78
87
|
args = list(sys.argv[1:] if argv is None else argv)
|
|
88
|
+
if args and args[0] in VERBATIM_COMMANDS:
|
|
89
|
+
# `hx mcp add fs npx server --help` is a server's argument, not a
|
|
90
|
+
# request for HX's usage.
|
|
91
|
+
return _dispatch(args)
|
|
79
92
|
|
|
80
93
|
if "--version" in args or "-V" in args:
|
|
81
94
|
print(f"hx {__version__}")
|
|
@@ -114,10 +127,11 @@ def _dispatch(args: list[str]) -> int:
|
|
|
114
127
|
|
|
115
128
|
|
|
116
129
|
def parse_args(args: list[str]) -> ParsedArgs:
|
|
117
|
-
# The documentation
|
|
118
|
-
#
|
|
119
|
-
# run of HX with an
|
|
120
|
-
|
|
130
|
+
# The documentation and management commands take their arguments verbatim:
|
|
131
|
+
# `hx docs --all` is a request for a section of the manual, and
|
|
132
|
+
# `hx mcp add jira --url URL` is for `hx mcp`, not a run of HX with an
|
|
133
|
+
# unknown option.
|
|
134
|
+
if args and args[0] in VERBATIM_COMMANDS:
|
|
121
135
|
return ParsedArgs(command=args[0], rest=tuple(args[1:]))
|
|
122
136
|
|
|
123
137
|
parsed = ParsedArgs()
|
|
@@ -250,6 +264,11 @@ class Runtime:
|
|
|
250
264
|
return []
|
|
251
265
|
statuses: list[Any] = await self.mcp.connect_all()
|
|
252
266
|
await self.mcp.register_tools(self.tools)
|
|
267
|
+
for status in statuses:
|
|
268
|
+
if status.needs_login:
|
|
269
|
+
self.notices.append(
|
|
270
|
+
f"MCP server {status.name} needs sign-in. Run /mcp login {status.name}."
|
|
271
|
+
)
|
|
253
272
|
return statuses
|
|
254
273
|
|
|
255
274
|
async def refresh_models_if_stale(self) -> None:
|
|
@@ -582,24 +601,31 @@ def run_tui_command(parsed: ParsedArgs) -> int:
|
|
|
582
601
|
from hx.tui.app import run_tui
|
|
583
602
|
|
|
584
603
|
try:
|
|
585
|
-
|
|
604
|
+
resume = _resume_target(parsed)
|
|
605
|
+
moved = _adopt_session_directory(parsed, resume)
|
|
606
|
+
runtime = build_runtime(parsed, resume=resume)
|
|
586
607
|
except MissingCredential as exc:
|
|
587
608
|
# Onboard for the route the configured model needs, not always for
|
|
588
609
|
# OpenRouter: a user set to a Codex model wants a sign-in, not a key.
|
|
589
610
|
if run_login(exc.provider_id) != 0:
|
|
590
611
|
return _report(exc)
|
|
591
612
|
try:
|
|
592
|
-
runtime = build_runtime(parsed, resume=
|
|
613
|
+
runtime = build_runtime(parsed, resume=resume)
|
|
593
614
|
except Exception as retry_exc:
|
|
594
615
|
return _report(retry_exc)
|
|
595
616
|
except Exception as exc:
|
|
596
617
|
return _report(exc)
|
|
618
|
+
if moved:
|
|
619
|
+
runtime.notices.append(moved)
|
|
620
|
+
|
|
621
|
+
relaunch: str | None = None
|
|
597
622
|
|
|
598
623
|
async def main_async() -> None:
|
|
624
|
+
nonlocal relaunch
|
|
599
625
|
try:
|
|
600
626
|
await runtime.connect_mcp()
|
|
601
627
|
await runtime.refresh_models_if_stale()
|
|
602
|
-
await run_tui(
|
|
628
|
+
relaunch = await run_tui(
|
|
603
629
|
runtime.loop,
|
|
604
630
|
runtime.bus,
|
|
605
631
|
runtime.settings,
|
|
@@ -619,12 +645,95 @@ def run_tui_command(parsed: ParsedArgs) -> int:
|
|
|
619
645
|
await _rename_closed_session(runtime.loop)
|
|
620
646
|
finally:
|
|
621
647
|
runtime.bus.close()
|
|
622
|
-
|
|
648
|
+
# Everything, not only the provider: a relaunch replaces this
|
|
649
|
+
# process, and an MCP server or background job left running would
|
|
650
|
+
# outlive the session that started it.
|
|
651
|
+
await runtime.aclose()
|
|
623
652
|
|
|
624
653
|
asyncio.run(main_async())
|
|
654
|
+
if relaunch is not None:
|
|
655
|
+
_relaunch(relaunch)
|
|
625
656
|
return 0
|
|
626
657
|
|
|
627
658
|
|
|
659
|
+
def _adopt_session_directory(parsed: ParsedArgs, session_id: str | None) -> str | None:
|
|
660
|
+
"""Run ``hx resume <id>`` in the directory the session was recorded in.
|
|
661
|
+
|
|
662
|
+
A session's paths, tool calls and permission grants all mean something in
|
|
663
|
+
its own directory; resumed against another one, the model edits files that
|
|
664
|
+
are not the ones it read. An explicit ``--cwd`` still wins.
|
|
665
|
+
|
|
666
|
+
Returns a notice when the recorded directory is gone and the session is
|
|
667
|
+
resumed here instead.
|
|
668
|
+
"""
|
|
669
|
+
if session_id is None or parsed.cwd is not None:
|
|
670
|
+
return None
|
|
671
|
+
from hx.core.session import SessionNotFound, read_meta
|
|
672
|
+
|
|
673
|
+
try:
|
|
674
|
+
recorded = Path(read_meta(session_id).cwd)
|
|
675
|
+
except SessionNotFound:
|
|
676
|
+
return None # build_runtime reports it
|
|
677
|
+
if recorded.is_dir():
|
|
678
|
+
parsed.cwd = recorded
|
|
679
|
+
return None
|
|
680
|
+
return f"This session was recorded in {recorded}, which no longer exists; resumed here."
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
RUN_OPTIONS_WITH_VALUES = frozenset(
|
|
684
|
+
{"--model", "--mode", "--cwd", "--system-prompt", "--append-system-prompt", "--image"}
|
|
685
|
+
)
|
|
686
|
+
"""The run options that take the next argument as their value."""
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def relaunch_argv(argv: list[str], session_id: str) -> list[str]:
|
|
690
|
+
"""The arguments to start HX again on ``session_id``.
|
|
691
|
+
|
|
692
|
+
The run's own options - ``--mode bypass``, ``--model`` - carry over, since
|
|
693
|
+
the user asked for them and nothing about switching sessions un-asks.
|
|
694
|
+
What picked the old session does not: the ``resume`` and its id, and a
|
|
695
|
+
``--cwd``, which the new session's own directory replaces.
|
|
696
|
+
"""
|
|
697
|
+
kept: list[str] = []
|
|
698
|
+
index = 0
|
|
699
|
+
while index < len(argv):
|
|
700
|
+
arg = argv[index]
|
|
701
|
+
if arg == "--cwd":
|
|
702
|
+
index += 2
|
|
703
|
+
continue
|
|
704
|
+
if arg in RUN_OPTIONS_WITH_VALUES:
|
|
705
|
+
kept += argv[index : index + 2]
|
|
706
|
+
index += 2
|
|
707
|
+
continue
|
|
708
|
+
if arg == "resume":
|
|
709
|
+
index += 1
|
|
710
|
+
if index < len(argv) and not argv[index].startswith("-"):
|
|
711
|
+
index += 1
|
|
712
|
+
continue
|
|
713
|
+
kept.append(arg)
|
|
714
|
+
index += 1
|
|
715
|
+
return [*kept, "resume", session_id]
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _relaunch(session_id: str) -> None:
|
|
719
|
+
"""Replace this process with HX resumed on ``session_id``.
|
|
720
|
+
|
|
721
|
+
``exec`` rather than a child: the terminal, the pid and the shell's job
|
|
722
|
+
control stay exactly as they were, and there is no parent left waiting.
|
|
723
|
+
|
|
724
|
+
From the directory this process was started in, as if the user had typed
|
|
725
|
+
``hx resume <id>`` there: that command moves into the session's own
|
|
726
|
+
directory by itself (:func:`_adopt_session_directory`), and the arguments
|
|
727
|
+
carried over - a script path, ``--system-prompt @p.md`` - were written
|
|
728
|
+
relative to here.
|
|
729
|
+
"""
|
|
730
|
+
prefix = sys.orig_argv[: len(sys.orig_argv) - len(sys.argv) + 1]
|
|
731
|
+
command = [sys.executable, *prefix[1:], *relaunch_argv(sys.argv[1:], session_id)]
|
|
732
|
+
sys.stdout.flush()
|
|
733
|
+
sys.stderr.flush()
|
|
734
|
+
os.execv(sys.executable, command)
|
|
735
|
+
|
|
736
|
+
|
|
628
737
|
RENAME_TIMEOUT_SECONDS = 10.0
|
|
629
738
|
"""A session name is not worth making the user wait for. Exit wins the tie."""
|
|
630
739
|
|
|
@@ -701,7 +810,13 @@ def run_print_command(parsed: ParsedArgs) -> int:
|
|
|
701
810
|
)
|
|
702
811
|
for status in await runtime.connect_mcp():
|
|
703
812
|
if not status.connected:
|
|
704
|
-
|
|
813
|
+
if status.needs_login:
|
|
814
|
+
print(
|
|
815
|
+
f"[mcp] {status.name} {status.error} - run `hx mcp login {status.name}`",
|
|
816
|
+
file=sys.stderr,
|
|
817
|
+
)
|
|
818
|
+
else:
|
|
819
|
+
print(f"[mcp] {status.name} unavailable: {status.error}", file=sys.stderr)
|
|
705
820
|
try:
|
|
706
821
|
result = await runtime.loop.run(parsed.prompt, images)
|
|
707
822
|
await _rename_closed_session(runtime.loop)
|
|
@@ -876,10 +991,14 @@ def _resume_target(parsed: ParsedArgs) -> str | None:
|
|
|
876
991
|
MCP_USAGE = """\
|
|
877
992
|
hx mcp list Show configured servers
|
|
878
993
|
hx mcp add NAME COMMAND [ARGS...] Add a stdio server
|
|
879
|
-
hx mcp add NAME --url URL
|
|
994
|
+
hx mcp add NAME --url URL [--header K:V]... Add an HTTP server
|
|
995
|
+
hx mcp login NAME Sign in to an HTTP server in the browser
|
|
996
|
+
hx mcp logout NAME Forget a server's sign-in
|
|
880
997
|
hx mcp remove NAME Remove a server
|
|
881
998
|
|
|
882
999
|
Add --user to write to ~/.hx/mcp.json instead of the project's .hx/mcp.json.
|
|
1000
|
+
An HTTP server that asks for sign-in gets it over OAuth unless a --header
|
|
1001
|
+
already sends Authorization.
|
|
883
1002
|
"""
|
|
884
1003
|
|
|
885
1004
|
|
|
@@ -891,6 +1010,9 @@ def run_mcp_command(args: list[str]) -> int:
|
|
|
891
1010
|
save_config,
|
|
892
1011
|
)
|
|
893
1012
|
|
|
1013
|
+
if args and args[0] in {"--help", "-h", "help"}:
|
|
1014
|
+
print(MCP_USAGE)
|
|
1015
|
+
return 0
|
|
894
1016
|
user_level = "--user" in args
|
|
895
1017
|
args = [arg for arg in args if arg != "--user"]
|
|
896
1018
|
cwd = Path.cwd()
|
|
@@ -903,6 +1025,9 @@ def run_mcp_command(args: list[str]) -> int:
|
|
|
903
1025
|
return 0
|
|
904
1026
|
statuses = asyncio.run(_probe_servers(configs))
|
|
905
1027
|
for status in statuses:
|
|
1028
|
+
if status.needs_login:
|
|
1029
|
+
print(f"{status.name:<20} {status.error} - run `hx mcp login {status.name}`")
|
|
1030
|
+
continue
|
|
906
1031
|
mark = "ok" if status.connected else "unavailable"
|
|
907
1032
|
detail = f" - {status.error}" if status.error else f" ({status.tool_count} tools)"
|
|
908
1033
|
print(f"{status.name:<20} {mark}{detail}")
|
|
@@ -914,7 +1039,12 @@ def run_mcp_command(args: list[str]) -> int:
|
|
|
914
1039
|
return 2
|
|
915
1040
|
name = args[1]
|
|
916
1041
|
if args[2] == "--url":
|
|
917
|
-
|
|
1042
|
+
try:
|
|
1043
|
+
url, headers = _parse_http_add(args[3:])
|
|
1044
|
+
except UsageError as exc:
|
|
1045
|
+
print(f"error: {exc}\n\n{MCP_USAGE}", file=sys.stderr)
|
|
1046
|
+
return 2
|
|
1047
|
+
config = MCPServerConfig(name=name, transport="http", url=url, headers=headers)
|
|
918
1048
|
else:
|
|
919
1049
|
config = MCPServerConfig(
|
|
920
1050
|
name=name, transport="stdio", command=args[2], args=tuple(args[3:])
|
|
@@ -922,6 +1052,16 @@ def run_mcp_command(args: list[str]) -> int:
|
|
|
922
1052
|
print(f"Added {name} to {save_config(config, cwd, user_level)}")
|
|
923
1053
|
return 0
|
|
924
1054
|
|
|
1055
|
+
if args[0] in {"login", "logout"}:
|
|
1056
|
+
if len(args) < 2:
|
|
1057
|
+
print(MCP_USAGE, file=sys.stderr)
|
|
1058
|
+
return 2
|
|
1059
|
+
server = next((c for c in load_configs(cwd) if c.name == args[1]), None)
|
|
1060
|
+
if server is None:
|
|
1061
|
+
print(f"No server named {args[1]}.", file=sys.stderr)
|
|
1062
|
+
return 1
|
|
1063
|
+
return _mcp_login(server) if args[0] == "login" else _mcp_logout(server)
|
|
1064
|
+
|
|
925
1065
|
if args[0] == "remove":
|
|
926
1066
|
if len(args) < 2:
|
|
927
1067
|
print(MCP_USAGE, file=sys.stderr)
|
|
@@ -936,6 +1076,69 @@ def run_mcp_command(args: list[str]) -> int:
|
|
|
936
1076
|
return 2
|
|
937
1077
|
|
|
938
1078
|
|
|
1079
|
+
def _parse_http_add(rest: list[str]) -> tuple[str, dict[str, str]]:
|
|
1080
|
+
"""``URL [--header 'Name: value']...`` from ``hx mcp add NAME --url``."""
|
|
1081
|
+
if not rest or rest[0].startswith("--"):
|
|
1082
|
+
raise UsageError("--url needs a URL")
|
|
1083
|
+
url, headers = rest[0], {}
|
|
1084
|
+
tail = rest[1:]
|
|
1085
|
+
while tail:
|
|
1086
|
+
flag = tail.pop(0)
|
|
1087
|
+
if flag not in {"--header", "-H"} or not tail:
|
|
1088
|
+
raise UsageError(f"unexpected {flag!r}")
|
|
1089
|
+
name, sep, value = tail.pop(0).partition(":")
|
|
1090
|
+
if not sep or not name.strip():
|
|
1091
|
+
raise UsageError("a header is written 'Name: value'")
|
|
1092
|
+
headers[name.strip()] = value.strip()
|
|
1093
|
+
return url, headers
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def _mcp_login(config: Any) -> int:
|
|
1097
|
+
from hx.auth.oauth.browser import OAuthError
|
|
1098
|
+
from hx.auth.oauth.callback import CallbackError
|
|
1099
|
+
from hx.mcp.client import MCPError
|
|
1100
|
+
from hx.mcp.manager import sign_in
|
|
1101
|
+
from hx.mcp.oauth import SignInNotRequired
|
|
1102
|
+
|
|
1103
|
+
try:
|
|
1104
|
+
asyncio.run(sign_in(config, ConsoleLogin()))
|
|
1105
|
+
except SignInNotRequired as exc:
|
|
1106
|
+
print(str(exc))
|
|
1107
|
+
return 0
|
|
1108
|
+
except (MCPError, OAuthError, CallbackError) as exc:
|
|
1109
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
1110
|
+
return 1
|
|
1111
|
+
except KeyboardInterrupt:
|
|
1112
|
+
print("\nCancelled.", file=sys.stderr)
|
|
1113
|
+
return 1
|
|
1114
|
+
|
|
1115
|
+
status = asyncio.run(_probe_servers([config]))[0]
|
|
1116
|
+
if status.connected:
|
|
1117
|
+
print(f"Signed in to {config.name}: {status.tool_count} tools available.")
|
|
1118
|
+
return 0
|
|
1119
|
+
print(f"Signed in, but {config.name} is still unavailable: {status.error}", file=sys.stderr)
|
|
1120
|
+
return 1
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def _mcp_logout(config: Any) -> int:
|
|
1124
|
+
from hx.auth.store import AuthStore
|
|
1125
|
+
from hx.mcp.oauth import credential_id
|
|
1126
|
+
|
|
1127
|
+
if not config.url:
|
|
1128
|
+
print(f"{config.name} is a local server; there is no sign-in to forget.")
|
|
1129
|
+
return 0
|
|
1130
|
+
if not config.uses_oauth:
|
|
1131
|
+
# The stored sign-in is the server's, not this entry's: another
|
|
1132
|
+
# project may reach the same server over OAuth and still need it.
|
|
1133
|
+
print(f"{config.name} sends its own Authorization header; there is no sign-in to forget.")
|
|
1134
|
+
return 0
|
|
1135
|
+
if AuthStore().delete(credential_id(config.url)):
|
|
1136
|
+
print(f"Signed out of {config.name}.")
|
|
1137
|
+
else:
|
|
1138
|
+
print(f"Not signed in to {config.name}.")
|
|
1139
|
+
return 0
|
|
1140
|
+
|
|
1141
|
+
|
|
939
1142
|
async def _probe_servers(configs: list[Any]) -> list[Any]:
|
|
940
1143
|
from hx.mcp.manager import MCPManager
|
|
941
1144
|
|
|
@@ -1093,6 +1296,9 @@ def run_auth_command(args: list[str]) -> int:
|
|
|
1093
1296
|
from hx.providers import registry
|
|
1094
1297
|
|
|
1095
1298
|
action = args[0] if args else "status"
|
|
1299
|
+
if action in {"--help", "-h", "help"}:
|
|
1300
|
+
print(AUTH_USAGE)
|
|
1301
|
+
return 0
|
|
1096
1302
|
resolver = AuthResolver()
|
|
1097
1303
|
|
|
1098
1304
|
if action == "status":
|