openhands 0.0.0__tar.gz → 1.0.0__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.
Potentially problematic release.
This version of openhands might be problematic. Click here for more details.
- openhands-1.0.0/.gitignore +52 -0
- openhands-1.0.0/Makefile +46 -0
- openhands-1.0.0/PKG-INFO +52 -0
- openhands-1.0.0/README.md +36 -0
- openhands-1.0.0/build.py +289 -0
- openhands-1.0.0/build.sh +48 -0
- openhands-1.0.0/hooks/rthook_profile_imports.py +68 -0
- openhands-1.0.0/openhands.spec +110 -0
- openhands-1.0.0/openhands_cli/__init__.py +3 -0
- openhands-1.0.0/openhands_cli/agent_chat.py +185 -0
- openhands-1.0.0/openhands_cli/argparsers/main_parser.py +56 -0
- openhands-1.0.0/openhands_cli/argparsers/serve_parser.py +31 -0
- openhands-1.0.0/openhands_cli/gui_launcher.py +229 -0
- openhands-1.0.0/openhands_cli/listeners/__init__.py +4 -0
- openhands-1.0.0/openhands_cli/listeners/loading_listener.py +63 -0
- openhands-1.0.0/openhands_cli/listeners/pause_listener.py +83 -0
- openhands-1.0.0/openhands_cli/llm_utils.py +57 -0
- openhands-1.0.0/openhands_cli/locations.py +13 -0
- openhands-1.0.0/openhands_cli/pt_style.py +30 -0
- openhands-1.0.0/openhands_cli/runner.py +178 -0
- openhands-1.0.0/openhands_cli/setup.py +116 -0
- openhands-1.0.0/openhands_cli/simple_main.py +59 -0
- openhands-1.0.0/openhands_cli/tui/__init__.py +5 -0
- openhands-1.0.0/openhands_cli/tui/settings/mcp_screen.py +217 -0
- openhands-1.0.0/openhands_cli/tui/settings/settings_screen.py +202 -0
- openhands-1.0.0/openhands_cli/tui/settings/store.py +93 -0
- openhands-1.0.0/openhands_cli/tui/status.py +109 -0
- openhands-1.0.0/openhands_cli/tui/tui.py +102 -0
- openhands-1.0.0/openhands_cli/tui/utils.py +14 -0
- openhands-1.0.0/openhands_cli/user_actions/__init__.py +17 -0
- openhands-1.0.0/openhands_cli/user_actions/agent_action.py +94 -0
- openhands-1.0.0/openhands_cli/user_actions/exit_session.py +18 -0
- openhands-1.0.0/openhands_cli/user_actions/settings_action.py +165 -0
- openhands-1.0.0/openhands_cli/user_actions/types.py +18 -0
- openhands-1.0.0/openhands_cli/user_actions/utils.py +199 -0
- openhands-1.0.0/pyproject.toml +100 -0
- openhands-1.0.0/tests/__init__.py +1 -0
- openhands-1.0.0/tests/commands/test_confirm_command.py +122 -0
- openhands-1.0.0/tests/commands/test_new_command.py +101 -0
- openhands-1.0.0/tests/commands/test_status_command.py +124 -0
- openhands-1.0.0/tests/conftest.py +56 -0
- openhands-1.0.0/tests/settings/test_first_time_user_settings.py +54 -0
- openhands-1.0.0/tests/settings/test_settings_input.py +140 -0
- openhands-1.0.0/tests/settings/test_settings_workflow.py +178 -0
- openhands-1.0.0/tests/test_confirmation_mode.py +484 -0
- openhands-1.0.0/tests/test_conversation_runner.py +142 -0
- openhands-1.0.0/tests/test_directory_separation.py +70 -0
- openhands-1.0.0/tests/test_exit_session_confirmation.py +107 -0
- openhands-1.0.0/tests/test_gui_launcher.py +201 -0
- openhands-1.0.0/tests/test_loading.py +69 -0
- openhands-1.0.0/tests/test_main.py +154 -0
- openhands-1.0.0/tests/test_mcp_config_validation.py +206 -0
- openhands-1.0.0/tests/test_pause_listener.py +53 -0
- openhands-1.0.0/tests/test_session_prompter.py +106 -0
- openhands-1.0.0/tests/test_tui.py +94 -0
- openhands-1.0.0/tests/utils.py +9 -0
- openhands-1.0.0/uv.lock +5655 -0
- openhands-0.0.0/PKG-INFO +0 -3
- openhands-0.0.0/README.md +0 -322
- openhands-0.0.0/openhands/__init__.py +0 -1
- openhands-0.0.0/openhands/sdk/__init__.py +0 -45
- openhands-0.0.0/openhands/sdk/agent/__init__.py +0 -8
- openhands-0.0.0/openhands/sdk/agent/agent/__init__.py +0 -6
- openhands-0.0.0/openhands/sdk/agent/agent/agent.py +0 -349
- openhands-0.0.0/openhands/sdk/agent/base.py +0 -103
- openhands-0.0.0/openhands/sdk/context/__init__.py +0 -28
- openhands-0.0.0/openhands/sdk/context/agent_context.py +0 -153
- openhands-0.0.0/openhands/sdk/context/condenser/__init__.py +0 -5
- openhands-0.0.0/openhands/sdk/context/condenser/condenser.py +0 -73
- openhands-0.0.0/openhands/sdk/context/condenser/no_op_condenser.py +0 -13
- openhands-0.0.0/openhands/sdk/context/manager.py +0 -5
- openhands-0.0.0/openhands/sdk/context/microagents/__init__.py +0 -26
- openhands-0.0.0/openhands/sdk/context/microagents/exceptions.py +0 -11
- openhands-0.0.0/openhands/sdk/context/microagents/microagent.py +0 -345
- openhands-0.0.0/openhands/sdk/context/microagents/types.py +0 -70
- openhands-0.0.0/openhands/sdk/context/utils/__init__.py +0 -8
- openhands-0.0.0/openhands/sdk/context/utils/prompt.py +0 -52
- openhands-0.0.0/openhands/sdk/context/view.py +0 -116
- openhands-0.0.0/openhands/sdk/conversation/__init__.py +0 -12
- openhands-0.0.0/openhands/sdk/conversation/conversation.py +0 -207
- openhands-0.0.0/openhands/sdk/conversation/state.py +0 -50
- openhands-0.0.0/openhands/sdk/conversation/types.py +0 -6
- openhands-0.0.0/openhands/sdk/conversation/visualizer.py +0 -300
- openhands-0.0.0/openhands/sdk/event/__init__.py +0 -27
- openhands-0.0.0/openhands/sdk/event/base.py +0 -148
- openhands-0.0.0/openhands/sdk/event/condenser.py +0 -49
- openhands-0.0.0/openhands/sdk/event/llm_convertible.py +0 -265
- openhands-0.0.0/openhands/sdk/event/types.py +0 -5
- openhands-0.0.0/openhands/sdk/event/user_action.py +0 -12
- openhands-0.0.0/openhands/sdk/event/utils.py +0 -30
- openhands-0.0.0/openhands/sdk/llm/__init__.py +0 -19
- openhands-0.0.0/openhands/sdk/llm/exceptions.py +0 -108
- openhands-0.0.0/openhands/sdk/llm/llm.py +0 -867
- openhands-0.0.0/openhands/sdk/llm/llm_registry.py +0 -116
- openhands-0.0.0/openhands/sdk/llm/message.py +0 -216
- openhands-0.0.0/openhands/sdk/llm/metadata.py +0 -34
- openhands-0.0.0/openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
- openhands-0.0.0/openhands/sdk/llm/utils/metrics.py +0 -311
- openhands-0.0.0/openhands/sdk/llm/utils/model_features.py +0 -153
- openhands-0.0.0/openhands/sdk/llm/utils/retry_mixin.py +0 -122
- openhands-0.0.0/openhands/sdk/llm/utils/telemetry.py +0 -252
- openhands-0.0.0/openhands/sdk/logger.py +0 -167
- openhands-0.0.0/openhands/sdk/mcp/__init__.py +0 -20
- openhands-0.0.0/openhands/sdk/mcp/client.py +0 -113
- openhands-0.0.0/openhands/sdk/mcp/definition.py +0 -69
- openhands-0.0.0/openhands/sdk/mcp/tool.py +0 -104
- openhands-0.0.0/openhands/sdk/mcp/utils.py +0 -59
- openhands-0.0.0/openhands/sdk/tests/llm/test_llm.py +0 -447
- openhands-0.0.0/openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
- openhands-0.0.0/openhands/sdk/tests/llm/test_model_features.py +0 -221
- openhands-0.0.0/openhands/sdk/tool/__init__.py +0 -30
- openhands-0.0.0/openhands/sdk/tool/builtins/__init__.py +0 -34
- openhands-0.0.0/openhands/sdk/tool/builtins/finish.py +0 -57
- openhands-0.0.0/openhands/sdk/tool/builtins/think.py +0 -60
- openhands-0.0.0/openhands/sdk/tool/schema.py +0 -236
- openhands-0.0.0/openhands/sdk/tool/security_prompt.py +0 -5
- openhands-0.0.0/openhands/sdk/tool/tool.py +0 -142
- openhands-0.0.0/openhands/sdk/utils/__init__.py +0 -14
- openhands-0.0.0/openhands/sdk/utils/discriminated_union.py +0 -210
- openhands-0.0.0/openhands/sdk/utils/json.py +0 -48
- openhands-0.0.0/openhands/sdk/utils/truncate.py +0 -44
- openhands-0.0.0/openhands/tools/__init__.py +0 -44
- openhands-0.0.0/openhands/tools/execute_bash/__init__.py +0 -30
- openhands-0.0.0/openhands/tools/execute_bash/constants.py +0 -31
- openhands-0.0.0/openhands/tools/execute_bash/definition.py +0 -166
- openhands-0.0.0/openhands/tools/execute_bash/impl.py +0 -38
- openhands-0.0.0/openhands/tools/execute_bash/metadata.py +0 -101
- openhands-0.0.0/openhands/tools/execute_bash/terminal/__init__.py +0 -22
- openhands-0.0.0/openhands/tools/execute_bash/terminal/factory.py +0 -113
- openhands-0.0.0/openhands/tools/execute_bash/terminal/interface.py +0 -189
- openhands-0.0.0/openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
- openhands-0.0.0/openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
- openhands-0.0.0/openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
- openhands-0.0.0/openhands/tools/execute_bash/utils/command.py +0 -150
- openhands-0.0.0/openhands/tools/str_replace_editor/__init__.py +0 -17
- openhands-0.0.0/openhands/tools/str_replace_editor/definition.py +0 -158
- openhands-0.0.0/openhands/tools/str_replace_editor/editor.py +0 -683
- openhands-0.0.0/openhands/tools/str_replace_editor/exceptions.py +0 -41
- openhands-0.0.0/openhands/tools/str_replace_editor/impl.py +0 -66
- openhands-0.0.0/openhands/tools/str_replace_editor/utils/__init__.py +0 -0
- openhands-0.0.0/openhands/tools/str_replace_editor/utils/config.py +0 -2
- openhands-0.0.0/openhands/tools/str_replace_editor/utils/constants.py +0 -9
- openhands-0.0.0/openhands/tools/str_replace_editor/utils/encoding.py +0 -135
- openhands-0.0.0/openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
- openhands-0.0.0/openhands/tools/str_replace_editor/utils/history.py +0 -122
- openhands-0.0.0/openhands/tools/str_replace_editor/utils/shell.py +0 -72
- openhands-0.0.0/openhands/tools/task_tracker/__init__.py +0 -16
- openhands-0.0.0/openhands/tools/task_tracker/definition.py +0 -336
- openhands-0.0.0/openhands/tools/utils/__init__.py +0 -1
- openhands-0.0.0/openhands.egg-info/PKG-INFO +0 -3
- openhands-0.0.0/openhands.egg-info/SOURCES.txt +0 -96
- openhands-0.0.0/openhands.egg-info/dependency_links.txt +0 -1
- openhands-0.0.0/openhands.egg-info/top_level.txt +0 -1
- openhands-0.0.0/pyproject.toml +0 -55
- openhands-0.0.0/setup.cfg +0 -4
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual Environment
|
|
24
|
+
.env
|
|
25
|
+
.venv
|
|
26
|
+
env/
|
|
27
|
+
venv/
|
|
28
|
+
ENV/
|
|
29
|
+
|
|
30
|
+
# IDE
|
|
31
|
+
.idea/
|
|
32
|
+
.vscode/
|
|
33
|
+
*.swp
|
|
34
|
+
*.swo
|
|
35
|
+
|
|
36
|
+
# Testing
|
|
37
|
+
.pytest_cache/
|
|
38
|
+
.coverage
|
|
39
|
+
htmlcov/
|
|
40
|
+
.tox/
|
|
41
|
+
.nox/
|
|
42
|
+
.coverage.*
|
|
43
|
+
coverage.xml
|
|
44
|
+
*.cover
|
|
45
|
+
.hypothesis/
|
|
46
|
+
|
|
47
|
+
# PyInstaller
|
|
48
|
+
# Usually these files are written by a python script from a template
|
|
49
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
50
|
+
*.manifest
|
|
51
|
+
# Note: We keep our custom spec file in version control
|
|
52
|
+
# *.spec
|
openhands-1.0.0/Makefile
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
.PHONY: help install install-dev test format clean run
|
|
2
|
+
|
|
3
|
+
# Default target
|
|
4
|
+
help:
|
|
5
|
+
@echo "OpenHands CLI - Available commands:"
|
|
6
|
+
@echo " install - Install the package"
|
|
7
|
+
@echo " install-dev - Install with development dependencies"
|
|
8
|
+
@echo " test - Run tests"
|
|
9
|
+
@echo " format - Format code with ruff"
|
|
10
|
+
@echo " clean - Clean build artifacts"
|
|
11
|
+
@echo " run - Run the CLI"
|
|
12
|
+
|
|
13
|
+
# Install the package
|
|
14
|
+
install:
|
|
15
|
+
uv sync
|
|
16
|
+
|
|
17
|
+
# Install with development dependencies
|
|
18
|
+
install-dev:
|
|
19
|
+
uv sync --group dev
|
|
20
|
+
|
|
21
|
+
# Run tests
|
|
22
|
+
test:
|
|
23
|
+
uv run pytest
|
|
24
|
+
|
|
25
|
+
# Format code
|
|
26
|
+
format:
|
|
27
|
+
uv run ruff format openhands_cli/
|
|
28
|
+
|
|
29
|
+
# Clean build artifacts
|
|
30
|
+
clean:
|
|
31
|
+
rm -rf .venv/
|
|
32
|
+
find . -type d -name "__pycache__" -exec rm -rf {} +
|
|
33
|
+
find . -type f -name "*.pyc" -delete
|
|
34
|
+
|
|
35
|
+
# Run the CLI
|
|
36
|
+
run:
|
|
37
|
+
uv run openhands
|
|
38
|
+
|
|
39
|
+
# Install UV if not present
|
|
40
|
+
install-uv:
|
|
41
|
+
@if ! command -v uv &> /dev/null; then \
|
|
42
|
+
echo "Installing UV..."; \
|
|
43
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh; \
|
|
44
|
+
else \
|
|
45
|
+
echo "UV is already installed"; \
|
|
46
|
+
fi
|
openhands-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openhands
|
|
3
|
+
Version: 1.0.0
|
|
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,36 @@
|
|
|
1
|
+
# OpenHands V1 CLI
|
|
2
|
+
|
|
3
|
+
A **lightweight, modern CLI** to interact with the OpenHands agent (powered by [agent-sdk](https://github.com/All-Hands-AI/agent-sdk)).
|
|
4
|
+
|
|
5
|
+
The [OpenHands V0 CLI (legacy)](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/cli) is being deprecated.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
- Prerequisites: Python 3.12+, curl
|
|
12
|
+
- Install uv (package manager):
|
|
13
|
+
```bash
|
|
14
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
15
|
+
# Restart your shell so "uv" is on PATH, or follow the installer hint
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Run the CLI locally
|
|
19
|
+
```bash
|
|
20
|
+
make install
|
|
21
|
+
|
|
22
|
+
# Start the CLI
|
|
23
|
+
make run
|
|
24
|
+
# or
|
|
25
|
+
uv run openhands
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Build a standalone executable
|
|
29
|
+
```bash
|
|
30
|
+
# Build (installs PyInstaller if needed)
|
|
31
|
+
./build.sh --install-pyinstaller
|
|
32
|
+
|
|
33
|
+
# The binary will be in dist/
|
|
34
|
+
./dist/openhands # macOS/Linux
|
|
35
|
+
# dist/openhands.exe # Windows
|
|
36
|
+
```
|
openhands-1.0.0/build.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Build script for OpenHands CLI using PyInstaller.
|
|
4
|
+
|
|
5
|
+
This script packages the OpenHands CLI into a standalone executable binary
|
|
6
|
+
using PyInstaller with the custom spec file.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import os
|
|
11
|
+
import select
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from openhands_cli.llm_utils import get_llm_metadata
|
|
19
|
+
from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR, WORK_DIR
|
|
20
|
+
|
|
21
|
+
from openhands.sdk import LLM
|
|
22
|
+
from openhands.tools.preset.default import get_default_agent
|
|
23
|
+
|
|
24
|
+
dummy_agent = get_default_agent(
|
|
25
|
+
llm=LLM(
|
|
26
|
+
model='dummy-model',
|
|
27
|
+
api_key='dummy-key',
|
|
28
|
+
metadata=get_llm_metadata(model_name='dummy-model', llm_type='openhands'),
|
|
29
|
+
),
|
|
30
|
+
cli_mode=True,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# =================================================
|
|
34
|
+
# SECTION: Build Binary
|
|
35
|
+
# =================================================
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def clean_build_directories() -> None:
|
|
39
|
+
"""Clean up previous build artifacts."""
|
|
40
|
+
print('🧹 Cleaning up previous build artifacts...')
|
|
41
|
+
|
|
42
|
+
build_dirs = ['build', 'dist', '__pycache__']
|
|
43
|
+
for dir_name in build_dirs:
|
|
44
|
+
if os.path.exists(dir_name):
|
|
45
|
+
print(f' Removing {dir_name}/')
|
|
46
|
+
shutil.rmtree(dir_name)
|
|
47
|
+
|
|
48
|
+
# Clean up .pyc files
|
|
49
|
+
for root, _dirs, files in os.walk('.'):
|
|
50
|
+
for file in files:
|
|
51
|
+
if file.endswith('.pyc'):
|
|
52
|
+
os.remove(os.path.join(root, file))
|
|
53
|
+
|
|
54
|
+
print('✅ Cleanup complete!')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def check_pyinstaller() -> bool:
|
|
58
|
+
"""Check if PyInstaller is available."""
|
|
59
|
+
try:
|
|
60
|
+
subprocess.run(
|
|
61
|
+
['uv', 'run', 'pyinstaller', '--version'], check=True, capture_output=True
|
|
62
|
+
)
|
|
63
|
+
return True
|
|
64
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
65
|
+
print(
|
|
66
|
+
'❌ PyInstaller is not available. Use --install-pyinstaller flag or install manually with:'
|
|
67
|
+
)
|
|
68
|
+
print(' uv add --dev pyinstaller')
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build_executable(
|
|
73
|
+
spec_file: str = 'openhands.spec',
|
|
74
|
+
clean: bool = True,
|
|
75
|
+
) -> bool:
|
|
76
|
+
"""Build the executable using PyInstaller."""
|
|
77
|
+
if clean:
|
|
78
|
+
clean_build_directories()
|
|
79
|
+
|
|
80
|
+
# Check if PyInstaller is available (installation is handled by build.sh)
|
|
81
|
+
if not check_pyinstaller():
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
print(f'🔨 Building executable using {spec_file}...')
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# Run PyInstaller with uv
|
|
88
|
+
cmd = ['uv', 'run', 'pyinstaller', spec_file, '--clean']
|
|
89
|
+
|
|
90
|
+
print(f'Running: {" ".join(cmd)}')
|
|
91
|
+
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
92
|
+
|
|
93
|
+
print('✅ Build completed successfully!')
|
|
94
|
+
|
|
95
|
+
# Check if the executable was created
|
|
96
|
+
dist_dir = Path('dist')
|
|
97
|
+
if dist_dir.exists():
|
|
98
|
+
executables = list(dist_dir.glob('*'))
|
|
99
|
+
if executables:
|
|
100
|
+
print('📁 Executable(s) created in dist/:')
|
|
101
|
+
for exe in executables:
|
|
102
|
+
size = exe.stat().st_size / (1024 * 1024) # Size in MB
|
|
103
|
+
print(f' - {exe.name} ({size:.1f} MB)')
|
|
104
|
+
else:
|
|
105
|
+
print('⚠️ No executables found in dist/ directory')
|
|
106
|
+
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
except subprocess.CalledProcessError as e:
|
|
110
|
+
print(f'❌ Build failed: {e}')
|
|
111
|
+
if e.stdout:
|
|
112
|
+
print('STDOUT:', e.stdout)
|
|
113
|
+
if e.stderr:
|
|
114
|
+
print('STDERR:', e.stderr)
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# =================================================
|
|
119
|
+
# SECTION: Test and profile binary
|
|
120
|
+
# =================================================
|
|
121
|
+
|
|
122
|
+
WELCOME_MARKERS = ['welcome', 'openhands cli', 'type /help', 'available commands', '>']
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _is_welcome(line: str) -> bool:
|
|
126
|
+
s = line.strip().lower()
|
|
127
|
+
return any(marker in s for marker in WELCOME_MARKERS)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_executable() -> bool:
|
|
131
|
+
"""Test the built executable, measuring boot time and total test time."""
|
|
132
|
+
print('🧪 Testing the built executable...')
|
|
133
|
+
|
|
134
|
+
spec_path = os.path.join(PERSISTENCE_DIR, AGENT_SETTINGS_PATH)
|
|
135
|
+
|
|
136
|
+
specs_path = Path(os.path.expanduser(spec_path))
|
|
137
|
+
if specs_path.exists():
|
|
138
|
+
print(f'⚠️ Using existing settings at {specs_path}')
|
|
139
|
+
else:
|
|
140
|
+
print(f'💾 Creating dummy settings at {specs_path}')
|
|
141
|
+
specs_path.parent.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
specs_path.write_text(dummy_agent.model_dump_json())
|
|
143
|
+
|
|
144
|
+
exe_path = Path('dist/openhands')
|
|
145
|
+
if not exe_path.exists():
|
|
146
|
+
exe_path = Path('dist/openhands.exe')
|
|
147
|
+
if not exe_path.exists():
|
|
148
|
+
print('❌ Executable not found!')
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
if os.name != 'nt':
|
|
153
|
+
os.chmod(exe_path, 0o755)
|
|
154
|
+
|
|
155
|
+
boot_start = time.time()
|
|
156
|
+
proc = subprocess.Popen(
|
|
157
|
+
[str(exe_path)],
|
|
158
|
+
stdin=subprocess.PIPE,
|
|
159
|
+
stdout=subprocess.PIPE,
|
|
160
|
+
stderr=subprocess.STDOUT,
|
|
161
|
+
text=True,
|
|
162
|
+
bufsize=1,
|
|
163
|
+
env={**os.environ},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# --- Wait for welcome ---
|
|
167
|
+
deadline = boot_start + 30
|
|
168
|
+
saw_welcome = False
|
|
169
|
+
captured = []
|
|
170
|
+
|
|
171
|
+
while time.time() < deadline:
|
|
172
|
+
if proc.poll() is not None:
|
|
173
|
+
break
|
|
174
|
+
rlist, _, _ = select.select([proc.stdout], [], [], 0.2)
|
|
175
|
+
if not rlist:
|
|
176
|
+
continue
|
|
177
|
+
line = proc.stdout.readline()
|
|
178
|
+
if not line:
|
|
179
|
+
continue
|
|
180
|
+
captured.append(line)
|
|
181
|
+
if _is_welcome(line):
|
|
182
|
+
saw_welcome = True
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
if not saw_welcome:
|
|
186
|
+
print('❌ Did not detect welcome prompt')
|
|
187
|
+
try:
|
|
188
|
+
proc.kill()
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
boot_end = time.time()
|
|
194
|
+
print(f'⏱️ Boot to welcome: {boot_end - boot_start:.2f} seconds')
|
|
195
|
+
|
|
196
|
+
# --- Run /help then /exit ---
|
|
197
|
+
if proc.stdin is None:
|
|
198
|
+
print('❌ stdin unavailable')
|
|
199
|
+
proc.kill()
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
proc.stdin.write('/help\n/exit\n')
|
|
203
|
+
proc.stdin.flush()
|
|
204
|
+
out, _ = proc.communicate(timeout=60)
|
|
205
|
+
|
|
206
|
+
total_end = time.time()
|
|
207
|
+
full_output = ''.join(captured) + (out or '')
|
|
208
|
+
|
|
209
|
+
print(f'⏱️ End-to-end test time: {total_end - boot_start:.2f} seconds')
|
|
210
|
+
|
|
211
|
+
if 'available commands' in full_output.lower():
|
|
212
|
+
print('✅ Executable starts, welcome detected, and /help works')
|
|
213
|
+
return True
|
|
214
|
+
else:
|
|
215
|
+
print('❌ /help output not found')
|
|
216
|
+
print('Output preview:', full_output[-500:])
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
except subprocess.TimeoutExpired:
|
|
220
|
+
print('❌ Executable test timed out')
|
|
221
|
+
try:
|
|
222
|
+
proc.kill()
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
return False
|
|
226
|
+
except Exception as e:
|
|
227
|
+
print(f'❌ Error testing executable: {e}')
|
|
228
|
+
try:
|
|
229
|
+
proc.kill()
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# =================================================
|
|
236
|
+
# SECTION: Main
|
|
237
|
+
# =================================================
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def main() -> int:
|
|
241
|
+
"""Main function."""
|
|
242
|
+
parser = argparse.ArgumentParser(description='Build OpenHands CLI executable')
|
|
243
|
+
parser.add_argument(
|
|
244
|
+
'--spec', default='openhands.spec', help='PyInstaller spec file to use'
|
|
245
|
+
)
|
|
246
|
+
parser.add_argument(
|
|
247
|
+
'--no-clean', action='store_true', help='Skip cleaning build directories'
|
|
248
|
+
)
|
|
249
|
+
parser.add_argument(
|
|
250
|
+
'--no-test', action='store_true', help='Skip testing the built executable'
|
|
251
|
+
)
|
|
252
|
+
parser.add_argument(
|
|
253
|
+
'--install-pyinstaller',
|
|
254
|
+
action='store_true',
|
|
255
|
+
help='Install PyInstaller using uv before building',
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
parser.add_argument(
|
|
259
|
+
'--no-build', action='store_true', help='Skip testing the built executable'
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
args = parser.parse_args()
|
|
263
|
+
|
|
264
|
+
print('🚀 OpenHands CLI Build Script')
|
|
265
|
+
print('=' * 40)
|
|
266
|
+
|
|
267
|
+
# Check if spec file exists
|
|
268
|
+
if not os.path.exists(args.spec):
|
|
269
|
+
print(f"❌ Spec file '{args.spec}' not found!")
|
|
270
|
+
return 1
|
|
271
|
+
|
|
272
|
+
# Build the executable
|
|
273
|
+
if not args.no_build and not build_executable(args.spec, clean=not args.no_clean):
|
|
274
|
+
return 1
|
|
275
|
+
|
|
276
|
+
# Test the executable
|
|
277
|
+
if not args.no_test:
|
|
278
|
+
if not test_executable():
|
|
279
|
+
print('❌ Executable test failed, build process failed')
|
|
280
|
+
return 1
|
|
281
|
+
|
|
282
|
+
print('\n🎉 Build process completed!')
|
|
283
|
+
print("📁 Check the 'dist/' directory for your executable")
|
|
284
|
+
|
|
285
|
+
return 0
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
if __name__ == '__main__':
|
|
289
|
+
sys.exit(main())
|
openhands-1.0.0/build.sh
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Shell script wrapper for building OpenHands CLI executable.
|
|
4
|
+
#
|
|
5
|
+
# This script provides a simple interface to build the OpenHands CLI
|
|
6
|
+
# using PyInstaller with uv package management.
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
set -e # Exit on any error
|
|
10
|
+
|
|
11
|
+
echo "🚀 OpenHands CLI Build Script"
|
|
12
|
+
echo "=============================="
|
|
13
|
+
|
|
14
|
+
# Check if uv is available
|
|
15
|
+
if ! command -v uv &> /dev/null; then
|
|
16
|
+
echo "❌ uv is required but not found! Please install uv first."
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Parse arguments to check for --install-pyinstaller
|
|
21
|
+
INSTALL_PYINSTALLER=false
|
|
22
|
+
PYTHON_ARGS=()
|
|
23
|
+
|
|
24
|
+
for arg in "$@"; do
|
|
25
|
+
case $arg in
|
|
26
|
+
--install-pyinstaller)
|
|
27
|
+
INSTALL_PYINSTALLER=true
|
|
28
|
+
PYTHON_ARGS+=("$arg")
|
|
29
|
+
;;
|
|
30
|
+
*)
|
|
31
|
+
PYTHON_ARGS+=("$arg")
|
|
32
|
+
;;
|
|
33
|
+
esac
|
|
34
|
+
done
|
|
35
|
+
|
|
36
|
+
# Install PyInstaller if requested
|
|
37
|
+
if [ "$INSTALL_PYINSTALLER" = true ]; then
|
|
38
|
+
echo "📦 Installing PyInstaller with uv..."
|
|
39
|
+
if uv add --dev pyinstaller; then
|
|
40
|
+
echo "✅ PyInstaller installed successfully with uv!"
|
|
41
|
+
else
|
|
42
|
+
echo "❌ Failed to install PyInstaller"
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Run the Python build script using uv
|
|
48
|
+
uv run python build.py "${PYTHON_ARGS[@]}"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
ENABLE = os.getenv('IMPORT_PROFILING', '0') not in ('', '0', 'false', 'False')
|
|
8
|
+
OUT = 'dist/import_profiler.csv'
|
|
9
|
+
THRESHOLD_MS = float(os.getenv('IMPORT_PROFILING_THRESHOLD_MS', '0'))
|
|
10
|
+
|
|
11
|
+
if ENABLE:
|
|
12
|
+
timings = defaultdict(float) # module -> total seconds (first load only)
|
|
13
|
+
counts = defaultdict(int) # module -> number of first-loads (should be 1)
|
|
14
|
+
max_dur = defaultdict(float) # module -> max single load seconds
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import importlib._bootstrap as _bootstrap # type: ignore[attr-defined]
|
|
18
|
+
except Exception:
|
|
19
|
+
_bootstrap = None
|
|
20
|
+
|
|
21
|
+
start_time = time.perf_counter()
|
|
22
|
+
|
|
23
|
+
if _bootstrap is not None:
|
|
24
|
+
_orig_find_and_load = _bootstrap._find_and_load
|
|
25
|
+
|
|
26
|
+
def _timed_find_and_load(name, import_):
|
|
27
|
+
preloaded = name in sys.modules # cache hit?
|
|
28
|
+
t0 = time.perf_counter()
|
|
29
|
+
try:
|
|
30
|
+
return _orig_find_and_load(name, import_)
|
|
31
|
+
finally:
|
|
32
|
+
if not preloaded:
|
|
33
|
+
dt = time.perf_counter() - t0
|
|
34
|
+
timings[name] += dt
|
|
35
|
+
counts[name] += 1
|
|
36
|
+
if dt > max_dur[name]:
|
|
37
|
+
max_dur[name] = dt
|
|
38
|
+
|
|
39
|
+
_bootstrap._find_and_load = _timed_find_and_load
|
|
40
|
+
|
|
41
|
+
@atexit.register
|
|
42
|
+
def _dump_import_profile():
|
|
43
|
+
def ms(s):
|
|
44
|
+
return f'{s * 1000:.3f}'
|
|
45
|
+
|
|
46
|
+
items = [
|
|
47
|
+
(name, counts[name], timings[name], max_dur[name])
|
|
48
|
+
for name in timings
|
|
49
|
+
if timings[name] * 1000 >= THRESHOLD_MS
|
|
50
|
+
]
|
|
51
|
+
items.sort(key=lambda x: x[2], reverse=True)
|
|
52
|
+
try:
|
|
53
|
+
with open(OUT, 'w', encoding='utf-8') as f:
|
|
54
|
+
f.write('module,count,total_ms,max_ms\n')
|
|
55
|
+
for name, cnt, tot_s, max_s in items:
|
|
56
|
+
f.write(f'{name},{cnt},{ms(tot_s)},{ms(max_s)}\n')
|
|
57
|
+
# brief summary
|
|
58
|
+
if items:
|
|
59
|
+
w = max(len(n) for n, *_ in items[:25])
|
|
60
|
+
sys.stderr.write('\n=== Import Time Profile (first-load only) ===\n')
|
|
61
|
+
sys.stderr.write(f'{"module".ljust(w)} count total_ms max_ms\n')
|
|
62
|
+
for name, cnt, tot_s, max_s in items[:25]:
|
|
63
|
+
sys.stderr.write(
|
|
64
|
+
f'{name.ljust(w)} {str(cnt).rjust(5)} {ms(tot_s).rjust(8)} {ms(max_s).rjust(7)}\n'
|
|
65
|
+
)
|
|
66
|
+
sys.stderr.write(f'\nImport profile written to: {OUT}\n')
|
|
67
|
+
except Exception as e:
|
|
68
|
+
sys.stderr.write(f'[import-profiler] failed to write profile: {e}\n')
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# -*- mode: python ; coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
PyInstaller spec file for OpenHands CLI.
|
|
4
|
+
|
|
5
|
+
This spec file configures PyInstaller to create a standalone executable
|
|
6
|
+
for the OpenHands CLI application.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from PyInstaller.utils.hooks import (
|
|
13
|
+
collect_submodules,
|
|
14
|
+
collect_data_files,
|
|
15
|
+
copy_metadata
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Get the project root directory (current working directory when running PyInstaller)
|
|
21
|
+
project_root = Path.cwd()
|
|
22
|
+
|
|
23
|
+
a = Analysis(
|
|
24
|
+
['openhands_cli/simple_main.py'],
|
|
25
|
+
pathex=[str(project_root)],
|
|
26
|
+
binaries=[],
|
|
27
|
+
datas=[
|
|
28
|
+
# Include any data files that might be needed
|
|
29
|
+
# Add more data files here if needed in the future
|
|
30
|
+
*collect_data_files('tiktoken'),
|
|
31
|
+
*collect_data_files('tiktoken_ext'),
|
|
32
|
+
*collect_data_files('litellm'),
|
|
33
|
+
*collect_data_files('fastmcp'),
|
|
34
|
+
*collect_data_files('mcp'),
|
|
35
|
+
# Include Jinja prompt templates required by the agent SDK
|
|
36
|
+
*collect_data_files('openhands.sdk.agent', includes=['prompts/*.j2']),
|
|
37
|
+
# Include package metadata for importlib.metadata
|
|
38
|
+
*copy_metadata('fastmcp'),
|
|
39
|
+
],
|
|
40
|
+
hiddenimports=[
|
|
41
|
+
# Explicitly include modules that might not be detected automatically
|
|
42
|
+
*collect_submodules('openhands_cli'),
|
|
43
|
+
*collect_submodules('prompt_toolkit'),
|
|
44
|
+
# Include OpenHands SDK submodules explicitly to avoid resolution issues
|
|
45
|
+
*collect_submodules('openhands.sdk'),
|
|
46
|
+
*collect_submodules('openhands.tools'),
|
|
47
|
+
*collect_submodules('tiktoken'),
|
|
48
|
+
*collect_submodules('tiktoken_ext'),
|
|
49
|
+
*collect_submodules('litellm'),
|
|
50
|
+
*collect_submodules('fastmcp'),
|
|
51
|
+
# Include mcp but exclude CLI parts that require typer
|
|
52
|
+
'mcp.types',
|
|
53
|
+
'mcp.client',
|
|
54
|
+
'mcp.server',
|
|
55
|
+
'mcp.shared',
|
|
56
|
+
'openhands.tools.execute_bash',
|
|
57
|
+
'openhands.tools.str_replace_editor',
|
|
58
|
+
'openhands.tools.task_tracker',
|
|
59
|
+
],
|
|
60
|
+
hookspath=[],
|
|
61
|
+
hooksconfig={},
|
|
62
|
+
runtime_hooks=[],
|
|
63
|
+
# runtime_hooks=[str(project_root / "hooks" / "rthook_profile_imports.py")],
|
|
64
|
+
excludes=[
|
|
65
|
+
# Exclude unnecessary modules to reduce binary size
|
|
66
|
+
'tkinter',
|
|
67
|
+
'matplotlib',
|
|
68
|
+
'numpy',
|
|
69
|
+
'scipy',
|
|
70
|
+
'pandas',
|
|
71
|
+
'IPython',
|
|
72
|
+
'jupyter',
|
|
73
|
+
'notebook',
|
|
74
|
+
# Exclude mcp CLI parts that cause issues
|
|
75
|
+
'mcp.cli',
|
|
76
|
+
'prompt_toolkit.contrib.ssh',
|
|
77
|
+
'fastmcp.cli',
|
|
78
|
+
'boto3',
|
|
79
|
+
'botocore',
|
|
80
|
+
'posthog',
|
|
81
|
+
'browser-use',
|
|
82
|
+
'openhands.tools.browser_use'
|
|
83
|
+
],
|
|
84
|
+
noarchive=False,
|
|
85
|
+
# IMPORTANT: do not use optimize=2 (-OO) because it strips docstrings used by PLY/bashlex grammar
|
|
86
|
+
optimize=0,
|
|
87
|
+
)
|
|
88
|
+
pyz = PYZ(a.pure)
|
|
89
|
+
|
|
90
|
+
exe = EXE(
|
|
91
|
+
pyz,
|
|
92
|
+
a.scripts,
|
|
93
|
+
a.binaries,
|
|
94
|
+
a.datas,
|
|
95
|
+
[],
|
|
96
|
+
name='openhands',
|
|
97
|
+
debug=False,
|
|
98
|
+
bootloader_ignore_signals=False,
|
|
99
|
+
strip=True, # Strip debug symbols to reduce size
|
|
100
|
+
upx=True, # Use UPX compression if available
|
|
101
|
+
upx_exclude=[],
|
|
102
|
+
runtime_tmpdir=None,
|
|
103
|
+
console=True, # CLI application needs console
|
|
104
|
+
disable_windowed_traceback=False,
|
|
105
|
+
argv_emulation=False,
|
|
106
|
+
target_arch=None,
|
|
107
|
+
codesign_identity=None,
|
|
108
|
+
entitlements_file=None,
|
|
109
|
+
icon=None, # Add icon path here if you have one
|
|
110
|
+
)
|