openhands 0.0.0__tar.gz → 1.0.1__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.

Files changed (156) hide show
  1. openhands-1.0.1/.gitignore +52 -0
  2. openhands-1.0.1/Makefile +46 -0
  3. openhands-1.0.1/PKG-INFO +52 -0
  4. openhands-1.0.1/README.md +36 -0
  5. openhands-1.0.1/build.py +289 -0
  6. openhands-1.0.1/build.sh +48 -0
  7. openhands-1.0.1/hooks/rthook_profile_imports.py +68 -0
  8. openhands-1.0.1/openhands.spec +110 -0
  9. openhands-1.0.1/openhands_cli/__init__.py +8 -0
  10. openhands-1.0.1/openhands_cli/agent_chat.py +186 -0
  11. openhands-1.0.1/openhands_cli/argparsers/main_parser.py +56 -0
  12. openhands-1.0.1/openhands_cli/argparsers/serve_parser.py +31 -0
  13. openhands-1.0.1/openhands_cli/gui_launcher.py +220 -0
  14. openhands-1.0.1/openhands_cli/listeners/__init__.py +4 -0
  15. openhands-1.0.1/openhands_cli/listeners/loading_listener.py +63 -0
  16. openhands-1.0.1/openhands_cli/listeners/pause_listener.py +83 -0
  17. openhands-1.0.1/openhands_cli/llm_utils.py +57 -0
  18. openhands-1.0.1/openhands_cli/locations.py +13 -0
  19. openhands-1.0.1/openhands_cli/pt_style.py +30 -0
  20. openhands-1.0.1/openhands_cli/runner.py +178 -0
  21. openhands-1.0.1/openhands_cli/setup.py +116 -0
  22. openhands-1.0.1/openhands_cli/simple_main.py +59 -0
  23. openhands-1.0.1/openhands_cli/tui/__init__.py +5 -0
  24. openhands-1.0.1/openhands_cli/tui/settings/mcp_screen.py +217 -0
  25. openhands-1.0.1/openhands_cli/tui/settings/settings_screen.py +202 -0
  26. openhands-1.0.1/openhands_cli/tui/settings/store.py +93 -0
  27. openhands-1.0.1/openhands_cli/tui/status.py +109 -0
  28. openhands-1.0.1/openhands_cli/tui/tui.py +100 -0
  29. openhands-1.0.1/openhands_cli/tui/utils.py +14 -0
  30. openhands-1.0.1/openhands_cli/user_actions/__init__.py +17 -0
  31. openhands-1.0.1/openhands_cli/user_actions/agent_action.py +95 -0
  32. openhands-1.0.1/openhands_cli/user_actions/exit_session.py +18 -0
  33. openhands-1.0.1/openhands_cli/user_actions/settings_action.py +171 -0
  34. openhands-1.0.1/openhands_cli/user_actions/types.py +18 -0
  35. openhands-1.0.1/openhands_cli/user_actions/utils.py +199 -0
  36. openhands-1.0.1/pyproject.toml +99 -0
  37. openhands-1.0.1/tests/__init__.py +1 -0
  38. openhands-1.0.1/tests/commands/test_confirm_command.py +122 -0
  39. openhands-1.0.1/tests/commands/test_new_command.py +101 -0
  40. openhands-1.0.1/tests/commands/test_status_command.py +124 -0
  41. openhands-1.0.1/tests/conftest.py +56 -0
  42. openhands-1.0.1/tests/settings/test_api_key_preservation.py +56 -0
  43. openhands-1.0.1/tests/settings/test_first_time_user_settings.py +54 -0
  44. openhands-1.0.1/tests/settings/test_settings_input.py +140 -0
  45. openhands-1.0.1/tests/settings/test_settings_workflow.py +178 -0
  46. openhands-1.0.1/tests/test_confirmation_mode.py +484 -0
  47. openhands-1.0.1/tests/test_conversation_runner.py +142 -0
  48. openhands-1.0.1/tests/test_directory_separation.py +70 -0
  49. openhands-1.0.1/tests/test_exit_session_confirmation.py +107 -0
  50. openhands-1.0.1/tests/test_gui_launcher.py +199 -0
  51. openhands-1.0.1/tests/test_loading.py +69 -0
  52. openhands-1.0.1/tests/test_main.py +154 -0
  53. openhands-1.0.1/tests/test_mcp_config_validation.py +206 -0
  54. openhands-1.0.1/tests/test_pause_listener.py +53 -0
  55. openhands-1.0.1/tests/test_session_prompter.py +106 -0
  56. openhands-1.0.1/tests/test_tui.py +94 -0
  57. openhands-1.0.1/tests/utils.py +9 -0
  58. openhands-1.0.1/uv.lock +5666 -0
  59. openhands-0.0.0/PKG-INFO +0 -3
  60. openhands-0.0.0/README.md +0 -322
  61. openhands-0.0.0/openhands/__init__.py +0 -1
  62. openhands-0.0.0/openhands/sdk/__init__.py +0 -45
  63. openhands-0.0.0/openhands/sdk/agent/__init__.py +0 -8
  64. openhands-0.0.0/openhands/sdk/agent/agent/__init__.py +0 -6
  65. openhands-0.0.0/openhands/sdk/agent/agent/agent.py +0 -349
  66. openhands-0.0.0/openhands/sdk/agent/base.py +0 -103
  67. openhands-0.0.0/openhands/sdk/context/__init__.py +0 -28
  68. openhands-0.0.0/openhands/sdk/context/agent_context.py +0 -153
  69. openhands-0.0.0/openhands/sdk/context/condenser/__init__.py +0 -5
  70. openhands-0.0.0/openhands/sdk/context/condenser/condenser.py +0 -73
  71. openhands-0.0.0/openhands/sdk/context/condenser/no_op_condenser.py +0 -13
  72. openhands-0.0.0/openhands/sdk/context/manager.py +0 -5
  73. openhands-0.0.0/openhands/sdk/context/microagents/__init__.py +0 -26
  74. openhands-0.0.0/openhands/sdk/context/microagents/exceptions.py +0 -11
  75. openhands-0.0.0/openhands/sdk/context/microagents/microagent.py +0 -345
  76. openhands-0.0.0/openhands/sdk/context/microagents/types.py +0 -70
  77. openhands-0.0.0/openhands/sdk/context/utils/__init__.py +0 -8
  78. openhands-0.0.0/openhands/sdk/context/utils/prompt.py +0 -52
  79. openhands-0.0.0/openhands/sdk/context/view.py +0 -116
  80. openhands-0.0.0/openhands/sdk/conversation/__init__.py +0 -12
  81. openhands-0.0.0/openhands/sdk/conversation/conversation.py +0 -207
  82. openhands-0.0.0/openhands/sdk/conversation/state.py +0 -50
  83. openhands-0.0.0/openhands/sdk/conversation/types.py +0 -6
  84. openhands-0.0.0/openhands/sdk/conversation/visualizer.py +0 -300
  85. openhands-0.0.0/openhands/sdk/event/__init__.py +0 -27
  86. openhands-0.0.0/openhands/sdk/event/base.py +0 -148
  87. openhands-0.0.0/openhands/sdk/event/condenser.py +0 -49
  88. openhands-0.0.0/openhands/sdk/event/llm_convertible.py +0 -265
  89. openhands-0.0.0/openhands/sdk/event/types.py +0 -5
  90. openhands-0.0.0/openhands/sdk/event/user_action.py +0 -12
  91. openhands-0.0.0/openhands/sdk/event/utils.py +0 -30
  92. openhands-0.0.0/openhands/sdk/llm/__init__.py +0 -19
  93. openhands-0.0.0/openhands/sdk/llm/exceptions.py +0 -108
  94. openhands-0.0.0/openhands/sdk/llm/llm.py +0 -867
  95. openhands-0.0.0/openhands/sdk/llm/llm_registry.py +0 -116
  96. openhands-0.0.0/openhands/sdk/llm/message.py +0 -216
  97. openhands-0.0.0/openhands/sdk/llm/metadata.py +0 -34
  98. openhands-0.0.0/openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
  99. openhands-0.0.0/openhands/sdk/llm/utils/metrics.py +0 -311
  100. openhands-0.0.0/openhands/sdk/llm/utils/model_features.py +0 -153
  101. openhands-0.0.0/openhands/sdk/llm/utils/retry_mixin.py +0 -122
  102. openhands-0.0.0/openhands/sdk/llm/utils/telemetry.py +0 -252
  103. openhands-0.0.0/openhands/sdk/logger.py +0 -167
  104. openhands-0.0.0/openhands/sdk/mcp/__init__.py +0 -20
  105. openhands-0.0.0/openhands/sdk/mcp/client.py +0 -113
  106. openhands-0.0.0/openhands/sdk/mcp/definition.py +0 -69
  107. openhands-0.0.0/openhands/sdk/mcp/tool.py +0 -104
  108. openhands-0.0.0/openhands/sdk/mcp/utils.py +0 -59
  109. openhands-0.0.0/openhands/sdk/tests/llm/test_llm.py +0 -447
  110. openhands-0.0.0/openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
  111. openhands-0.0.0/openhands/sdk/tests/llm/test_model_features.py +0 -221
  112. openhands-0.0.0/openhands/sdk/tool/__init__.py +0 -30
  113. openhands-0.0.0/openhands/sdk/tool/builtins/__init__.py +0 -34
  114. openhands-0.0.0/openhands/sdk/tool/builtins/finish.py +0 -57
  115. openhands-0.0.0/openhands/sdk/tool/builtins/think.py +0 -60
  116. openhands-0.0.0/openhands/sdk/tool/schema.py +0 -236
  117. openhands-0.0.0/openhands/sdk/tool/security_prompt.py +0 -5
  118. openhands-0.0.0/openhands/sdk/tool/tool.py +0 -142
  119. openhands-0.0.0/openhands/sdk/utils/__init__.py +0 -14
  120. openhands-0.0.0/openhands/sdk/utils/discriminated_union.py +0 -210
  121. openhands-0.0.0/openhands/sdk/utils/json.py +0 -48
  122. openhands-0.0.0/openhands/sdk/utils/truncate.py +0 -44
  123. openhands-0.0.0/openhands/tools/__init__.py +0 -44
  124. openhands-0.0.0/openhands/tools/execute_bash/__init__.py +0 -30
  125. openhands-0.0.0/openhands/tools/execute_bash/constants.py +0 -31
  126. openhands-0.0.0/openhands/tools/execute_bash/definition.py +0 -166
  127. openhands-0.0.0/openhands/tools/execute_bash/impl.py +0 -38
  128. openhands-0.0.0/openhands/tools/execute_bash/metadata.py +0 -101
  129. openhands-0.0.0/openhands/tools/execute_bash/terminal/__init__.py +0 -22
  130. openhands-0.0.0/openhands/tools/execute_bash/terminal/factory.py +0 -113
  131. openhands-0.0.0/openhands/tools/execute_bash/terminal/interface.py +0 -189
  132. openhands-0.0.0/openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
  133. openhands-0.0.0/openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
  134. openhands-0.0.0/openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
  135. openhands-0.0.0/openhands/tools/execute_bash/utils/command.py +0 -150
  136. openhands-0.0.0/openhands/tools/str_replace_editor/__init__.py +0 -17
  137. openhands-0.0.0/openhands/tools/str_replace_editor/definition.py +0 -158
  138. openhands-0.0.0/openhands/tools/str_replace_editor/editor.py +0 -683
  139. openhands-0.0.0/openhands/tools/str_replace_editor/exceptions.py +0 -41
  140. openhands-0.0.0/openhands/tools/str_replace_editor/impl.py +0 -66
  141. openhands-0.0.0/openhands/tools/str_replace_editor/utils/__init__.py +0 -0
  142. openhands-0.0.0/openhands/tools/str_replace_editor/utils/config.py +0 -2
  143. openhands-0.0.0/openhands/tools/str_replace_editor/utils/constants.py +0 -9
  144. openhands-0.0.0/openhands/tools/str_replace_editor/utils/encoding.py +0 -135
  145. openhands-0.0.0/openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
  146. openhands-0.0.0/openhands/tools/str_replace_editor/utils/history.py +0 -122
  147. openhands-0.0.0/openhands/tools/str_replace_editor/utils/shell.py +0 -72
  148. openhands-0.0.0/openhands/tools/task_tracker/__init__.py +0 -16
  149. openhands-0.0.0/openhands/tools/task_tracker/definition.py +0 -336
  150. openhands-0.0.0/openhands/tools/utils/__init__.py +0 -1
  151. openhands-0.0.0/openhands.egg-info/PKG-INFO +0 -3
  152. openhands-0.0.0/openhands.egg-info/SOURCES.txt +0 -96
  153. openhands-0.0.0/openhands.egg-info/dependency_links.txt +0 -1
  154. openhands-0.0.0/openhands.egg-info/top_level.txt +0 -1
  155. openhands-0.0.0/pyproject.toml +0 -55
  156. 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
@@ -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
@@ -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,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
+ ```
@@ -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 + 60
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())
@@ -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
+ )