hx-cli 0.2.5__tar.gz → 0.2.7__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.7}/.github/workflows/ci.yml +13 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/.gitignore +1 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/CHANGELOG.md +111 -2
- {hx_cli-0.2.5 → hx_cli-0.2.7}/PKG-INFO +76 -25
- {hx_cli-0.2.5 → hx_cli-0.2.7}/README.md +75 -24
- {hx_cli-0.2.5 → hx_cli-0.2.7}/pyproject.toml +1 -1
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/__init__.py +1 -1
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/cli.py +218 -12
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/config.py +1 -1
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/compaction.py +6 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/loop.py +15 -11
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/session.py +59 -9
- hx_cli-0.2.7/src/hx/mcp/client.py +662 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/mcp/manager.py +208 -22
- hx_cli-0.2.7/src/hx/mcp/oauth.py +766 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/permissions/engine.py +1 -1
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/permissions/sandbox.py +84 -22
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/openrouter.py +16 -6
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/keydecode.py +173 -15
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/loop.py +5 -1
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/markdown.py +19 -7
- hx_cli-0.2.7/src/hx/term/modifiers.py +116 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/primitives.py +54 -3
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/screen.py +38 -7
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/terminal.py +114 -66
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/registry.py +4 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/app.py +4 -3
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/commands.py +135 -24
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/format.py +22 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/limits.py +4 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/renderers.py +69 -16
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/runtime.py +78 -31
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/blocks.py +49 -6
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/login.py +3 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/permission.py +14 -5
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/pickers.py +79 -15
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/prompt.py +149 -15
- hx_cli-0.2.7/tests/conftest.py +28 -0
- hx_cli-0.2.7/tests/e2e/conftest.py +302 -0
- hx_cli-0.2.7/tests/e2e/report.py +205 -0
- hx_cli-0.2.7/tests/e2e/stub.py +363 -0
- hx_cli-0.2.7/tests/e2e/terminal.py +473 -0
- hx_cli-0.2.7/tests/e2e/test_chat.py +213 -0
- hx_cli-0.2.7/tests/e2e/test_cli.py +143 -0
- hx_cli-0.2.7/tests/e2e/test_commands.py +255 -0
- hx_cli-0.2.7/tests/e2e/test_extend.py +276 -0
- hx_cli-0.2.7/tests/e2e/test_input.py +457 -0
- hx_cli-0.2.7/tests/e2e/test_print.py +275 -0
- hx_cli-0.2.7/tests/e2e/test_tools.py +209 -0
- hx_cli-0.2.5/src/hx/mcp/client.py +0 -367
- hx_cli-0.2.5/src/hx/providers/fake.py +0 -124
- hx_cli-0.2.5/tests/agents/test_subagent.py +0 -310
- hx_cli-0.2.5/tests/auth/__init__.py +0 -0
- hx_cli-0.2.5/tests/auth/test_codex_oauth.py +0 -226
- hx_cli-0.2.5/tests/auth/test_devin_oauth.py +0 -119
- hx_cli-0.2.5/tests/auth/test_pkce.py +0 -27
- hx_cli-0.2.5/tests/auth/test_resolve.py +0 -166
- hx_cli-0.2.5/tests/auth/test_store.py +0 -218
- hx_cli-0.2.5/tests/conftest.py +0 -63
- hx_cli-0.2.5/tests/core/__init__.py +0 -0
- hx_cli-0.2.5/tests/core/test_checkpoints.py +0 -200
- hx_cli-0.2.5/tests/core/test_compaction.py +0 -137
- hx_cli-0.2.5/tests/core/test_context.py +0 -158
- hx_cli-0.2.5/tests/core/test_images.py +0 -281
- hx_cli-0.2.5/tests/core/test_lateinject.py +0 -52
- hx_cli-0.2.5/tests/core/test_loop.py +0 -738
- hx_cli-0.2.5/tests/core/test_loop_images.py +0 -181
- hx_cli-0.2.5/tests/core/test_rewind.py +0 -206
- hx_cli-0.2.5/tests/core/test_session.py +0 -264
- hx_cli-0.2.5/tests/core/test_settings_layers.py +0 -67
- hx_cli-0.2.5/tests/core/test_settings_migration.py +0 -152
- hx_cli-0.2.5/tests/core/test_steering.py +0 -203
- hx_cli-0.2.5/tests/core/test_title.py +0 -70
- hx_cli-0.2.5/tests/core/test_usage.py +0 -25
- hx_cli-0.2.5/tests/hooks/__init__.py +0 -0
- hx_cli-0.2.5/tests/hooks/test_engine.py +0 -205
- hx_cli-0.2.5/tests/hooks/test_example_check.py +0 -98
- hx_cli-0.2.5/tests/mcp/__init__.py +0 -0
- hx_cli-0.2.5/tests/mcp/test_manager.py +0 -240
- hx_cli-0.2.5/tests/permissions/__init__.py +0 -0
- hx_cli-0.2.5/tests/permissions/test_engine.py +0 -570
- hx_cli-0.2.5/tests/permissions/test_parser.py +0 -202
- hx_cli-0.2.5/tests/permissions/test_sandbox.py +0 -189
- hx_cli-0.2.5/tests/providers/__init__.py +0 -0
- hx_cli-0.2.5/tests/providers/test_catalogue.py +0 -349
- hx_cli-0.2.5/tests/providers/test_codex.py +0 -113
- hx_cli-0.2.5/tests/providers/test_codex_catalogue.py +0 -205
- hx_cli-0.2.5/tests/providers/test_devin.py +0 -489
- hx_cli-0.2.5/tests/providers/test_devin_catalogue.py +0 -229
- hx_cli-0.2.5/tests/providers/test_openrouter.py +0 -308
- hx_cli-0.2.5/tests/providers/test_protowire.py +0 -100
- hx_cli-0.2.5/tests/providers/test_registry.py +0 -117
- hx_cli-0.2.5/tests/providers/test_responses_codec.py +0 -330
- hx_cli-0.2.5/tests/skills/__init__.py +0 -0
- hx_cli-0.2.5/tests/skills/test_loader.py +0 -157
- hx_cli-0.2.5/tests/term/__init__.py +0 -0
- hx_cli-0.2.5/tests/term/conftest.py +0 -59
- hx_cli-0.2.5/tests/term/test_ansi.py +0 -190
- hx_cli-0.2.5/tests/term/test_keydecode.py +0 -233
- hx_cli-0.2.5/tests/term/test_loop.py +0 -221
- hx_cli-0.2.5/tests/term/test_markdown.py +0 -260
- hx_cli-0.2.5/tests/term/test_primitives.py +0 -206
- hx_cli-0.2.5/tests/term/test_sanitize.py +0 -77
- hx_cli-0.2.5/tests/term/test_screen.py +0 -673
- hx_cli-0.2.5/tests/term/test_terminal.py +0 -321
- hx_cli-0.2.5/tests/term/test_width.py +0 -109
- hx_cli-0.2.5/tests/test_cli.py +0 -264
- hx_cli-0.2.5/tests/test_docs.py +0 -203
- hx_cli-0.2.5/tests/test_git.py +0 -355
- hx_cli-0.2.5/tests/test_install.py +0 -55
- hx_cli-0.2.5/tests/test_integration.py +0 -696
- hx_cli-0.2.5/tests/test_net.py +0 -110
- hx_cli-0.2.5/tests/test_paths.py +0 -54
- hx_cli-0.2.5/tests/test_readme.py +0 -309
- hx_cli-0.2.5/tests/test_trace.py +0 -538
- hx_cli-0.2.5/tests/tools/__init__.py +0 -0
- hx_cli-0.2.5/tests/tools/test_background.py +0 -207
- hx_cli-0.2.5/tests/tools/test_bash.py +0 -210
- hx_cli-0.2.5/tests/tools/test_files.py +0 -284
- hx_cli-0.2.5/tests/tools/test_hashline.py +0 -197
- hx_cli-0.2.5/tests/tools/test_output.py +0 -31
- hx_cli-0.2.5/tests/tools/test_symbols.py +0 -127
- hx_cli-0.2.5/tests/tools/test_todo.py +0 -104
- hx_cli-0.2.5/tests/tools/test_websearch.py +0 -296
- hx_cli-0.2.5/tests/tui/__init__.py +0 -0
- hx_cli-0.2.5/tests/tui/support.py +0 -146
- hx_cli-0.2.5/tests/tui/test_autocomplete.py +0 -186
- hx_cli-0.2.5/tests/tui/test_clipboard.py +0 -111
- hx_cli-0.2.5/tests/tui/test_effort.py +0 -167
- hx_cli-0.2.5/tests/tui/test_images.py +0 -319
- hx_cli-0.2.5/tests/tui/test_keys.py +0 -136
- hx_cli-0.2.5/tests/tui/test_login.py +0 -424
- hx_cli-0.2.5/tests/tui/test_renderers.py +0 -204
- hx_cli-0.2.5/tests/tui/test_session.py +0 -1134
- hx_cli-0.2.5/tests/tui/test_theme_json.py +0 -105
- hx_cli-0.2.5/tests/tui/test_trace_command.py +0 -257
- hx_cli-0.2.5/tests/tui/views/__init__.py +0 -0
- hx_cli-0.2.5/tests/tui/views/test_blocks.py +0 -264
- hx_cli-0.2.5/tests/tui/views/test_permission.py +0 -328
- hx_cli-0.2.5/tests/tui/views/test_pickers.py +0 -314
- hx_cli-0.2.5/tests/tui/views/test_prompt.py +0 -374
- hx_cli-0.2.5/tests/tui/views/test_runtime.py +0 -567
- hx_cli-0.2.5/tests/tui/views/test_status.py +0 -197
- hx_cli-0.2.5/tests/tui/views/test_transcript.py +0 -211
- {hx_cli-0.2.5 → hx_cli-0.2.7}/examples/hooks/check.sh +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/install.sh +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/agents/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/agents/definitions.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/agents/subagent.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/oauth/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/oauth/browser.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/oauth/callback.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/oauth/codex.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/oauth/devin.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/oauth/pkce.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/resolve.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/auth/store.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/checkpoints.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/context.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/events.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/images.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/lateinject.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/messages.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/title.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/core/usage.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/docs.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/frontmatter.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/git.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/hooks/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/hooks/engine.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/hooks/spec.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/keys.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/mcp/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/net.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/paths.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/permissions/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/permissions/parser.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/base.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/codex.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/codex_catalogue.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/devin.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/devin_catalogue.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/devin_wire.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/models.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/protowire.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/registry.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/providers/responses_codec.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/skills/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/skills/loader.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/skills/runtime.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/ansi.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/buffer.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/component.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/demo.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/editor.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/sanitize.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/syntax.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/undo.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/width.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/term/word_nav.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/anchors.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/base.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/bash.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/edit.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/glob.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/grep.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/output.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/read.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/symbols.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/task.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/todo.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/websearch.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tools/write.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/trace.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/clipboard.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/fuzzy.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/glyphs.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/killring.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/paint.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/roles.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/theme.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/theme_json.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/themes/ansi.json +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/themes/dark.json +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/themes/light.json +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/__init__.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/dialog.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/status.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/src/hx/tui/views/transcript.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/tests/__init__.py +0 -0
- {hx_cli-0.2.5/tests/agents → hx_cli-0.2.7/tests/e2e}/__init__.py +0 -0
- {hx_cli-0.2.5/tests/mcp → hx_cli-0.2.7/tests/e2e}/fixtures/echo_server.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/tests/test_live.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/tests/test_live_codex.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/tests/test_live_devin.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/tests/test_live_tavily.py +0 -0
- {hx_cli-0.2.5 → hx_cli-0.2.7}/uv.lock +0 -0
|
@@ -36,8 +36,21 @@ jobs:
|
|
|
36
36
|
- uses: actions/checkout@v4
|
|
37
37
|
- uses: astral-sh/setup-uv@v3
|
|
38
38
|
- run: uv sync --extra dev --python ${{ matrix.python }}
|
|
39
|
+
# bubblewrap, so the Linux sandbox tests run instead of skipping. Ubuntu
|
|
40
|
+
# 24.04 confines unprivileged user namespaces with AppArmor, which bwrap
|
|
41
|
+
# needs; the runner is throwaway, so lift that rather than ship a profile.
|
|
42
|
+
- if: runner.os == 'Linux'
|
|
43
|
+
run: |
|
|
44
|
+
sudo apt-get update && sudo apt-get install -y bubblewrap
|
|
45
|
+
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
|
39
46
|
# `live` needs an API key and real spend, so it never runs in CI.
|
|
40
47
|
- run: uv run pytest -m "not live"
|
|
48
|
+
# What every end-to-end test saw, screen by screen - pass or fail.
|
|
49
|
+
- uses: actions/upload-artifact@v4
|
|
50
|
+
if: always()
|
|
51
|
+
with:
|
|
52
|
+
name: e2e-report-${{ matrix.os }}-${{ matrix.python }}
|
|
53
|
+
path: e2e-report/
|
|
41
54
|
|
|
42
55
|
install-script:
|
|
43
56
|
runs-on: ubuntu-latest
|
|
@@ -7,7 +7,7 @@ wheel, `hx changelog` prints it, and a session can answer "what's new?" or
|
|
|
7
7
|
"which version added web search?" from it rather than from a guess. `hx docs`
|
|
8
8
|
prints the manual next to it.
|
|
9
9
|
|
|
10
|
-
The rules, enforced by `tests/
|
|
10
|
+
The rules, enforced by `tests/e2e/test_cli.py` so a release cannot forget them:
|
|
11
11
|
|
|
12
12
|
- Every released version has a `## [x.y.z] - YYYY-MM-DD` section here, and the
|
|
13
13
|
version in `src/hx/__init__.py` always has one. Bumping the version without
|
|
@@ -28,6 +28,113 @@ the project follows [semantic versioning](https://semver.org/).
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
+
## [0.2.7] - 2026-09-27
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- `HX_OPENROUTER_BASE_URL` points the OpenRouter route at any OpenAI-compatible
|
|
36
|
+
endpoint - a gateway, a recording proxy.
|
|
37
|
+
- Pressing `esc` on a running turn leaves an "Interrupted" line in the transcript,
|
|
38
|
+
so a reply cut off mid-sentence no longer reads as a finished one.
|
|
39
|
+
- A paste over 10 lines or 1000 characters shows in the prompt as
|
|
40
|
+
`[Pasted text #1 +40 lines]` and is sent in full; the token moves and deletes
|
|
41
|
+
as one character, and survives history recall and `ctrl+k`/`ctrl+y`.
|
|
42
|
+
- `\` then `enter` inserts a newline in any terminal, including Apple's Terminal
|
|
43
|
+
and tmux without extended keys, where `shift+enter` cannot be told from `enter`. `\\` then
|
|
44
|
+
`enter` sends a message that ends in a backslash.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- `shift+enter` inserts a newline instead of sending the message in kitty, Ghostty,
|
|
49
|
+
WezTerm, iTerm2 and tmux (with `extended-keys on`): HX now asks the terminal for
|
|
50
|
+
the kitty keyboard protocol, falling back to xterm's modifyOtherKeys, and hands
|
|
51
|
+
it back on exit, `ctrl+z` and `/fullscreen`. `ctrl+shift+z` (redo) works there too.
|
|
52
|
+
- `shift+enter` also inserts a newline on macOS in terminals that send it as a
|
|
53
|
+
plain `enter` (VS Code's, Apple's Terminal) or as `option+enter` (the Claude
|
|
54
|
+
desktop app's terminal panel): HX asks macOS which modifiers are held when the
|
|
55
|
+
key arrives. A real `option+enter` still steers, an `enter` hard on the heels of
|
|
56
|
+
a shifted character (`?` then `enter`) still sends, and `HX_MODIFIER_PROBE=0`
|
|
57
|
+
turns the check off.
|
|
58
|
+
- A multi-line paste keeps its line breaks. Terminals send them as carriage
|
|
59
|
+
returns, which HX was deleting, so a pasted table arrived as one long line.
|
|
60
|
+
- Lines typed with `shift+enter` stay separate lines in the transcript instead of
|
|
61
|
+
being joined into one paragraph.
|
|
62
|
+
- On macOS the sandbox now refuses *writes* to credential paths (`~/.ssh`,
|
|
63
|
+
`~/.aws`, `~/.hx/auth.json`, ...) as well as reads, so a session opened in your
|
|
64
|
+
home directory cannot plant an `authorized_keys` entry.
|
|
65
|
+
- On Linux the same holds for a credential path that does not exist yet: a
|
|
66
|
+
session opened in your home directory with no `~/.ssh` could create one and
|
|
67
|
+
plant an `authorized_keys` on the host. HX now creates such a path, empty and
|
|
68
|
+
private (`0700` for a directory, `0600` for a file), and mounts a read-only
|
|
69
|
+
shadow over it. Existing credential directories are shadowed read-only too, so
|
|
70
|
+
a write fails with "Read-only file system" instead of vanishing.
|
|
71
|
+
- Shell commands the model runs show their output in the transcript again - the
|
|
72
|
+
last lines, with `ctrl+o` for the rest - instead of only "Took 12ms".
|
|
73
|
+
- `/context` no longer fails with "'UsageLedger' object has no attribute
|
|
74
|
+
'total_tokens'"; it lists what fills the window.
|
|
75
|
+
- `/copy` copies the last reply and says how it was copied, instead of failing.
|
|
76
|
+
- `/theme light` reports "Theme: light" rather than "Theme: None".
|
|
77
|
+
- `/compact` on a short session says only that there is nothing to compact, no
|
|
78
|
+
longer followed by a "Compacting…" that never finishes; a compaction that
|
|
79
|
+
saves nothing still reports that it finished, and the context gauge keeps
|
|
80
|
+
counting the system prompt afterwards.
|
|
81
|
+
- `/help` lines its key descriptions up however long a binding is, and a long
|
|
82
|
+
row in `/agents`, `/cost` or any other command output wraps under its own
|
|
83
|
+
column instead of at the left edge.
|
|
84
|
+
- The "always allow" option on an approval names where the rule is really
|
|
85
|
+
saved (`~/.hx`), and the answered approval shows a file relative to the
|
|
86
|
+
project rather than as an absolute path.
|
|
87
|
+
- `/permissions` no longer says the same thing twice.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## [0.2.6] - 2026-09-23
|
|
92
|
+
|
|
93
|
+
### Added
|
|
94
|
+
|
|
95
|
+
- Sign in to remote MCP servers over OAuth. A server that asks for it -
|
|
96
|
+
Atlassian's `https://mcp.atlassian.com/v2/mcp`, for one - shows as needing
|
|
97
|
+
sign-in; `/mcp login <server>` or `hx mcp login <server>` opens the browser
|
|
98
|
+
and the tools arrive without a restart. Tokens refresh on their own;
|
|
99
|
+
`/mcp logout <server>` forgets them. `"oauth": {"clientId": ...}` in
|
|
100
|
+
`mcp.json` covers servers without client registration.
|
|
101
|
+
- MCP servers that change their tool list mid-session have the new list
|
|
102
|
+
swapped in, whether they announce it in a reply or on their own stream.
|
|
103
|
+
- `hx mcp add NAME --url URL --header "Name: value"` saves headers, such as an
|
|
104
|
+
API token, with the server.
|
|
105
|
+
- `/mcp reconnect <server>` restarts one server without restarting HX.
|
|
106
|
+
- `/resume` always offers a list to switch to: this directory's sessions
|
|
107
|
+
first, then every other directory's, each row naming where it lives.
|
|
108
|
+
Picking one from elsewhere restarts HX in that directory on it, with the
|
|
109
|
+
same `--mode` and `--model`. The session already open is no longer listed,
|
|
110
|
+
and the list is no longer cut at 20.
|
|
111
|
+
|
|
112
|
+
### Fixed
|
|
113
|
+
|
|
114
|
+
- `/resume` said "No previous sessions in this directory" in a fresh
|
|
115
|
+
directory, or inside a session resumed from another one, instead of
|
|
116
|
+
listing anything.
|
|
117
|
+
- `hx resume <id>` ran the session in whatever directory you typed it in,
|
|
118
|
+
not the one it was recorded in, so its paths and tool calls pointed at the
|
|
119
|
+
wrong project.
|
|
120
|
+
- `hx mcp add NAME --url URL`, `--user`, and server arguments like `npx -y`
|
|
121
|
+
were refused as unknown options, so none of them could be run.
|
|
122
|
+
- `hx mcp add` dropped headers from the entry it wrote.
|
|
123
|
+
- A remote MCP server that expires its session is initialized again instead
|
|
124
|
+
of failing every call until HX restarts.
|
|
125
|
+
- A multi-line shell command no longer breaks the screen. Its approval shows
|
|
126
|
+
one row per line, the tool call shows the first line and `+N lines`
|
|
127
|
+
(`ctrl+o` shows the rest), and the approval options and prompt are no longer
|
|
128
|
+
drawn several times over.
|
|
129
|
+
- `/resume` from inside a resumed session showed the resumed conversation
|
|
130
|
+
pushed off the top of the screen, leaving only the "Resumed" notice.
|
|
131
|
+
- A session title with a line break in it no longer splits its row in the
|
|
132
|
+
`/resume` picker.
|
|
133
|
+
- Started in your home directory, the status bar named it `~/.`; it now reads
|
|
134
|
+
`~`.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
31
138
|
## [0.2.5] - 2026-09-23
|
|
32
139
|
|
|
33
140
|
### Added
|
|
@@ -592,7 +699,9 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
|
|
|
592
699
|
- `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
|
|
593
700
|
self-update, and `install.sh` bootstrapping uv with a pinned Python.
|
|
594
701
|
|
|
595
|
-
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.
|
|
702
|
+
[Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.7...HEAD
|
|
703
|
+
[0.2.7]: https://github.com/aletisunil/hx/compare/v0.2.6...v0.2.7
|
|
704
|
+
[0.2.6]: https://github.com/aletisunil/hx/compare/v0.2.5...v0.2.6
|
|
596
705
|
[0.2.5]: https://github.com/aletisunil/hx/compare/v0.2.4...v0.2.5
|
|
597
706
|
[0.2.4]: https://github.com/aletisunil/hx/compare/v0.2.3...v0.2.4
|
|
598
707
|
[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.7
|
|
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
|
|
@@ -348,7 +350,7 @@ allowlist it if you would rather not be asked:
|
|
|
348
350
|
```
|
|
349
351
|
|
|
350
352
|
`CHANGELOG.md` lists what each version added, and a release cannot skip it:
|
|
351
|
-
`tests/
|
|
353
|
+
`tests/e2e/test_cli.py` fails when `__version__` has no section (see
|
|
352
354
|
[Releasing](#releasing)).
|
|
353
355
|
|
|
354
356
|
### Keys
|
|
@@ -360,7 +362,7 @@ be rebound (see [Keybindings](#keybindings)).
|
|
|
360
362
|
| Key | Does |
|
|
361
363
|
|---|---|
|
|
362
364
|
| `enter` | send |
|
|
363
|
-
| `
|
|
365
|
+
| `shift+enter` | newline (also `ctrl+j`; in a terminal that cannot send `shift+enter`, end the line with `\` and press `enter`) |
|
|
364
366
|
| `esc` | interrupt the current turn |
|
|
365
367
|
| `ctrl+c` | copy the selected text; with nothing selected, clear the prompt (twice on an empty prompt exits) |
|
|
366
368
|
| `ctrl+d` | exit, when the prompt is empty |
|
|
@@ -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 |
|
|
@@ -662,8 +664,11 @@ merge: it is read per layer, and only from your own settings. See
|
|
|
662
664
|
Environment overrides: `HX_MODEL`, `HX_SUBAGENT_MODEL`, `HX_MAX_TOKENS`,
|
|
663
665
|
`HX_PERMISSION_MODE`, `HX_SANDBOX`, `HX_COMPACT_AT`, `HX_GIT_NOTICES`,
|
|
664
666
|
`HX_THEME`, `HX_QUIET_STARTUP`. Also `HX_HOME` to relocate user state, `HX_TAVILY_API_KEY`
|
|
665
|
-
for web search,
|
|
666
|
-
[Credentials](#credentials))
|
|
667
|
+
for web search, `HX_CA_BUNDLE` / `HX_SSL_NO_VERIFY` for TLS (see
|
|
668
|
+
[Credentials](#credentials)), and `HX_OPENROUTER_BASE_URL` to point the OpenRouter
|
|
669
|
+
route at another OpenAI-compatible endpoint - a gateway, or a recording proxy.
|
|
670
|
+
`HX_MODIFIER_PROBE=0` stops HX asking macOS whether shift is held when an
|
|
671
|
+
`enter` arrives (see [Keys](#keys)).
|
|
667
672
|
|
|
668
673
|
### The system prompt
|
|
669
674
|
|
|
@@ -701,6 +706,13 @@ if you want a cheaper model for it — and it is counted in `/cost` like any oth
|
|
|
701
706
|
If the call fails the session is still named, from your first message. `/title
|
|
702
707
|
<text>` renames it.
|
|
703
708
|
|
|
709
|
+
`/resume` lists every session except the one you are in: this directory's
|
|
710
|
+
first, then other directories', each row naming where it lives. A session from
|
|
711
|
+
this directory is swapped in place. One from elsewhere belongs to that
|
|
712
|
+
directory - its sandbox, permission rules, prompt and tools - so HX restarts
|
|
713
|
+
itself there with `hx resume <id>`, keeping options like `--mode` and
|
|
714
|
+
`--model`.
|
|
715
|
+
|
|
704
716
|
### Themes
|
|
705
717
|
|
|
706
718
|
A theme is a JSON file: a `vars` block of raw colours, and a `colors` block
|
|
@@ -765,7 +777,8 @@ confidence prompts rather than passing.
|
|
|
765
777
|
**An OS sandbox** wraps command execution: Seatbelt on macOS, bubblewrap on
|
|
766
778
|
Linux. The filesystem is readable, writes are confined to the project and the
|
|
767
779
|
temp dir, credential paths (`~/.ssh`, `~/.aws`, and HX's own `auth.json`) are
|
|
768
|
-
unreadable
|
|
780
|
+
unreadable and unwritable - even inside the project, and even before they
|
|
781
|
+
exist - and outbound network is off. If neither backend is present the
|
|
769
782
|
status bar says `no-sandbox` rather than implying protection that is not there.
|
|
770
783
|
|
|
771
784
|
Modes cycle with shift+tab: `plan` (read-only — mutating tools are not even
|
|
@@ -996,10 +1009,41 @@ construction.
|
|
|
996
1009
|
}
|
|
997
1010
|
```
|
|
998
1011
|
|
|
999
|
-
Or `hx mcp add local python server.py
|
|
1012
|
+
Or `hx mcp add local python server.py`, or
|
|
1013
|
+
`hx mcp add remote --url https://example.com/mcp`. Tools arrive namespaced
|
|
1000
1014
|
`mcp__<server>__<tool>` in a deterministic order. Servers connect concurrently
|
|
1001
1015
|
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.
|
|
1016
|
+
dropped rather than taking the session with it. A server that changes its tool
|
|
1017
|
+
list mid-session (`notifications/tools/list_changed`) has the new list swapped
|
|
1018
|
+
in without a restart.
|
|
1019
|
+
|
|
1020
|
+
A remote server that asks for sign-in - Atlassian's, for one - is signed in to
|
|
1021
|
+
over OAuth, with nothing to configure:
|
|
1022
|
+
|
|
1023
|
+
```json
|
|
1024
|
+
{ "mcpServers": { "atlassian": { "url": "https://mcp.atlassian.com/v2/mcp" } } }
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
It shows as needing sign-in until you run `/mcp login atlassian` in a session
|
|
1028
|
+
or `hx mcp login atlassian` in a shell. That opens the browser, and the tools
|
|
1029
|
+
arrive as soon as you approve - no restart. HX follows the MCP authorization
|
|
1030
|
+
spec: it finds the authorization server from the server's metadata, registers
|
|
1031
|
+
itself as a client, signs in with PKCE, and keeps the token in `~/.hx/auth.json`
|
|
1032
|
+
(mode 0600), refreshing it before it expires and again if the server rejects
|
|
1033
|
+
it. Over SSH, paste the final redirect URL into the prompt instead.
|
|
1034
|
+
`/mcp logout atlassian` forgets it.
|
|
1035
|
+
|
|
1036
|
+
A server that does not offer client registration needs a client you register
|
|
1037
|
+
with it yourself, and anything else about the sign-in can be pinned the same
|
|
1038
|
+
way:
|
|
1039
|
+
|
|
1040
|
+
```json
|
|
1041
|
+
"oauth": { "clientId": "...", "clientSecret": "...", "scope": "read write", "callbackPort": 33418 }
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
To send a token of your own instead - an API key - give the server an
|
|
1045
|
+
`Authorization` header, and OAuth stays out of the way:
|
|
1046
|
+
`hx mcp add jira --url https://mcp.atlassian.com/v2/mcp --header "Authorization: Basic <base64 email:token>"`.
|
|
1003
1047
|
|
|
1004
1048
|
**Hooks** are shell commands HX runs at named points in a turn. Four events:
|
|
1005
1049
|
`PreToolUse`, `PostToolUse`, `UserPromptSubmit` and `Stop`.
|
|
@@ -1077,6 +1121,23 @@ uv run mypy # strict
|
|
|
1077
1121
|
uv run hx # run from the checkout
|
|
1078
1122
|
```
|
|
1079
1123
|
|
|
1124
|
+
The suite is end-to-end. Each test in `tests/e2e/` starts the installed `hx`
|
|
1125
|
+
binary - on a real pseudo-terminal for the TUI, or as a plain subprocess for
|
|
1126
|
+
print mode and the other commands - in a private `$HOME` and `$HX_HOME`, and
|
|
1127
|
+
points it at a local OpenRouter stand-in with `HX_OPENROUTER_BASE_URL`.
|
|
1128
|
+
Everything between the keyboard and the socket is the shipped code; only the
|
|
1129
|
+
model's replies are scripted. The screen is read back through a VT emulator,
|
|
1130
|
+
so assertions are on the cells a terminal shows, not on the bytes HX meant to
|
|
1131
|
+
write.
|
|
1132
|
+
|
|
1133
|
+
Every run writes `e2e-report/`: `index.html` shows each test's outcome and
|
|
1134
|
+
every screen it checked, rendered in colour, and one text file per screen lets
|
|
1135
|
+
two runs be compared with `diff -r`. CI uploads it from every test job as an
|
|
1136
|
+
`e2e-report-*` artifact. Set `HX_E2E_REPORT` to write it somewhere else.
|
|
1137
|
+
|
|
1138
|
+
Tests that need the OS sandbox are skipped where there is no backend
|
|
1139
|
+
(`sandbox-exec` on macOS, `bwrap` on Linux).
|
|
1140
|
+
|
|
1080
1141
|
Tests marked `live` hit a real API. The OpenRouter ones cost money:
|
|
1081
1142
|
|
|
1082
1143
|
```sh
|
|
@@ -1093,17 +1154,7 @@ uv run pytest -m live tests/test_live_devin.py
|
|
|
1093
1154
|
|
|
1094
1155
|
They are the only place the wire formats, streaming, tool use and a genuine
|
|
1095
1156
|
cache hit are proven against a real service; everything else runs against the
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
Tests marked `sandbox` exercise the real OS sandbox and are skipped where no
|
|
1099
|
-
backend exists.
|
|
1100
|
-
|
|
1101
|
-
The TUI has SVG layout snapshots. Read the diff before accepting a change to
|
|
1102
|
-
them - they exist to catch a frame that quietly lost a row:
|
|
1103
|
-
|
|
1104
|
-
```sh
|
|
1105
|
-
uv run pytest tests/tui/test_snapshots.py --snapshot-update
|
|
1106
|
-
```
|
|
1157
|
+
stand-in.
|
|
1107
1158
|
|
|
1108
1159
|
### Layout
|
|
1109
1160
|
|
|
@@ -1112,7 +1163,7 @@ src/hx/
|
|
|
1112
1163
|
cli.py config.py paths.py frontmatter.py git.py
|
|
1113
1164
|
core/ loop, context assembly, compaction, late injection, sessions, usage
|
|
1114
1165
|
auth/ credential store, OAuth flows, per-route resolution
|
|
1115
|
-
providers/ OpenRouter, Codex, Devin, the model catalogue
|
|
1166
|
+
providers/ OpenRouter, Codex, Devin, the model catalogue
|
|
1116
1167
|
tools/ Bash, Read, Write, Edit, Glob, Grep, Symbols, TodoWrite, Task,
|
|
1117
1168
|
WebSearch, WebFetch, line anchors, output capping
|
|
1118
1169
|
permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
|
|
@@ -1149,12 +1200,12 @@ Each release:
|
|
|
1149
1200
|
# 1. move CHANGELOG.md's [Unreleased] entries under the new version, dated,
|
|
1150
1201
|
# and leave a fresh empty [Unreleased] behind
|
|
1151
1202
|
# 2. bump __version__ in src/hx/__init__.py
|
|
1152
|
-
# 3. uv run pytest tests/
|
|
1203
|
+
# 3. uv run pytest tests/e2e/test_cli.py # the version must have a section
|
|
1153
1204
|
git commit -am "release: X.Y.Z"
|
|
1154
1205
|
git tag v$(uv run hx --version | cut -d' ' -f2) && git push origin main --tags
|
|
1155
1206
|
```
|
|
1156
1207
|
|
|
1157
|
-
Step 1 is not optional and is not a convention: `tests/
|
|
1208
|
+
Step 1 is not optional and is not a convention: `tests/e2e/test_cli.py` fails the
|
|
1158
1209
|
whole suite when `__version__` has no `CHANGELOG.md` section, so a release that
|
|
1159
1210
|
skips it cannot get past CI to the `publish` job. The file ships inside the
|
|
1160
1211
|
wheel and `hx changelog` prints it, so a version with no section there reaches
|
|
@@ -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
|
|
@@ -308,7 +310,7 @@ allowlist it if you would rather not be asked:
|
|
|
308
310
|
```
|
|
309
311
|
|
|
310
312
|
`CHANGELOG.md` lists what each version added, and a release cannot skip it:
|
|
311
|
-
`tests/
|
|
313
|
+
`tests/e2e/test_cli.py` fails when `__version__` has no section (see
|
|
312
314
|
[Releasing](#releasing)).
|
|
313
315
|
|
|
314
316
|
### Keys
|
|
@@ -320,7 +322,7 @@ be rebound (see [Keybindings](#keybindings)).
|
|
|
320
322
|
| Key | Does |
|
|
321
323
|
|---|---|
|
|
322
324
|
| `enter` | send |
|
|
323
|
-
| `
|
|
325
|
+
| `shift+enter` | newline (also `ctrl+j`; in a terminal that cannot send `shift+enter`, end the line with `\` and press `enter`) |
|
|
324
326
|
| `esc` | interrupt the current turn |
|
|
325
327
|
| `ctrl+c` | copy the selected text; with nothing selected, clear the prompt (twice on an empty prompt exits) |
|
|
326
328
|
| `ctrl+d` | exit, when the prompt is empty |
|
|
@@ -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 |
|
|
@@ -622,8 +624,11 @@ merge: it is read per layer, and only from your own settings. See
|
|
|
622
624
|
Environment overrides: `HX_MODEL`, `HX_SUBAGENT_MODEL`, `HX_MAX_TOKENS`,
|
|
623
625
|
`HX_PERMISSION_MODE`, `HX_SANDBOX`, `HX_COMPACT_AT`, `HX_GIT_NOTICES`,
|
|
624
626
|
`HX_THEME`, `HX_QUIET_STARTUP`. Also `HX_HOME` to relocate user state, `HX_TAVILY_API_KEY`
|
|
625
|
-
for web search,
|
|
626
|
-
[Credentials](#credentials))
|
|
627
|
+
for web search, `HX_CA_BUNDLE` / `HX_SSL_NO_VERIFY` for TLS (see
|
|
628
|
+
[Credentials](#credentials)), and `HX_OPENROUTER_BASE_URL` to point the OpenRouter
|
|
629
|
+
route at another OpenAI-compatible endpoint - a gateway, or a recording proxy.
|
|
630
|
+
`HX_MODIFIER_PROBE=0` stops HX asking macOS whether shift is held when an
|
|
631
|
+
`enter` arrives (see [Keys](#keys)).
|
|
627
632
|
|
|
628
633
|
### The system prompt
|
|
629
634
|
|
|
@@ -661,6 +666,13 @@ if you want a cheaper model for it — and it is counted in `/cost` like any oth
|
|
|
661
666
|
If the call fails the session is still named, from your first message. `/title
|
|
662
667
|
<text>` renames it.
|
|
663
668
|
|
|
669
|
+
`/resume` lists every session except the one you are in: this directory's
|
|
670
|
+
first, then other directories', each row naming where it lives. A session from
|
|
671
|
+
this directory is swapped in place. One from elsewhere belongs to that
|
|
672
|
+
directory - its sandbox, permission rules, prompt and tools - so HX restarts
|
|
673
|
+
itself there with `hx resume <id>`, keeping options like `--mode` and
|
|
674
|
+
`--model`.
|
|
675
|
+
|
|
664
676
|
### Themes
|
|
665
677
|
|
|
666
678
|
A theme is a JSON file: a `vars` block of raw colours, and a `colors` block
|
|
@@ -725,7 +737,8 @@ confidence prompts rather than passing.
|
|
|
725
737
|
**An OS sandbox** wraps command execution: Seatbelt on macOS, bubblewrap on
|
|
726
738
|
Linux. The filesystem is readable, writes are confined to the project and the
|
|
727
739
|
temp dir, credential paths (`~/.ssh`, `~/.aws`, and HX's own `auth.json`) are
|
|
728
|
-
unreadable
|
|
740
|
+
unreadable and unwritable - even inside the project, and even before they
|
|
741
|
+
exist - and outbound network is off. If neither backend is present the
|
|
729
742
|
status bar says `no-sandbox` rather than implying protection that is not there.
|
|
730
743
|
|
|
731
744
|
Modes cycle with shift+tab: `plan` (read-only — mutating tools are not even
|
|
@@ -956,10 +969,41 @@ construction.
|
|
|
956
969
|
}
|
|
957
970
|
```
|
|
958
971
|
|
|
959
|
-
Or `hx mcp add local python server.py
|
|
972
|
+
Or `hx mcp add local python server.py`, or
|
|
973
|
+
`hx mcp add remote --url https://example.com/mcp`. Tools arrive namespaced
|
|
960
974
|
`mcp__<server>__<tool>` in a deterministic order. Servers connect concurrently
|
|
961
975
|
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.
|
|
976
|
+
dropped rather than taking the session with it. A server that changes its tool
|
|
977
|
+
list mid-session (`notifications/tools/list_changed`) has the new list swapped
|
|
978
|
+
in without a restart.
|
|
979
|
+
|
|
980
|
+
A remote server that asks for sign-in - Atlassian's, for one - is signed in to
|
|
981
|
+
over OAuth, with nothing to configure:
|
|
982
|
+
|
|
983
|
+
```json
|
|
984
|
+
{ "mcpServers": { "atlassian": { "url": "https://mcp.atlassian.com/v2/mcp" } } }
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
It shows as needing sign-in until you run `/mcp login atlassian` in a session
|
|
988
|
+
or `hx mcp login atlassian` in a shell. That opens the browser, and the tools
|
|
989
|
+
arrive as soon as you approve - no restart. HX follows the MCP authorization
|
|
990
|
+
spec: it finds the authorization server from the server's metadata, registers
|
|
991
|
+
itself as a client, signs in with PKCE, and keeps the token in `~/.hx/auth.json`
|
|
992
|
+
(mode 0600), refreshing it before it expires and again if the server rejects
|
|
993
|
+
it. Over SSH, paste the final redirect URL into the prompt instead.
|
|
994
|
+
`/mcp logout atlassian` forgets it.
|
|
995
|
+
|
|
996
|
+
A server that does not offer client registration needs a client you register
|
|
997
|
+
with it yourself, and anything else about the sign-in can be pinned the same
|
|
998
|
+
way:
|
|
999
|
+
|
|
1000
|
+
```json
|
|
1001
|
+
"oauth": { "clientId": "...", "clientSecret": "...", "scope": "read write", "callbackPort": 33418 }
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
To send a token of your own instead - an API key - give the server an
|
|
1005
|
+
`Authorization` header, and OAuth stays out of the way:
|
|
1006
|
+
`hx mcp add jira --url https://mcp.atlassian.com/v2/mcp --header "Authorization: Basic <base64 email:token>"`.
|
|
963
1007
|
|
|
964
1008
|
**Hooks** are shell commands HX runs at named points in a turn. Four events:
|
|
965
1009
|
`PreToolUse`, `PostToolUse`, `UserPromptSubmit` and `Stop`.
|
|
@@ -1037,6 +1081,23 @@ uv run mypy # strict
|
|
|
1037
1081
|
uv run hx # run from the checkout
|
|
1038
1082
|
```
|
|
1039
1083
|
|
|
1084
|
+
The suite is end-to-end. Each test in `tests/e2e/` starts the installed `hx`
|
|
1085
|
+
binary - on a real pseudo-terminal for the TUI, or as a plain subprocess for
|
|
1086
|
+
print mode and the other commands - in a private `$HOME` and `$HX_HOME`, and
|
|
1087
|
+
points it at a local OpenRouter stand-in with `HX_OPENROUTER_BASE_URL`.
|
|
1088
|
+
Everything between the keyboard and the socket is the shipped code; only the
|
|
1089
|
+
model's replies are scripted. The screen is read back through a VT emulator,
|
|
1090
|
+
so assertions are on the cells a terminal shows, not on the bytes HX meant to
|
|
1091
|
+
write.
|
|
1092
|
+
|
|
1093
|
+
Every run writes `e2e-report/`: `index.html` shows each test's outcome and
|
|
1094
|
+
every screen it checked, rendered in colour, and one text file per screen lets
|
|
1095
|
+
two runs be compared with `diff -r`. CI uploads it from every test job as an
|
|
1096
|
+
`e2e-report-*` artifact. Set `HX_E2E_REPORT` to write it somewhere else.
|
|
1097
|
+
|
|
1098
|
+
Tests that need the OS sandbox are skipped where there is no backend
|
|
1099
|
+
(`sandbox-exec` on macOS, `bwrap` on Linux).
|
|
1100
|
+
|
|
1040
1101
|
Tests marked `live` hit a real API. The OpenRouter ones cost money:
|
|
1041
1102
|
|
|
1042
1103
|
```sh
|
|
@@ -1053,17 +1114,7 @@ uv run pytest -m live tests/test_live_devin.py
|
|
|
1053
1114
|
|
|
1054
1115
|
They are the only place the wire formats, streaming, tool use and a genuine
|
|
1055
1116
|
cache hit are proven against a real service; everything else runs against the
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
Tests marked `sandbox` exercise the real OS sandbox and are skipped where no
|
|
1059
|
-
backend exists.
|
|
1060
|
-
|
|
1061
|
-
The TUI has SVG layout snapshots. Read the diff before accepting a change to
|
|
1062
|
-
them - they exist to catch a frame that quietly lost a row:
|
|
1063
|
-
|
|
1064
|
-
```sh
|
|
1065
|
-
uv run pytest tests/tui/test_snapshots.py --snapshot-update
|
|
1066
|
-
```
|
|
1117
|
+
stand-in.
|
|
1067
1118
|
|
|
1068
1119
|
### Layout
|
|
1069
1120
|
|
|
@@ -1072,7 +1123,7 @@ src/hx/
|
|
|
1072
1123
|
cli.py config.py paths.py frontmatter.py git.py
|
|
1073
1124
|
core/ loop, context assembly, compaction, late injection, sessions, usage
|
|
1074
1125
|
auth/ credential store, OAuth flows, per-route resolution
|
|
1075
|
-
providers/ OpenRouter, Codex, Devin, the model catalogue
|
|
1126
|
+
providers/ OpenRouter, Codex, Devin, the model catalogue
|
|
1076
1127
|
tools/ Bash, Read, Write, Edit, Glob, Grep, Symbols, TodoWrite, Task,
|
|
1077
1128
|
WebSearch, WebFetch, line anchors, output capping
|
|
1078
1129
|
permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
|
|
@@ -1109,12 +1160,12 @@ Each release:
|
|
|
1109
1160
|
# 1. move CHANGELOG.md's [Unreleased] entries under the new version, dated,
|
|
1110
1161
|
# and leave a fresh empty [Unreleased] behind
|
|
1111
1162
|
# 2. bump __version__ in src/hx/__init__.py
|
|
1112
|
-
# 3. uv run pytest tests/
|
|
1163
|
+
# 3. uv run pytest tests/e2e/test_cli.py # the version must have a section
|
|
1113
1164
|
git commit -am "release: X.Y.Z"
|
|
1114
1165
|
git tag v$(uv run hx --version | cut -d' ' -f2) && git push origin main --tags
|
|
1115
1166
|
```
|
|
1116
1167
|
|
|
1117
|
-
Step 1 is not optional and is not a convention: `tests/
|
|
1168
|
+
Step 1 is not optional and is not a convention: `tests/e2e/test_cli.py` fails the
|
|
1118
1169
|
whole suite when `__version__` has no `CHANGELOG.md` section, so a release that
|
|
1119
1170
|
skips it cannot get past CI to the `publish` job. The file ships inside the
|
|
1120
1171
|
wheel and `hx changelog` prints it, so a version with no section there reaches
|
|
@@ -124,5 +124,5 @@ asyncio_mode = "auto"
|
|
|
124
124
|
addopts = "-q --strict-markers -m 'not live'"
|
|
125
125
|
markers = [
|
|
126
126
|
"live: hits a real API (OpenRouter, or Tavily for the web search tools)",
|
|
127
|
-
"
|
|
127
|
+
"e2e: drives the installed hx binary against a local OpenRouter stand-in",
|
|
128
128
|
]
|