Skip to content

Commit dc2dc7d

Browse files
authored
Merge branch 'main' into cli-user-skill
2 parents e858e1e + 6289967 commit dc2dc7d

36 files changed

+4205
-59
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,4 @@ $RECYCLE.BIN/
193193
*.log
194194
.coverage
195195
.pytest_cache/
196+
software-agent-sdk/

Makefile

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,36 @@
1-
.PHONY: help install install-dev test format clean run
1+
SHELL := /usr/bin/env bash
2+
.SHELLFLAGS := -eu -o pipefail -c
3+
4+
# Colors for output
5+
ECHO := printf '%b\n'
6+
GREEN := \033[32m
7+
YELLOW := \033[33m
8+
RED := \033[31m
9+
CYAN := \033[36m
10+
RESET := \033[0m
11+
12+
.PHONY: help install install-dev test format clean run check-uv-version build
13+
14+
check-uv-version:
15+
@$(ECHO) "$(YELLOW)Checking uv version...$(RESET)"
16+
@UV_VERSION=$$(uv --version | cut -d' ' -f2); \
17+
REQUIRED_VERSION=$(REQUIRED_UV_VERSION); \
18+
if [ "$$(printf '%s\n' "$$REQUIRED_VERSION" "$$UV_VERSION" | sort -V | head -n1)" != "$$REQUIRED_VERSION" ]; then \
19+
$(ECHO) "$(RED)Error: uv version $$UV_VERSION is less than required $$REQUIRED_VERSION$(RESET)"; \
20+
$(ECHO) "$(YELLOW)Please update uv with: uv self update$(RESET)"; \
21+
exit 1; \
22+
fi; \
23+
$(ECHO) "$(GREEN)uv version $$UV_VERSION meets requirements$(RESET)"
24+
25+
build: check-uv-version
26+
@$(ECHO) "$(CYAN)Setting up OpenHands V1 development environment...$(RESET)"
27+
@$(ECHO) "$(YELLOW)Installing dependencies with uv sync --dev...$(RESET)"
28+
@uv sync --dev
29+
@$(ECHO) "$(GREEN)Dependencies installed successfully.$(RESET)"
30+
@$(ECHO) "$(YELLOW)Setting up pre-commit hooks...$(RESET)"
31+
@uv run pre-commit install
32+
@$(ECHO) "$(GREEN)Pre-commit hooks installed successfully.$(RESET)"
33+
@$(ECHO) "$(GREEN)Build complete! Development environment is ready.$(RESET)"
234

335
# Default target
436
help:

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
# OpenHands V1 CLI
22

3-
A **lightweight, modern CLI** to interact with the OpenHands agent (powered by [OpenHands software-agent-sdk](https://github.com/OpenHands/software-agent-sdk)).
3+
A **lightweight, modern CLI** to interact with the OpenHands agent (powered by [OpenHands Software Agent SDK](https://github.com/OpenHands/software-agent-sdk)).
44

55
---
66

77
## Quickstart
88

99
- Prerequisites: Python 3.12+, curl
1010
- Install uv (package manager):
11+
1112
```bash
1213
curl -LsSf https://astral.sh/uv/install.sh | sh
1314
# Restart your shell so "uv" is on PATH, or follow the installer hint
1415
```
1516

1617
### Run the CLI locally
18+
1719
```bash
1820
make install
1921

@@ -24,6 +26,7 @@ uv run openhands
2426
```
2527

2628
### Build a standalone executable
29+
2730
```bash
2831
# Build (installs PyInstaller if needed)
2932
./build.sh --install-pyinstaller

build.py

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import argparse
1010
import os
11+
import re
1112
import select
1213
import shutil
1314
import subprocess
@@ -233,6 +234,129 @@ def test_executable(dummy_agent) -> bool:
233234
return False
234235

235236

237+
def test_version() -> bool:
238+
"""Test the --version flag of the built executable."""
239+
print("🧪 Testing binary --version flag...")
240+
241+
# Find the binary executable
242+
exe_path = Path("dist/openhands")
243+
if not exe_path.exists():
244+
exe_path = Path("dist/openhands.exe")
245+
if not exe_path.exists():
246+
print("❌ Binary executable not found!")
247+
return False
248+
249+
try:
250+
# Make binary executable on Unix-like systems
251+
if os.name != "nt":
252+
os.chmod(exe_path, 0o755)
253+
254+
# Run --version and capture output
255+
print(f"Running: {exe_path} --version")
256+
result = subprocess.run(
257+
[str(exe_path), "--version"],
258+
capture_output=True,
259+
text=True,
260+
timeout=30,
261+
)
262+
263+
if result.returncode != 0:
264+
print("❌ Failed to run binary --version command!")
265+
print(f"Exit code: {result.returncode}")
266+
print(f"Output: {result.stdout}")
267+
print(f"Error: {result.stderr}")
268+
return False
269+
270+
version_output = result.stdout + result.stderr
271+
print(f"Version output: {version_output}")
272+
273+
# Check if output contains "OpenHands CLI"
274+
if "OpenHands CLI" not in version_output:
275+
print("❌ Version output does not contain 'OpenHands CLI'!")
276+
print(f"Output: {version_output}")
277+
return False
278+
279+
# Check if output contains a valid version number (X.Y.Z format)
280+
if not re.search(r"\d+\.\d+\.\d+", version_output):
281+
print("❌ Version output does not contain a valid version number!")
282+
print(f"Output: {version_output}")
283+
return False
284+
285+
print("✅ Binary --version test passed!")
286+
return True
287+
288+
except subprocess.TimeoutExpired:
289+
print("❌ Binary --version test timed out")
290+
return False
291+
except Exception as e:
292+
print(f"❌ Error testing binary --version: {e}")
293+
return False
294+
295+
296+
def test_acp_executable() -> bool:
297+
"""Test the ACP server in the built executable with JSON-RPC messages."""
298+
print("🧪 Testing ACP server in the built executable...")
299+
300+
# Import test utilities
301+
from openhands_cli.acp_impl.test_utils import test_jsonrpc_messages
302+
303+
exe_path = Path("dist/openhands")
304+
if not exe_path.exists():
305+
exe_path = Path("dist/openhands.exe")
306+
if not exe_path.exists():
307+
print("❌ Executable not found!")
308+
return False
309+
310+
if os.name != "nt":
311+
os.chmod(exe_path, 0o755)
312+
313+
# JSON-RPC messages to test
314+
test_messages = [
315+
{
316+
"jsonrpc": "2.0",
317+
"id": 1,
318+
"method": "initialize",
319+
"params": {
320+
"protocolVersion": 1,
321+
"clientCapabilities": {
322+
"fs": {"readTextFile": True, "writeTextFile": True},
323+
"terminal": True,
324+
"_meta": {"terminal_output": True, "terminal-auth": True},
325+
},
326+
"clientInfo": {"name": "zed", "title": "Zed", "version": "0.212.7"},
327+
},
328+
},
329+
{
330+
"jsonrpc": "2.0",
331+
"id": 2,
332+
"method": "session/new",
333+
"params": {
334+
"cwd": "/tmp",
335+
"mcpServers": [],
336+
},
337+
},
338+
]
339+
340+
# Run the test
341+
success, responses = test_jsonrpc_messages(
342+
str(exe_path),
343+
["acp"],
344+
test_messages,
345+
timeout_per_message=15.0, # Increased timeout for CI environments
346+
verbose=True,
347+
)
348+
349+
# Print summary
350+
print(f"\n{'=' * 60}")
351+
print("ACP Test Summary:")
352+
print(f" Messages sent: {len(test_messages)}")
353+
print(f" Responses received: {len(responses)}")
354+
print(f" Test result: {'✅ PASSED' if success else '❌ FAILED'}")
355+
print(f"{'=' * 60}")
356+
357+
return success
358+
359+
236360
# =================================================
237361
# SECTION: Main
238362
# =================================================
@@ -257,7 +381,7 @@ def main() -> int:
257381
)
258382

259383
parser.add_argument(
260-
"--no-build", action="store_true", help="Skip testing the built executable"
384+
"--no-build", action="store_true", help="Skip building the executable"
261385
)
262386

263387
args = parser.parse_args()
@@ -276,6 +400,12 @@ def main() -> int:
276400

277401
# Test the executable
278402
if not args.no_test:
403+
# First test --version flag
404+
if not test_version():
405+
print("❌ Binary --version test failed, build process failed")
406+
return 1
407+
408+
# Then test full executable with agent
279409
model_name = "dummy-model"
280410
extra_kwargs: dict[str, Any] = {}
281411
if should_set_litellm_extra_body(model_name):
@@ -290,6 +420,11 @@ def main() -> int:
290420
print("❌ Executable test failed, build process failed")
291421
return 1
292422

423+
print("\n" + "=" * 60)
424+
if not test_acp_executable():
425+
print("❌ ACP test failed, build process failed")
426+
return 1
427+
293428
print("\n🎉 Build process completed!")
294429
print("📁 Check the 'dist/' directory for your executable")
295430

hooks/rthook_profile_imports.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
start_time = time.perf_counter()
2323

2424
if _bootstrap is not None:
25-
_orig_find_and_load = _bootstrap._find_and_loadn # type: ignore
25+
_orig_find_and_load = _bootstrap._find_and_load # type: ignore
2626

2727
def _timed_find_and_load(name, import_):
2828
preloaded = name in sys.modules # cache hit?

openhands-cli.spec

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ a = Analysis(
3636
*collect_data_files('openhands.sdk'),
3737
# Include package metadata for importlib.metadata
3838
*copy_metadata('fastmcp'),
39+
*copy_metadata('agent-client-protocol'),
3940
],
4041
hiddenimports=[
4142
# Explicitly include modules that might not be detected automatically
@@ -48,6 +49,8 @@ a = Analysis(
4849
*collect_submodules('tiktoken_ext'),
4950
*collect_submodules('litellm'),
5051
*collect_submodules('fastmcp'),
52+
# Include Agent Client Protocol (ACP) for 'openhands acp' command
53+
*collect_submodules('acp'),
5154
# Include mcp but exclude CLI parts that require typer
5255
'mcp.types',
5356
'mcp.client',

openhands_cli/acp_impl/README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# ACP Implementation
2+
3+
## What is the Agent Client Protocol (ACP)?
4+
5+
The [Agent Client Protocol (ACP)](https://agentclientprotocol.com/protocol/overview) is a standardized communication protocol that enables code editors and IDEs to interact with AI agents. ACP defines how clients (like code editors) and agents (like OpenHands) communicate through a JSON-RPC 2.0 interface.
6+
7+
For more details about the protocol, see the [ACP documentation](https://agentclientprotocol.com/protocol/overview).
8+
9+
## Development Guide
10+
11+
### Setup with Zed IDE
12+
13+
Follow the documentation at [OpenHands ACP Guide](https://docs.openhands.dev/openhands/usage/run-openhands/acp#zed-ide) for general setup instructions.
14+
15+
#### Option 1: Test with PR Branch
16+
17+
Add this agent configuration to test with the PR branch:
18+
19+
```json
20+
"OpenHands-uvx": {
21+
"command": "uvx",
22+
"args": [
23+
"--from",
24+
"git+https://github.com/OpenHands/OpenHands-CLI.git@xw/acp-simplification",
25+
"openhands",
26+
"acp"
27+
],
28+
"env": {}
29+
}
30+
```
31+
32+
#### Option 2: Launch Local Instance
33+
34+
Use this configuration to run your local development version:
35+
36+
```json
37+
"OpenHands-local": {
38+
"command": "uv",
39+
"args": [
40+
"run",
41+
"--project",
42+
"/YOUR_LOCAL_PATH/OpenHands-CLI",
43+
"openhands",
44+
"acp"
45+
],
46+
"env": {}
47+
}
48+
```
49+
50+
### Debugging
51+
52+
In Zed IDE, open ACP logs before starting a conversation:
53+
- Press `Cmd+Shift+P`
54+
- Search for **"dev: open acp log"**
55+
- This visualizes all events between the ACP server and client
56+
57+
### Testing with JSON-RPC CLI
58+
59+
To reproduce errors or test manually, send JSON-RPC events directly using the test script:
60+
61+
```bash
62+
uv run python scripts/acp/jsonrpc_cli.py ./dist/openhands acp
63+
```
64+
65+
This interactive CLI allows you to:
66+
- Send JSON-RPC messages as single lines
67+
- View stdout/stderr responses
68+
- Exit with `:q`, `:quit`, or `:exit`

openhands_cli/acp_impl/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""OpenHands Agent Client Protocol (ACP) Implementation."""

0 commit comments

Comments
 (0)