Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 81 additions & 34 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11963,69 +11963,116 @@ def _dispatch_secrets(args): # noqa: ANN001
build_tools_parser(subparsers, cmd_tools=cmd_tools)

# =========================================================================
# computer-use command — manage Computer Use (cua-driver) on macOS
# computer-use command — manage Computer Use desktop backends
# =========================================================================
computer_use_parser = subparsers.add_parser(
"computer-use",
help="Manage the Computer Use (cua-driver) backend (macOS)",
help="Manage Computer Use desktop backends",
description=(
"Install or check the cua-driver binary used by the\n"
"`computer_use` toolset. macOS-only.\n\n"
"Use `hermes computer-use install` to fetch and run the\n"
"upstream cua-driver installer. This is equivalent to the\n"
"post-setup hook that `hermes tools` runs when you first\n"
"enable the Computer Use toolset, and is a stable target\n"
"for re-running the install if it didn't fire (e.g. when\n"
"toggling the toolset on a returning-user setup)."
"Install or check the desktop driver used by the `computer_use`\n"
"toolset. macOS uses cua-driver. Linux uses linux-computer-use.\n\n"
"Use `hermes computer-use install` to install the appropriate\n"
"driver for this platform. This is equivalent to the post-setup\n"
"hook that `hermes tools` runs when you first enable the Computer\n"
"Use toolset, and is a stable target for re-running the install."
),
)
computer_use_sub = computer_use_parser.add_subparsers(dest="computer_use_action")

computer_use_install = computer_use_sub.add_parser(
"install",
help="Install or repair the cua-driver binary (macOS)",
help="Install or repair the desktop computer-use driver for this platform",

)
computer_use_install.add_argument(
"--upgrade",
action="store_true",
help=(
"Re-run the upstream installer even if cua-driver is already on "
"PATH. The upstream install.sh always pulls the latest release, "
"so this performs an in-place upgrade."
"Re-run the platform installer even if a driver is already on PATH. "
"On macOS this refreshes cua-driver; on Linux this upgrades the "
"linux-computer-use package when pipx is available."
),
)
computer_use_sub.add_parser(
"status",
help="Print whether cua-driver is installed and on PATH",
help="Print whether the platform computer-use driver is installed and on PATH",
)

def cmd_computer_use(args):
action = getattr(args, "computer_use_action", None)
if action == "install":
from hermes_cli.tools_config import install_cua_driver
install_cua_driver(upgrade=bool(getattr(args, "upgrade", False)))
return
import shutil
import subprocess
import sys

upgrade = bool(getattr(args, "upgrade", False))
if sys.platform == "darwin":
from hermes_cli.tools_config import install_cua_driver
install_cua_driver(upgrade=upgrade)
return
if sys.platform.startswith("linux"):
if shutil.which("linux-computer-use") and not upgrade:
print("linux-computer-use: already installed")
print(" Refresh to latest: hermes computer-use install --upgrade")
return
pkg = "git+https://github.com/tyy130/linux-computer-use"
if shutil.which("pipx"):
cmd = ["pipx", "install", pkg]
if upgrade and shutil.which("linux-computer-use"):
cmd = ["pipx", "upgrade", "linux-computer-use"]
subprocess.run(cmd, check=True)
return
if shutil.which("uv"):
cmd = ["uv", "tool", "install", pkg]
if upgrade:
cmd.insert(3, "--upgrade")
subprocess.run(cmd, check=True)
return
print("linux-computer-use install requires pipx or uv on PATH.")
print(f" pipx install {pkg}")
print(f" uv tool install {pkg}")
raise SystemExit(1)
print("computer-use install is currently supported on macOS and Linux.")
raise SystemExit(1)
if action == "status":
import shutil
import subprocess
path = shutil.which("cua-driver")
if path:
version = ""
try:
version = subprocess.run(
["cua-driver", "--version"],
capture_output=True, text=True, timeout=5,
).stdout.strip()
except Exception:
pass
if version:
print(f"cua-driver: installed at {path} ({version})")
import sys

def _print_binary(name, refresh_cmd):
path = shutil.which(name)
if path:
version = ""
try:
version = subprocess.run(
[name, "--version"],
capture_output=True, text=True, timeout=5,
).stdout.strip()
except Exception:
pass
if version:
print(f"{name}: installed at {path} ({version})", flush=True)
else:
print(f"{name}: installed at {path}", flush=True)
print(f" Refresh to latest: {refresh_cmd}", flush=True)
return True
print(f"{name}: not installed")
return False

if sys.platform == "darwin":
if not _print_binary("cua-driver", "hermes computer-use install --upgrade"):
print(" Run: hermes computer-use install")
return
if sys.platform.startswith("linux"):
if _print_binary("linux-computer-use", "hermes computer-use install --upgrade"):
try:
subprocess.run(["linux-computer-use", "status"], check=False, timeout=10)
except Exception:
pass
else:
print(f"cua-driver: installed at {path}")
print(" Refresh to latest: hermes computer-use install --upgrade")
print(" Run: hermes computer-use install")
return
print("cua-driver: not installed")
print(" Run: hermes computer-use install")
print("computer_use backend: unsupported platform")
return
# No subcommand → show help
computer_use_parser.print_help()
Expand Down
84 changes: 71 additions & 13 deletions hermes_cli/tools_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,23 +516,19 @@ def _checklist_toolset_keys(platform: str) -> Set[str]:
],
},
"computer_use": {
"name": "Computer Use (macOS)",
"name": "Computer Use (Desktop)",
"icon": "🖱️",
"platform_gate": "darwin",
"providers": [
{
"name": "cua-driver (background)",
"name": "Platform driver (cua-driver on macOS, linux-computer-use on Linux)",
"badge": "★ recommended · free · local",
"tag": (
"macOS background computer-use via SkyLight SPIs — does "
"NOT steal your cursor or focus. Works with any model."
"Desktop computer-use for any tool-capable model. macOS "
"uses background cua-driver; Linux uses an X11-first MCP "
"driver and may move pointer/focus."
),
"env_vars": [
# cua-driver reads HOME/TMPDIR from the process env, no
# extra keys required. HERMES_CUA_DRIVER_VERSION is an
# optional pin for reproducibility across macOS updates.
],
"post_setup": "cua_driver",
"env_vars": [],
"post_setup": "computer_use_driver",
},
],
},
Expand Down Expand Up @@ -789,6 +785,65 @@ def install_cua_driver(upgrade: bool = False) -> bool:
return ok


def install_linux_computer_use(upgrade: bool = False) -> bool:
"""Install or refresh the Linux computer-use MCP driver."""
import platform as _plat
import shutil
import subprocess

if _plat.system() != "Linux":
if not upgrade:
_print_warning(" linux-computer-use is Linux-only; skipping.")
return False

binary = shutil.which("linux-computer-use")
if binary and not upgrade:
_print_success(f" linux-computer-use already installed: {binary}")
return True

pkg = "git+https://github.com/tyy130/linux-computer-use"
if shutil.which("pipx"):
cmd = ["pipx", "install", pkg]
if binary and upgrade:
cmd = ["pipx", "upgrade", "linux-computer-use"]
elif shutil.which("uv"):
cmd = ["uv", "tool", "install", pkg]
if upgrade:
cmd.insert(3, "--upgrade")
else:
_print_warning(" pipx or uv is required to install linux-computer-use automatically.")
_print_info(f" pipx install {pkg}")
_print_info(f" uv tool install {pkg}")
return bool(binary)

_print_info(" Installing linux-computer-use (Linux desktop MCP driver)...")
try:
result = subprocess.run(cmd, timeout=300)
if result.returncode == 0 and shutil.which("linux-computer-use"):
_print_success(" linux-computer-use installed.")
_print_info(" For full control on Linux, use an X11 session with xdotool + scrot installed.")
return True
_print_warning(" linux-computer-use installation did not complete. Run manually:")
_print_info(f" {' '.join(cmd)}")
return bool(binary)
except subprocess.TimeoutExpired:
_print_warning(" linux-computer-use installation timed out. Run manually:")
_print_info(f" {' '.join(cmd)}")
return bool(binary)


def install_computer_use_driver(upgrade: bool = False) -> bool:
"""Install the platform-appropriate Computer Use driver."""
import platform as _plat
system = _plat.system()
if system == "Darwin":
return install_cua_driver(upgrade=upgrade)
if system == "Linux":
return install_linux_computer_use(upgrade=upgrade)
_print_warning(" Computer Use desktop driver is currently supported on macOS and Linux.")
return False


def _run_cua_driver_installer(label: str = "Installing", verbose: bool = True) -> bool:
"""Run the upstream cua-driver install.sh. Returns True on success.

Expand Down Expand Up @@ -970,8 +1025,8 @@ def _run_post_setup(post_setup_key: str):
_print_warning(" Node.js not found. Install Camofox via Docker:")
_print_info(" docker run -p 9377:9377 -e CAMOFOX_PORT=9377 jo-inc/camofox-browser")

elif post_setup_key == "cua_driver":
install_cua_driver(upgrade=False)
elif post_setup_key in {"computer_use_driver", "cua_driver"}:
install_computer_use_driver(upgrade=False)

elif post_setup_key == "kittentts":
try:
Expand Down Expand Up @@ -2141,6 +2196,9 @@ def _hidden_nous_gateway_message(
# a no-key provider, and (b) an installed-state check is cheap and
# doesn't trigger a heavy import.
"cua_driver": lambda: bool(shutil.which(_cua_driver_cmd())),
"computer_use_driver": lambda: bool(
shutil.which(_cua_driver_cmd()) or shutil.which("linux-computer-use")
),
}


Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,10 @@ nemo-relay = ["nemo-relay==0.3"]
homeassistant = ["aiohttp==3.13.4"]
sms = ["aiohttp==3.13.4"]
teams = ["microsoft-teams-apps==2.0.13.4", "aiohttp==3.13.4"]
# Computer use — macOS background desktop control via cua-driver (MCP stdio).
# The cua-driver binary itself is installed via `hermes tools` post-setup
# (curl install script); this extra just pins the MCP client used to talk
# to it, which is already provided by the `mcp` extra.
# Computer use — desktop control via MCP stdio backends. macOS uses cua-driver
# (installed via `hermes tools` / `hermes computer-use install`). Linux uses the
# companion linux-computer-use executable (`linux-computer-use mcp`). This extra
# pins the MCP client used to talk to those drivers.
computer-use = ["mcp==1.26.0", "starlette==1.0.1"] # starlette: CVE-2026-48710
acp = ["agent-client-protocol==0.9.0"]
# mistral: Voxtral STT + TTS. Pinned to an exact verified-clean version.
Expand Down
10 changes: 8 additions & 2 deletions tests/tools/test_computer_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,17 @@ def test_tool_registers_with_registry(self):
assert entry.toolset == "computer_use"
assert entry.schema["name"] == "computer_use"

def test_check_fn_is_false_on_linux(self):
def test_check_fn_uses_linux_driver_on_linux(self):
import tools.computer_use_tool # noqa: F401
from tools.registry import registry
entry = registry._tools["computer_use"]
if sys.platform != "darwin":
if sys.platform.startswith("linux"):
with patch.dict(os.environ, {"HERMES_COMPUTER_USE_BACKEND": ""}, clear=False):
with patch("tools.computer_use.linux_backend.linux_driver_binary_available", return_value=True):
assert entry.check_fn() is True
with patch("tools.computer_use.linux_backend.linux_driver_binary_available", return_value=False):
assert entry.check_fn() is False
elif sys.platform != "darwin":
assert entry.check_fn() is False


Expand Down
Loading