chore: import upstream snapshot with attribution
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:03:19 +08:00
commit 91e75e620b
3227 changed files with 1307078 additions and 0 deletions
@@ -0,0 +1,21 @@
"""Utility modules for CUA CLI."""
from .async_utils import run_async
from .output import (
print_error,
print_info,
print_json,
print_success,
print_table,
print_warning,
)
__all__ = [
"print_table",
"print_json",
"print_error",
"print_success",
"print_warning",
"print_info",
"run_async",
]
@@ -0,0 +1,20 @@
"""Async utilities for CUA CLI."""
import asyncio
from typing import Any, Coroutine, TypeVar
T = TypeVar("T")
def run_async(coro: Coroutine[Any, Any, T]) -> T:
"""Run an async coroutine synchronously.
This is the standard pattern for CLI commands that need to call async code.
Args:
coro: The coroutine to run
Returns:
The result of the coroutine
"""
return asyncio.run(coro)
+103
View File
@@ -0,0 +1,103 @@
"""Docker utilities for CUA CLI."""
import platform
import shutil
import subprocess
from pathlib import Path
def find_free_port(start: int = 5000, end: int = 9000) -> int:
"""Find a free port in the given range.
Args:
start: Start of port range (inclusive)
end: End of port range (exclusive)
Returns:
First available port in the range
Raises:
RuntimeError: If no free port found
"""
import socket
for port in range(start, end):
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", port))
return port
except OSError:
continue
raise RuntimeError(f"No free port found in range {start}-{end}")
def allocate_ports(vnc_default: int = 8006, api_default: int = 5000) -> tuple[int, int]:
"""Allocate VNC and API ports, auto-selecting if defaults are in use.
Args:
vnc_default: Preferred VNC port
api_default: Preferred API port
Returns:
Tuple of (vnc_port, api_port)
"""
import socket
def is_port_free(port: int) -> bool:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", port))
return True
except OSError:
return False
vnc_port = vnc_default if is_port_free(vnc_default) else find_free_port(8000, 9000)
api_port = api_default if is_port_free(api_default) else find_free_port(5000, 6000)
return vnc_port, api_port
def create_overlay_copy(golden_path: Path, overlay_path: Path, verbose: bool = False) -> None:
"""Copy golden image to overlay directory for COW-like behavior.
This is a temporary workaround until proper QEMU overlay support is added.
Uses native `cp -a` on Unix for speed (5x faster than Python shutil).
Falls back to shutil.copytree on Windows.
WIP: https://github.com/trycua/cua/issues/699
Args:
golden_path: Path to golden image directory
overlay_path: Path to overlay directory (will be created/cleaned)
verbose: Print progress messages
Raises:
RuntimeError: If copy fails
"""
if overlay_path.exists():
shutil.rmtree(overlay_path)
overlay_path.mkdir(parents=True, exist_ok=True)
if verbose:
print(f" Source: {golden_path}")
print(f" Overlay: {overlay_path}")
print(" (This may take a while for large images)")
print(" WIP: https://github.com/trycua/cua/issues/699")
if platform.system() == "Windows":
try:
for item in golden_path.iterdir():
src = golden_path / item.name
dst = overlay_path / item.name
if src.is_dir():
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
except Exception as e:
raise RuntimeError(f"Failed to create overlay: {e}")
else:
result = subprocess.run(
["cp", "-a", f"{golden_path}/.", str(overlay_path)], capture_output=True, text=True
)
if result.returncode != 0:
raise RuntimeError(f"Failed to create overlay: {result.stderr or 'cp failed'}")
@@ -0,0 +1,66 @@
"""Output formatting utilities for CUA CLI."""
import json
from typing import Any
from rich.console import Console
from rich.table import Table
console = Console()
error_console = Console(stderr=True)
def print_table(
data: list[dict[str, Any]],
columns: list[tuple[str, str]] | None = None,
title: str | None = None,
) -> None:
"""Print data as a formatted table.
Args:
data: List of dictionaries to display
columns: List of (key, header) tuples. If None, uses all keys from first item.
title: Optional table title
"""
if not data:
console.print("[dim]No data to display[/dim]")
return
table = Table(title=title, show_header=True, header_style="bold")
if columns is None:
columns = [(k, k.upper()) for k in data[0].keys()]
for _, header in columns:
table.add_column(header)
for item in data:
row = [str(item.get(key, "")) for key, _ in columns]
table.add_row(*row)
console.print(table)
def print_json(data: Any) -> None:
"""Print data as formatted JSON."""
console.print_json(json.dumps(data, indent=2, default=str))
def print_error(message: str) -> None:
"""Print an error message to stderr."""
error_console.print(f"[red]Error:[/red] {message}")
def print_success(message: str) -> None:
"""Print a success message."""
console.print(f"[green]{message}[/green]")
def print_warning(message: str) -> None:
"""Print a warning message."""
console.print(f"[yellow]Warning:[/yellow] {message}")
def print_info(message: str) -> None:
"""Print an info message."""
console.print(f"[blue]{message}[/blue]")
@@ -0,0 +1,30 @@
"""XDG Base Directory helpers for CUA CLI."""
import os
from pathlib import Path
def get_xdg_data_home() -> Path:
"""Get XDG_DATA_HOME, defaulting to ~/.local/share per spec."""
xdg_data = os.environ.get("XDG_DATA_HOME")
if xdg_data:
return Path(xdg_data)
return Path.home() / ".local" / "share"
def get_xdg_state_home() -> Path:
"""Get XDG_STATE_HOME, defaulting to ~/.local/state per spec."""
xdg_state = os.environ.get("XDG_STATE_HOME")
if xdg_state:
return Path(xdg_state)
return Path.home() / ".local" / "state"
def get_data_dir() -> Path:
"""Get the CUA data directory (for images)."""
return get_xdg_data_home() / "cua"
def get_state_dir() -> Path:
"""Get the CUA state directory (for registries)."""
return get_xdg_state_home() / "cua"
@@ -0,0 +1,148 @@
"""Image registry for CUA CLI.
Manages a JSON registry of locally stored images at ~/.local/state/cua/images.json.
"""
import json
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
from cua_cli.utils.paths import get_data_dir, get_state_dir
def get_image_registry_path() -> Path:
"""Get the image registry file path."""
return get_state_dir() / "images.json"
def load_image_registry() -> Dict[str, Any]:
"""Load the image registry."""
registry_path = get_image_registry_path()
if registry_path.exists():
try:
data = json.loads(registry_path.read_text())
return data.get("images", data)
except Exception:
return {}
return {}
def save_image_registry(registry: Dict[str, Any]) -> None:
"""Save the image registry."""
registry_path = get_image_registry_path()
registry_path.parent.mkdir(parents=True, exist_ok=True)
data = {"version": 1, "images": registry}
registry_path.write_text(json.dumps(data, indent=2))
def register_image(
name: str,
platform: str,
path: Path,
description: str = "",
docker_image: Optional[str] = None,
config: Optional[Dict] = None,
parent: Optional[str] = None,
tags: Optional[List[str]] = None,
apps_installed: Optional[List[str]] = None,
) -> None:
"""Register an image in the registry."""
from cua_cli.commands.platform import PLATFORMS
registry = load_image_registry()
entry: Dict[str, Any] = {
"platform": platform,
"path": str(path),
"description": description,
"created_at": datetime.now().isoformat(),
}
if docker_image:
entry["docker_image"] = docker_image
if config:
entry["config"] = config
if parent:
entry["parent"] = parent
if tags:
entry["tags"] = tags
if apps_installed:
entry["apps_installed"] = apps_installed
platform_config = PLATFORMS.get(platform, {})
if platform_config.get("image_marker"):
entry["marker_file"] = platform_config["image_marker"]
registry[name] = entry
save_image_registry(registry)
def unregister_image(name: str) -> None:
"""Remove an image from the registry."""
registry = load_image_registry()
if name in registry:
del registry[name]
save_image_registry(registry)
def get_image_info(name: str) -> Optional[Dict]:
"""Get image info by name."""
registry = load_image_registry()
return registry.get(name)
def list_images() -> Dict[str, Any]:
"""List all registered images."""
return load_image_registry()
def auto_discover_images() -> None:
"""Scan images directory and register any unregistered images.
Handles images created with --detach mode that never got registered.
"""
from cua_cli.commands.platform import PLATFORMS
images_dir = get_data_dir() / "images"
if not images_dir.exists():
return
registry = load_image_registry()
modified = False
for image_dir in images_dir.iterdir():
if not image_dir.is_dir():
continue
name = image_dir.name
if name in registry:
continue
for platform_name, config in PLATFORMS.items():
marker = config.get("image_marker")
if not marker:
continue
marker_path = image_dir / marker
if marker_path.exists():
print(f"Auto-registering discovered image: {name} ({platform_name})")
entry = {
"platform": platform_name,
"path": str(image_dir),
"description": f"Auto-discovered {platform_name} image",
"created_at": datetime.now().isoformat(),
"docker_image": config.get("image"),
"config": {"memory": "8G", "cpus": "8"},
"tags": ["auto-discovered"],
"marker_file": marker,
}
registry[name] = entry
modified = True
break
if modified:
save_image_registry(registry)
@@ -0,0 +1,262 @@
"""Trajectory recording utility for cua do actions.
Records each action into a replayable trajectory compatible with the
TrajectoryViewer at cua.ai/trajectory-viewer.
All state is file-based (each `cua do` invocation is a separate process).
The current session path is stored in ~/.cua/do_target.json under the
``trajectory_session`` key.
Directory layout::
~/.cua/trajectories/{machine_name}/{YYYYMMDD-HHMMSS}/
turn_001/
screenshot.png
turn_001_agent_response.json
turn_002/
screenshot.png
turn_002_agent_response.json
"""
from __future__ import annotations
import json
import shutil
import time
import uuid
import zipfile
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any
_CUA_DIR = Path.home() / ".cua"
_TRAJECTORIES_DIR = _CUA_DIR / "trajectories"
_STATE_FILE = _CUA_DIR / "do_target.json"
# ── state helpers ────────────────────────────────────────────────────────────
def _load_state() -> dict:
if _STATE_FILE.exists():
try:
return json.loads(_STATE_FILE.read_text())
except Exception:
pass
return {}
def _save_state(state: dict) -> None:
_STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
_STATE_FILE.write_text(json.dumps(state, indent=2))
# ── session management ───────────────────────────────────────────────────────
def ensure_session(state: dict) -> Path:
"""Return the current session directory, creating one if needed.
If ``trajectory_session`` is already set in *state* and the directory
exists, it is reused. Otherwise a new timestamped directory is created
under ``~/.cua/trajectories/{machine_name}/`` and persisted to state.
"""
existing = state.get("trajectory_session")
if existing:
p = Path(existing)
if p.is_dir():
return p
machine = state.get("name") or state.get("provider") or "unknown"
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
session_dir = _TRAJECTORIES_DIR / machine / ts
session_dir.mkdir(parents=True, exist_ok=True)
state["trajectory_session"] = str(session_dir)
_save_state(state)
return session_dir
def reset_session(state: dict) -> None:
"""Clear ``trajectory_session`` from state (called on ``cua do switch``)."""
state.pop("trajectory_session", None)
_save_state(state)
# ── turn recording ───────────────────────────────────────────────────────────
def get_next_turn_number(session_dir: Path) -> int:
"""Scan for existing ``turn_NNN`` dirs and return the next number."""
max_n = 0
for child in session_dir.iterdir():
if child.is_dir() and child.name.startswith("turn_"):
try:
n = int(child.name.split("_", 1)[1])
if n > max_n:
max_n = n
except (ValueError, IndexError):
pass
return max_n + 1
def _build_action_dict(action_type: str, action_params: dict[str, Any]) -> dict[str, Any]:
"""Build the ``action`` dict for the agent-response JSON."""
action: dict[str, Any] = {"type": action_type}
action.update(action_params)
return action
def record_turn(
session_dir: Path,
action_type: str,
action_params: dict[str, Any],
screenshot_bytes: bytes | None = None,
) -> Path:
"""Write a single turn to the session directory.
Creates ``turn_NNN/screenshot.png`` (if *screenshot_bytes* provided)
and ``turn_NNN/turn_NNN_agent_response.json``.
Returns the turn directory.
"""
turn_num = get_next_turn_number(session_dir)
turn_name = f"turn_{turn_num:03d}"
turn_dir = session_dir / turn_name
turn_dir.mkdir(parents=True, exist_ok=True)
# Screenshot
if screenshot_bytes:
(turn_dir / "screenshot.png").write_bytes(screenshot_bytes)
# Agent response JSON (TrajectoryViewer-compatible)
call_id = f"call_{uuid.uuid4().hex[:12]}"
ts_ms = int(time.time())
response_json: dict[str, Any] = {
"model": "cua-cli",
"response": {
"id": f"resp_{int(time.time() * 1000)}",
"object": "response",
"created_at": ts_ms,
"status": "completed",
"model": "cua-cli",
"output": [
{
"type": "computer_call",
"id": call_id,
"call_id": call_id,
"action": _build_action_dict(action_type, action_params),
"pending_safety_checks": [],
"status": "completed",
}
],
},
}
json_path = turn_dir / f"{turn_name}_agent_response.json"
json_path.write_text(json.dumps(response_json, indent=2))
return turn_dir
# ── listing / inspection ─────────────────────────────────────────────────────
def list_trajectories(machine: str | None = None) -> list[dict[str, Any]]:
"""Return a list of trajectory sessions.
Each entry is ``{machine, session, path, turns, created}``.
If *machine* is given, only that machine's sessions are returned.
"""
results: list[dict[str, Any]] = []
if not _TRAJECTORIES_DIR.is_dir():
return results
machines = [_TRAJECTORIES_DIR / machine] if machine else sorted(_TRAJECTORIES_DIR.iterdir())
for machine_dir in machines:
if not machine_dir.is_dir():
continue
for session_dir in sorted(machine_dir.iterdir()):
if not session_dir.is_dir():
continue
turns = sum(
1 for c in session_dir.iterdir() if c.is_dir() and c.name.startswith("turn_")
)
# Parse created timestamp from dir name
try:
created = datetime.strptime(session_dir.name, "%Y%m%d-%H%M%S")
except ValueError:
created = datetime.fromtimestamp(session_dir.stat().st_ctime)
results.append(
{
"machine": machine_dir.name,
"session": session_dir.name,
"path": str(session_dir),
"turns": turns,
"created": created.isoformat(),
}
)
return results
# ── zipping ──────────────────────────────────────────────────────────────────
def zip_trajectory(session_path: str | Path) -> Path:
"""Create a zip of a session directory in TrajectoryViewer-compatible format.
Returns the path to the created zip file (placed next to the session dir).
"""
session_dir = Path(session_path)
zip_path = session_dir.parent / f"{session_dir.name}.zip"
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for child in sorted(session_dir.rglob("*")):
if child.is_file():
zf.write(child, child.relative_to(session_dir))
return zip_path
# ── cleanup ──────────────────────────────────────────────────────────────────
def clean_trajectories(
older_than_days: int | None = None,
machine: str | None = None,
) -> list[str]:
"""Delete trajectory sessions matching the criteria.
Returns a list of deleted session paths.
"""
deleted: list[str] = []
if not _TRAJECTORIES_DIR.is_dir():
return deleted
cutoff = datetime.now() - timedelta(days=older_than_days) if older_than_days else None
machines = [_TRAJECTORIES_DIR / machine] if machine else sorted(_TRAJECTORIES_DIR.iterdir())
for machine_dir in machines:
if not machine_dir.is_dir():
continue
for session_dir in sorted(machine_dir.iterdir()):
if not session_dir.is_dir():
continue
if cutoff:
try:
created = datetime.strptime(session_dir.name, "%Y%m%d-%H%M%S")
except ValueError:
created = datetime.fromtimestamp(session_dir.stat().st_ctime)
if created >= cutoff:
continue
shutil.rmtree(session_dir)
deleted.append(str(session_dir))
# Also remove zip if it exists
zip_path = session_dir.parent / f"{session_dir.name}.zip"
if zip_path.exists():
zip_path.unlink()
# Remove empty machine dirs
if machine_dir.is_dir() and not any(machine_dir.iterdir()):
machine_dir.rmdir()
return deleted