openhands 0.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
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.
Potentially problematic release.
This version of openhands might be problematic. Click here for more details.
- openhands-1.0.1.dist-info/METADATA +52 -0
- openhands-1.0.1.dist-info/RECORD +31 -0
- {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
- openhands-1.0.1.dist-info/entry_points.txt +2 -0
- openhands_cli/__init__.py +8 -0
- openhands_cli/agent_chat.py +186 -0
- openhands_cli/argparsers/main_parser.py +56 -0
- openhands_cli/argparsers/serve_parser.py +31 -0
- openhands_cli/gui_launcher.py +220 -0
- openhands_cli/listeners/__init__.py +4 -0
- openhands_cli/listeners/loading_listener.py +63 -0
- openhands_cli/listeners/pause_listener.py +83 -0
- openhands_cli/llm_utils.py +57 -0
- openhands_cli/locations.py +13 -0
- openhands_cli/pt_style.py +30 -0
- openhands_cli/runner.py +178 -0
- openhands_cli/setup.py +116 -0
- openhands_cli/simple_main.py +59 -0
- openhands_cli/tui/__init__.py +5 -0
- openhands_cli/tui/settings/mcp_screen.py +217 -0
- openhands_cli/tui/settings/settings_screen.py +202 -0
- openhands_cli/tui/settings/store.py +93 -0
- openhands_cli/tui/status.py +109 -0
- openhands_cli/tui/tui.py +100 -0
- openhands_cli/tui/utils.py +14 -0
- openhands_cli/user_actions/__init__.py +17 -0
- openhands_cli/user_actions/agent_action.py +95 -0
- openhands_cli/user_actions/exit_session.py +18 -0
- openhands_cli/user_actions/settings_action.py +171 -0
- openhands_cli/user_actions/types.py +18 -0
- openhands_cli/user_actions/utils.py +199 -0
- openhands/__init__.py +0 -1
- openhands/sdk/__init__.py +0 -45
- openhands/sdk/agent/__init__.py +0 -8
- openhands/sdk/agent/agent/__init__.py +0 -6
- openhands/sdk/agent/agent/agent.py +0 -349
- openhands/sdk/agent/base.py +0 -103
- openhands/sdk/context/__init__.py +0 -28
- openhands/sdk/context/agent_context.py +0 -153
- openhands/sdk/context/condenser/__init__.py +0 -5
- openhands/sdk/context/condenser/condenser.py +0 -73
- openhands/sdk/context/condenser/no_op_condenser.py +0 -13
- openhands/sdk/context/manager.py +0 -5
- openhands/sdk/context/microagents/__init__.py +0 -26
- openhands/sdk/context/microagents/exceptions.py +0 -11
- openhands/sdk/context/microagents/microagent.py +0 -345
- openhands/sdk/context/microagents/types.py +0 -70
- openhands/sdk/context/utils/__init__.py +0 -8
- openhands/sdk/context/utils/prompt.py +0 -52
- openhands/sdk/context/view.py +0 -116
- openhands/sdk/conversation/__init__.py +0 -12
- openhands/sdk/conversation/conversation.py +0 -207
- openhands/sdk/conversation/state.py +0 -50
- openhands/sdk/conversation/types.py +0 -6
- openhands/sdk/conversation/visualizer.py +0 -300
- openhands/sdk/event/__init__.py +0 -27
- openhands/sdk/event/base.py +0 -148
- openhands/sdk/event/condenser.py +0 -49
- openhands/sdk/event/llm_convertible.py +0 -265
- openhands/sdk/event/types.py +0 -5
- openhands/sdk/event/user_action.py +0 -12
- openhands/sdk/event/utils.py +0 -30
- openhands/sdk/llm/__init__.py +0 -19
- openhands/sdk/llm/exceptions.py +0 -108
- openhands/sdk/llm/llm.py +0 -867
- openhands/sdk/llm/llm_registry.py +0 -116
- openhands/sdk/llm/message.py +0 -216
- openhands/sdk/llm/metadata.py +0 -34
- openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
- openhands/sdk/llm/utils/metrics.py +0 -311
- openhands/sdk/llm/utils/model_features.py +0 -153
- openhands/sdk/llm/utils/retry_mixin.py +0 -122
- openhands/sdk/llm/utils/telemetry.py +0 -252
- openhands/sdk/logger.py +0 -167
- openhands/sdk/mcp/__init__.py +0 -20
- openhands/sdk/mcp/client.py +0 -113
- openhands/sdk/mcp/definition.py +0 -69
- openhands/sdk/mcp/tool.py +0 -104
- openhands/sdk/mcp/utils.py +0 -59
- openhands/sdk/tests/llm/test_llm.py +0 -447
- openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
- openhands/sdk/tests/llm/test_model_features.py +0 -221
- openhands/sdk/tool/__init__.py +0 -30
- openhands/sdk/tool/builtins/__init__.py +0 -34
- openhands/sdk/tool/builtins/finish.py +0 -57
- openhands/sdk/tool/builtins/think.py +0 -60
- openhands/sdk/tool/schema.py +0 -236
- openhands/sdk/tool/security_prompt.py +0 -5
- openhands/sdk/tool/tool.py +0 -142
- openhands/sdk/utils/__init__.py +0 -14
- openhands/sdk/utils/discriminated_union.py +0 -210
- openhands/sdk/utils/json.py +0 -48
- openhands/sdk/utils/truncate.py +0 -44
- openhands/tools/__init__.py +0 -44
- openhands/tools/execute_bash/__init__.py +0 -30
- openhands/tools/execute_bash/constants.py +0 -31
- openhands/tools/execute_bash/definition.py +0 -166
- openhands/tools/execute_bash/impl.py +0 -38
- openhands/tools/execute_bash/metadata.py +0 -101
- openhands/tools/execute_bash/terminal/__init__.py +0 -22
- openhands/tools/execute_bash/terminal/factory.py +0 -113
- openhands/tools/execute_bash/terminal/interface.py +0 -189
- openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
- openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
- openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
- openhands/tools/execute_bash/utils/command.py +0 -150
- openhands/tools/str_replace_editor/__init__.py +0 -17
- openhands/tools/str_replace_editor/definition.py +0 -158
- openhands/tools/str_replace_editor/editor.py +0 -683
- openhands/tools/str_replace_editor/exceptions.py +0 -41
- openhands/tools/str_replace_editor/impl.py +0 -66
- openhands/tools/str_replace_editor/utils/__init__.py +0 -0
- openhands/tools/str_replace_editor/utils/config.py +0 -2
- openhands/tools/str_replace_editor/utils/constants.py +0 -9
- openhands/tools/str_replace_editor/utils/encoding.py +0 -135
- openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
- openhands/tools/str_replace_editor/utils/history.py +0 -122
- openhands/tools/str_replace_editor/utils/shell.py +0 -72
- openhands/tools/task_tracker/__init__.py +0 -16
- openhands/tools/task_tracker/definition.py +0 -336
- openhands/tools/utils/__init__.py +0 -1
- openhands-0.0.0.dist-info/METADATA +0 -3
- openhands-0.0.0.dist-info/RECORD +0 -94
- openhands-0.0.0.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openhands
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: OpenHands CLI - Terminal User Interface for OpenHands AI Agent
|
|
5
|
+
Author-email: OpenHands Team <contact@all-hands.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Requires-Dist: openhands-sdk
|
|
12
|
+
Requires-Dist: openhands-tools
|
|
13
|
+
Requires-Dist: prompt-toolkit>=3
|
|
14
|
+
Requires-Dist: typer>=0.17.4
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# OpenHands V1 CLI
|
|
18
|
+
|
|
19
|
+
A **lightweight, modern CLI** to interact with the OpenHands agent (powered by [agent-sdk](https://github.com/All-Hands-AI/agent-sdk)).
|
|
20
|
+
|
|
21
|
+
The [OpenHands V0 CLI (legacy)](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/cli) is being deprecated.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
- Prerequisites: Python 3.12+, curl
|
|
28
|
+
- Install uv (package manager):
|
|
29
|
+
```bash
|
|
30
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
31
|
+
# Restart your shell so "uv" is on PATH, or follow the installer hint
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Run the CLI locally
|
|
35
|
+
```bash
|
|
36
|
+
make install
|
|
37
|
+
|
|
38
|
+
# Start the CLI
|
|
39
|
+
make run
|
|
40
|
+
# or
|
|
41
|
+
uv run openhands
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Build a standalone executable
|
|
45
|
+
```bash
|
|
46
|
+
# Build (installs PyInstaller if needed)
|
|
47
|
+
./build.sh --install-pyinstaller
|
|
48
|
+
|
|
49
|
+
# The binary will be in dist/
|
|
50
|
+
./dist/openhands # macOS/Linux
|
|
51
|
+
# dist/openhands.exe # Windows
|
|
52
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
openhands_cli/__init__.py,sha256=AqdjE-Up0uBSDyHvNjF0zWHNmA4ps-lsQWFpUWCWNEc,187
|
|
2
|
+
openhands_cli/agent_chat.py,sha256=FZ1NC31xJPXqyFGUtS-JqDI_HVP5155xEz59AHp3DIw,6224
|
|
3
|
+
openhands_cli/gui_launcher.py,sha256=EnZGcZMQzOHLwkmiHcYvt4WX2HfK0aT6L0t5ADWEfT0,7198
|
|
4
|
+
openhands_cli/llm_utils.py,sha256=eRhAjysm77GvzArKb-75agqTlR_4zzxTrYD1eQbgqbg,1606
|
|
5
|
+
openhands_cli/locations.py,sha256=zG5OnpJiX0kQ1fprEsOSi_cNqydEvxunPBCTpNrVaJ0,438
|
|
6
|
+
openhands_cli/pt_style.py,sha256=UxCxV-t88n5JjjYYMD0jf5apg4B5-WkfQ4EX_Eusj1I,1266
|
|
7
|
+
openhands_cli/runner.py,sha256=Oc6WEaJ0S-zfsPf30U4ozWrnqquvD2bpwX3AUnStpfs,6157
|
|
8
|
+
openhands_cli/setup.py,sha256=HWb2_6N6qgXvhIzh7jLecS7HA5EEcp7b3VroDxGiGz0,3799
|
|
9
|
+
openhands_cli/simple_main.py,sha256=7MmX8DgSvlFURzYsNqcE2vQ2MtdYTr-V88JlBmJlWvo,1697
|
|
10
|
+
openhands_cli/argparsers/main_parser.py,sha256=ceYNnktzp4xMwrsvQZl0Py6uG3atUgBsPFfqM7Cig9k,1677
|
|
11
|
+
openhands_cli/argparsers/serve_parser.py,sha256=WU2ClG5idbs966zJ5GfBXiaMH50e0ryGC1vD24JyJJ8,900
|
|
12
|
+
openhands_cli/listeners/__init__.py,sha256=T251L6mUEQdfk1x7-AWITlDjgVDwfW8XkyKQYK6Z5O0,180
|
|
13
|
+
openhands_cli/listeners/loading_listener.py,sha256=9sbuy64iAPqgvNojS-3RlWvepmaMC3R6FqlhTdYFKv8,1995
|
|
14
|
+
openhands_cli/listeners/pause_listener.py,sha256=FtWxj8T5r2lYIoquueeCf-3kF3zFMJ2e25wqOeF7QtM,2622
|
|
15
|
+
openhands_cli/tui/__init__.py,sha256=2KA7nugQxpIj4sEVDJavBbGHqcnySJFSvbPu1WRonPg,84
|
|
16
|
+
openhands_cli/tui/status.py,sha256=QNbS9OEK6d_c0P1eEQKfytuUF1R7uPxFl5nN-yv3cdA,3628
|
|
17
|
+
openhands_cli/tui/tui.py,sha256=JBahocaunF0LwJJLkcCmwSZN9xydb3SmH7fb8Jw8rFc,3545
|
|
18
|
+
openhands_cli/tui/utils.py,sha256=t7vJHU_AcLzuLTPku88Aqd51jyWY8HwH5Y2n-Bpa-WU,536
|
|
19
|
+
openhands_cli/tui/settings/mcp_screen.py,sha256=LMdcGD6DfwZQ4jnp-KM4gYiPXfNGHHPAqtEAXLKZtmA,8215
|
|
20
|
+
openhands_cli/tui/settings/settings_screen.py,sha256=CRekM2ZgWJ_YgzC-Jg1SgX2YeviU69Hb1cuzi5pVdio,7087
|
|
21
|
+
openhands_cli/tui/settings/store.py,sha256=tTMvYbdu9gs4GDNBoHFGFJnwm1PfjShgsVoMDjpQJ2Y,3485
|
|
22
|
+
openhands_cli/user_actions/__init__.py,sha256=Tf-lQj7-6bNl5rohSdcAFApkKbQb-N9JcKZgHa1HF60,501
|
|
23
|
+
openhands_cli/user_actions/agent_action.py,sha256=0v6kQPh8iE4CGjARA8vVD_TqSpS-aiscE3ucLC7jlZA,3283
|
|
24
|
+
openhands_cli/user_actions/exit_session.py,sha256=3u1CqWeYAEYfZi9BFh_dUwvy9aXXuvFbsZUHNGjVzuY,624
|
|
25
|
+
openhands_cli/user_actions/settings_action.py,sha256=QqWj37CAlrkV6FW9SEmmwsvKzdGyUYv4DlGjUweKsmg,5707
|
|
26
|
+
openhands_cli/user_actions/types.py,sha256=zMHoJqxmtbxAIq6O3RC-XoGzgbZzWZvRFNMOGW4MViQ,407
|
|
27
|
+
openhands_cli/user_actions/utils.py,sha256=qPTgP_1shnWgA7itrzTyfEGb_meXO-zq98JzFMUqCx4,6362
|
|
28
|
+
openhands-1.0.1.dist-info/METADATA,sha256=GV83vM7e_TD4avAskm3v-ZbwzJ_1EZ-A9jEAOzEF94I,1370
|
|
29
|
+
openhands-1.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
30
|
+
openhands-1.0.1.dist-info/entry_points.txt,sha256=CltyhTfx4lPndNPQXrZZDj5LmXrYXykVyIqoedPXsas,61
|
|
31
|
+
openhands-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Agent chat functionality for OpenHands CLI.
|
|
4
|
+
Provides a conversation interface with an AI agent using OpenHands patterns.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
from openhands.sdk import (
|
|
11
|
+
Message,
|
|
12
|
+
TextContent,
|
|
13
|
+
)
|
|
14
|
+
from openhands.sdk.conversation.state import AgentExecutionStatus
|
|
15
|
+
from prompt_toolkit import print_formatted_text
|
|
16
|
+
from prompt_toolkit.formatted_text import HTML
|
|
17
|
+
|
|
18
|
+
from openhands_cli.runner import ConversationRunner
|
|
19
|
+
from openhands_cli.setup import MissingAgentSpec, setup_conversation, start_fresh_conversation
|
|
20
|
+
from openhands_cli.tui.settings.mcp_screen import MCPScreen
|
|
21
|
+
from openhands_cli.tui.settings.settings_screen import SettingsScreen
|
|
22
|
+
from openhands_cli.tui.status import display_status
|
|
23
|
+
from openhands_cli.tui.tui import (
|
|
24
|
+
display_help,
|
|
25
|
+
display_welcome,
|
|
26
|
+
)
|
|
27
|
+
from openhands_cli.user_actions import UserConfirmation, exit_session_confirmation
|
|
28
|
+
from openhands_cli.user_actions.utils import get_session_prompter
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _restore_tty() -> None:
|
|
32
|
+
"""
|
|
33
|
+
Ensure terminal modes are reset in case prompt_toolkit cleanup didn't run.
|
|
34
|
+
- Turn off application cursor keys (DECCKM): ESC[?1l
|
|
35
|
+
- Turn off bracketed paste: ESC[?2004l
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
sys.stdout.write('\x1b[?1l\x1b[?2004l')
|
|
39
|
+
sys.stdout.flush()
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _print_exit_hint(conversation_id: str) -> None:
|
|
45
|
+
"""Print a resume hint with the current conversation ID."""
|
|
46
|
+
print_formatted_text(
|
|
47
|
+
HTML(f'<grey>Conversation ID:</grey> <yellow>{conversation_id}</yellow>')
|
|
48
|
+
)
|
|
49
|
+
print_formatted_text(
|
|
50
|
+
HTML(
|
|
51
|
+
f'<grey>Hint:</grey> run <gold>openhands --resume {conversation_id}</gold> '
|
|
52
|
+
'to resume this conversation.'
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
|
59
|
+
"""Run the agent chat session using the agent SDK.
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
AgentSetupError: If agent setup fails
|
|
64
|
+
KeyboardInterrupt: If user interrupts the session
|
|
65
|
+
EOFError: If EOF is encountered
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
conversation = start_fresh_conversation(resume_conversation_id)
|
|
70
|
+
except MissingAgentSpec:
|
|
71
|
+
print_formatted_text(HTML('\n<yellow>Setup is required to use OpenHands CLI.</yellow>'))
|
|
72
|
+
print_formatted_text(HTML('\n<yellow>Goodbye! 👋</yellow>'))
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
display_welcome(conversation.id, bool(resume_conversation_id))
|
|
77
|
+
|
|
78
|
+
# Track session start time for uptime calculation
|
|
79
|
+
session_start_time = datetime.now()
|
|
80
|
+
|
|
81
|
+
# Create conversation runner to handle state machine logic
|
|
82
|
+
runner = ConversationRunner(conversation)
|
|
83
|
+
session = get_session_prompter()
|
|
84
|
+
|
|
85
|
+
# Main chat loop
|
|
86
|
+
while True:
|
|
87
|
+
try:
|
|
88
|
+
# Get user input
|
|
89
|
+
user_input = session.prompt(
|
|
90
|
+
HTML('<gold>> </gold>'),
|
|
91
|
+
multiline=False,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if not user_input.strip():
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
# Handle commands
|
|
98
|
+
command = user_input.strip().lower()
|
|
99
|
+
|
|
100
|
+
message = Message(
|
|
101
|
+
role='user',
|
|
102
|
+
content=[TextContent(text=user_input)],
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if command == '/exit':
|
|
106
|
+
exit_confirmation = exit_session_confirmation()
|
|
107
|
+
if exit_confirmation == UserConfirmation.ACCEPT:
|
|
108
|
+
print_formatted_text(HTML('\n<yellow>Goodbye! 👋</yellow>'))
|
|
109
|
+
_print_exit_hint(conversation.id)
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
elif command == '/settings':
|
|
113
|
+
settings_screen = SettingsScreen(conversation)
|
|
114
|
+
settings_screen.display_settings()
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
elif command == '/mcp':
|
|
118
|
+
mcp_screen = MCPScreen()
|
|
119
|
+
mcp_screen.display_mcp_info(conversation.agent)
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
elif command == '/clear':
|
|
123
|
+
display_welcome(conversation.id)
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
elif command == '/new':
|
|
127
|
+
try:
|
|
128
|
+
# Start a fresh conversation (no resume ID = new conversation)
|
|
129
|
+
conversation = setup_conversation()
|
|
130
|
+
runner = ConversationRunner(conversation)
|
|
131
|
+
display_welcome(conversation.id, resume=False)
|
|
132
|
+
print_formatted_text(
|
|
133
|
+
HTML('<green>✓ Started fresh conversation</green>')
|
|
134
|
+
)
|
|
135
|
+
continue
|
|
136
|
+
except Exception as e:
|
|
137
|
+
print_formatted_text(
|
|
138
|
+
HTML(f'<red>Error starting fresh conversation: {e}</red>')
|
|
139
|
+
)
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
elif command == '/help':
|
|
143
|
+
display_help()
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
elif command == '/status':
|
|
147
|
+
display_status(conversation, session_start_time=session_start_time)
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
elif command == '/confirm':
|
|
151
|
+
runner.toggle_confirmation_mode()
|
|
152
|
+
new_status = (
|
|
153
|
+
'enabled' if runner.is_confirmation_mode_active else 'disabled'
|
|
154
|
+
)
|
|
155
|
+
print_formatted_text(
|
|
156
|
+
HTML(f'<yellow>Confirmation mode {new_status}</yellow>')
|
|
157
|
+
)
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
elif command == '/resume':
|
|
161
|
+
if not (
|
|
162
|
+
conversation.state.agent_status == AgentExecutionStatus.PAUSED
|
|
163
|
+
or conversation.state.agent_status
|
|
164
|
+
== AgentExecutionStatus.WAITING_FOR_CONFIRMATION
|
|
165
|
+
):
|
|
166
|
+
print_formatted_text(
|
|
167
|
+
HTML('<red>No paused conversation to resume...</red>')
|
|
168
|
+
)
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# Resume without new message
|
|
172
|
+
message = None
|
|
173
|
+
|
|
174
|
+
runner.process_message(message)
|
|
175
|
+
|
|
176
|
+
print() # Add spacing
|
|
177
|
+
|
|
178
|
+
except KeyboardInterrupt:
|
|
179
|
+
exit_confirmation = exit_session_confirmation()
|
|
180
|
+
if exit_confirmation == UserConfirmation.ACCEPT:
|
|
181
|
+
print_formatted_text(HTML('\n<yellow>Goodbye! 👋</yellow>'))
|
|
182
|
+
_print_exit_hint(conversation.id)
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
# Clean up terminal state
|
|
186
|
+
_restore_tty()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Main argument parser for OpenHands CLI."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_main_parser() -> argparse.ArgumentParser:
|
|
7
|
+
"""Create the main argument parser with CLI as default and serve as subcommand.
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
The configured argument parser
|
|
11
|
+
"""
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
description='OpenHands CLI - Terminal User Interface for OpenHands AI Agent',
|
|
14
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
15
|
+
epilog="""
|
|
16
|
+
By default, OpenHands runs in CLI mode (terminal interface).
|
|
17
|
+
Use 'serve' subcommand to launch the GUI server instead.
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
openhands # Start CLI mode
|
|
21
|
+
openhands --resume conversation-id # Resume a conversation in CLI mode
|
|
22
|
+
openhands serve # Launch GUI server
|
|
23
|
+
openhands serve --gpu # Launch GUI server with GPU support
|
|
24
|
+
"""
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# CLI arguments at top level (default mode)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
'--resume',
|
|
30
|
+
type=str,
|
|
31
|
+
help='Conversation ID to resume'
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Only serve as subcommand
|
|
35
|
+
subparsers = parser.add_subparsers(
|
|
36
|
+
dest='command',
|
|
37
|
+
help='Additional commands'
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Add serve subcommand
|
|
41
|
+
serve_parser = subparsers.add_parser(
|
|
42
|
+
'serve',
|
|
43
|
+
help='Launch the OpenHands GUI server using Docker (web interface)'
|
|
44
|
+
)
|
|
45
|
+
serve_parser.add_argument(
|
|
46
|
+
'--mount-cwd',
|
|
47
|
+
action='store_true',
|
|
48
|
+
help='Mount the current working directory in the Docker container'
|
|
49
|
+
)
|
|
50
|
+
serve_parser.add_argument(
|
|
51
|
+
'--gpu',
|
|
52
|
+
action='store_true',
|
|
53
|
+
help='Enable GPU support in the Docker container'
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return parser
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Argument parser for serve subcommand."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def add_serve_parser(subparsers: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
|
7
|
+
"""Add serve subcommand parser.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
subparsers: The subparsers object to add the serve parser to
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
The serve argument parser
|
|
14
|
+
"""
|
|
15
|
+
serve_parser = subparsers.add_parser(
|
|
16
|
+
'serve',
|
|
17
|
+
help='Launch the OpenHands GUI server using Docker (web interface)'
|
|
18
|
+
)
|
|
19
|
+
serve_parser.add_argument(
|
|
20
|
+
'--mount-cwd',
|
|
21
|
+
help='Mount the current working directory into the GUI server container',
|
|
22
|
+
action='store_true',
|
|
23
|
+
default=False,
|
|
24
|
+
)
|
|
25
|
+
serve_parser.add_argument(
|
|
26
|
+
'--gpu',
|
|
27
|
+
help='Enable GPU support by mounting all GPUs into the Docker container via nvidia-docker',
|
|
28
|
+
action='store_true',
|
|
29
|
+
default=False,
|
|
30
|
+
)
|
|
31
|
+
return serve_parser
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""GUI launcher for OpenHands CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from prompt_toolkit import print_formatted_text
|
|
10
|
+
from prompt_toolkit.formatted_text import HTML
|
|
11
|
+
from openhands_cli.locations import PERSISTENCE_DIR
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _format_docker_command_for_logging(cmd: list[str]) -> str:
|
|
15
|
+
"""Format a Docker command for logging with grey color.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
cmd (list[str]): The Docker command as a list of strings
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
str: The formatted command string in grey HTML color
|
|
22
|
+
"""
|
|
23
|
+
cmd_str = ' '.join(cmd)
|
|
24
|
+
return f'<grey>Running Docker command: {cmd_str}</grey>'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_docker_requirements() -> bool:
|
|
28
|
+
"""Check if Docker is installed and running.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
bool: True if Docker is available and running, False otherwise.
|
|
32
|
+
"""
|
|
33
|
+
# Check if Docker is installed
|
|
34
|
+
if not shutil.which('docker'):
|
|
35
|
+
print_formatted_text(
|
|
36
|
+
HTML('<ansired>❌ Docker is not installed or not in PATH.</ansired>')
|
|
37
|
+
)
|
|
38
|
+
print_formatted_text(
|
|
39
|
+
HTML(
|
|
40
|
+
'<grey>Please install Docker first: https://docs.docker.com/get-docker/</grey>'
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
# Check if Docker daemon is running
|
|
46
|
+
try:
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
['docker', 'info'], capture_output=True, text=True, timeout=10
|
|
49
|
+
)
|
|
50
|
+
if result.returncode != 0:
|
|
51
|
+
print_formatted_text(
|
|
52
|
+
HTML('<ansired>❌ Docker daemon is not running.</ansired>')
|
|
53
|
+
)
|
|
54
|
+
print_formatted_text(
|
|
55
|
+
HTML('<grey>Please start Docker and try again.</grey>')
|
|
56
|
+
)
|
|
57
|
+
return False
|
|
58
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e:
|
|
59
|
+
print_formatted_text(
|
|
60
|
+
HTML('<ansired>❌ Failed to check Docker status.</ansired>')
|
|
61
|
+
)
|
|
62
|
+
print_formatted_text(HTML(f'<grey>Error: {e}</grey>'))
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def ensure_config_dir_exists() -> Path:
|
|
69
|
+
"""Ensure the OpenHands configuration directory exists and return its path."""
|
|
70
|
+
path = Path(PERSISTENCE_DIR)
|
|
71
|
+
path.mkdir(exist_ok=True, parents=True)
|
|
72
|
+
return path
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_openhands_version() -> str:
|
|
76
|
+
"""Get the OpenHands version for Docker images.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
str: The version string to use for Docker images
|
|
80
|
+
"""
|
|
81
|
+
# For now, use 'latest' as the default version
|
|
82
|
+
# In the future, this could be read from a version file or environment variable
|
|
83
|
+
return os.environ.get('OPENHANDS_VERSION', 'latest')
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def launch_gui_server(mount_cwd: bool = False, gpu: bool = False) -> None:
|
|
87
|
+
"""Launch the OpenHands GUI server using Docker.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
mount_cwd: If True, mount the current working directory into the container.
|
|
91
|
+
gpu: If True, enable GPU support by mounting all GPUs into the container via nvidia-docker.
|
|
92
|
+
"""
|
|
93
|
+
print_formatted_text(
|
|
94
|
+
HTML('<ansiblue>🚀 Launching OpenHands GUI server...</ansiblue>')
|
|
95
|
+
)
|
|
96
|
+
print_formatted_text('')
|
|
97
|
+
|
|
98
|
+
# Check Docker requirements
|
|
99
|
+
if not check_docker_requirements():
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
|
|
102
|
+
# Ensure config directory exists
|
|
103
|
+
config_dir = ensure_config_dir_exists()
|
|
104
|
+
|
|
105
|
+
# Get the current version for the Docker image
|
|
106
|
+
version = get_openhands_version()
|
|
107
|
+
runtime_image = f'docker.all-hands.dev/all-hands-ai/runtime:{version}-nikolaik'
|
|
108
|
+
app_image = f'docker.all-hands.dev/all-hands-ai/openhands:{version}'
|
|
109
|
+
|
|
110
|
+
print_formatted_text(HTML('<grey>Pulling required Docker images...</grey>'))
|
|
111
|
+
|
|
112
|
+
# Pull the runtime image first
|
|
113
|
+
pull_cmd = ['docker', 'pull', runtime_image]
|
|
114
|
+
print_formatted_text(HTML(_format_docker_command_for_logging(pull_cmd)))
|
|
115
|
+
try:
|
|
116
|
+
subprocess.run(pull_cmd, check=True)
|
|
117
|
+
except subprocess.CalledProcessError:
|
|
118
|
+
print_formatted_text(
|
|
119
|
+
HTML('<ansired>❌ Failed to pull runtime image.</ansired>')
|
|
120
|
+
)
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
|
|
123
|
+
print_formatted_text('')
|
|
124
|
+
print_formatted_text(
|
|
125
|
+
HTML('<ansigreen>✅ Starting OpenHands GUI server...</ansigreen>')
|
|
126
|
+
)
|
|
127
|
+
print_formatted_text(
|
|
128
|
+
HTML('<grey>The server will be available at: http://localhost:3000</grey>')
|
|
129
|
+
)
|
|
130
|
+
print_formatted_text(HTML('<grey>Press Ctrl+C to stop the server.</grey>'))
|
|
131
|
+
print_formatted_text('')
|
|
132
|
+
|
|
133
|
+
# Build the Docker command
|
|
134
|
+
docker_cmd = [
|
|
135
|
+
'docker',
|
|
136
|
+
'run',
|
|
137
|
+
'-it',
|
|
138
|
+
'--rm',
|
|
139
|
+
'--pull=always',
|
|
140
|
+
'-e',
|
|
141
|
+
f'SANDBOX_RUNTIME_CONTAINER_IMAGE={runtime_image}',
|
|
142
|
+
'-e',
|
|
143
|
+
'LOG_ALL_EVENTS=true',
|
|
144
|
+
'-v',
|
|
145
|
+
'/var/run/docker.sock:/var/run/docker.sock',
|
|
146
|
+
'-v',
|
|
147
|
+
f'{config_dir}:/.openhands',
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
# Add GPU support if requested
|
|
151
|
+
if gpu:
|
|
152
|
+
print_formatted_text(
|
|
153
|
+
HTML('<ansigreen>🖥️ Enabling GPU support via nvidia-docker...</ansigreen>')
|
|
154
|
+
)
|
|
155
|
+
# Add the --gpus all flag to enable all GPUs
|
|
156
|
+
docker_cmd.insert(2, '--gpus')
|
|
157
|
+
docker_cmd.insert(3, 'all')
|
|
158
|
+
# Add environment variable to pass GPU support to sandbox containers
|
|
159
|
+
docker_cmd.extend(
|
|
160
|
+
[
|
|
161
|
+
'-e',
|
|
162
|
+
'SANDBOX_ENABLE_GPU=true',
|
|
163
|
+
]
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Add current working directory mount if requested
|
|
167
|
+
if mount_cwd:
|
|
168
|
+
cwd = Path.cwd()
|
|
169
|
+
# Following the documentation at https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem
|
|
170
|
+
docker_cmd.extend(
|
|
171
|
+
[
|
|
172
|
+
'-e',
|
|
173
|
+
f'SANDBOX_VOLUMES={cwd}:/workspace:rw',
|
|
174
|
+
]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Set user ID for Unix-like systems only
|
|
178
|
+
if os.name != 'nt': # Not Windows
|
|
179
|
+
try:
|
|
180
|
+
user_id = subprocess.check_output(['id', '-u'], text=True).strip()
|
|
181
|
+
docker_cmd.extend(['-e', f'SANDBOX_USER_ID={user_id}'])
|
|
182
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
183
|
+
# If 'id' command fails or doesn't exist, skip setting user ID
|
|
184
|
+
pass
|
|
185
|
+
# Print the folder that will be mounted to inform the user
|
|
186
|
+
print_formatted_text(
|
|
187
|
+
HTML(
|
|
188
|
+
f'<ansigreen>📂 Mounting current directory:</ansigreen> <ansiyellow>{cwd}</ansiyellow> <ansigreen>to</ansigreen> <ansiyellow>/workspace</ansiyellow>'
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
docker_cmd.extend(
|
|
193
|
+
[
|
|
194
|
+
'-p',
|
|
195
|
+
'3000:3000',
|
|
196
|
+
'--add-host',
|
|
197
|
+
'host.docker.internal:host-gateway',
|
|
198
|
+
'--name',
|
|
199
|
+
'openhands-app',
|
|
200
|
+
app_image,
|
|
201
|
+
]
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
# Log and run the Docker command
|
|
206
|
+
print_formatted_text(HTML(_format_docker_command_for_logging(docker_cmd)))
|
|
207
|
+
subprocess.run(docker_cmd, check=True)
|
|
208
|
+
except subprocess.CalledProcessError as e:
|
|
209
|
+
print_formatted_text('')
|
|
210
|
+
print_formatted_text(
|
|
211
|
+
HTML('<ansired>❌ Failed to start OpenHands GUI server.</ansired>')
|
|
212
|
+
)
|
|
213
|
+
print_formatted_text(HTML(f'<grey>Error: {e}</grey>'))
|
|
214
|
+
sys.exit(1)
|
|
215
|
+
except KeyboardInterrupt:
|
|
216
|
+
print_formatted_text('')
|
|
217
|
+
print_formatted_text(
|
|
218
|
+
HTML('<ansigreen>✓ OpenHands GUI server stopped successfully.</ansigreen>')
|
|
219
|
+
)
|
|
220
|
+
sys.exit(0)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Loading animation utilities for OpenHands CLI.
|
|
3
|
+
Provides animated loading screens during agent initialization.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def display_initialization_animation(text: str, is_loaded: threading.Event) -> None:
|
|
12
|
+
"""Display a spinning animation while agent is being initialized.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
text: The text to display alongside the animation
|
|
16
|
+
is_loaded: Threading event that signals when loading is complete
|
|
17
|
+
"""
|
|
18
|
+
ANIMATION_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
19
|
+
|
|
20
|
+
i = 0
|
|
21
|
+
while not is_loaded.is_set():
|
|
22
|
+
sys.stdout.write('\n')
|
|
23
|
+
sys.stdout.write(
|
|
24
|
+
f'\033[s\033[J\033[38;2;255;215;0m[{ANIMATION_FRAMES[i % len(ANIMATION_FRAMES)]}] {text}\033[0m\033[u\033[1A'
|
|
25
|
+
)
|
|
26
|
+
sys.stdout.flush()
|
|
27
|
+
time.sleep(0.1)
|
|
28
|
+
i += 1
|
|
29
|
+
|
|
30
|
+
sys.stdout.write('\r' + ' ' * (len(text) + 10) + '\r')
|
|
31
|
+
sys.stdout.flush()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LoadingContext:
|
|
35
|
+
"""Context manager for displaying loading animations in a separate thread."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, text: str):
|
|
38
|
+
"""Initialize the loading context.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
text: The text to display during loading
|
|
42
|
+
"""
|
|
43
|
+
self.text = text
|
|
44
|
+
self.is_loaded = threading.Event()
|
|
45
|
+
self.loading_thread: threading.Thread | None = None
|
|
46
|
+
|
|
47
|
+
def __enter__(self) -> 'LoadingContext':
|
|
48
|
+
"""Start the loading animation in a separate thread."""
|
|
49
|
+
self.loading_thread = threading.Thread(
|
|
50
|
+
target=display_initialization_animation,
|
|
51
|
+
args=(self.text, self.is_loaded),
|
|
52
|
+
daemon=True,
|
|
53
|
+
)
|
|
54
|
+
self.loading_thread.start()
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
58
|
+
"""Stop the loading animation and clean up the thread."""
|
|
59
|
+
self.is_loaded.set()
|
|
60
|
+
if self.loading_thread:
|
|
61
|
+
self.loading_thread.join(
|
|
62
|
+
timeout=1.0
|
|
63
|
+
) # Wait up to 1 second for thread to finish
|