b81ab901f3
moa, fallback, worktree, browser, secrets, egress, migrate, whatsapp-cloud, checkpoints, bundles, curator, pets, journey, computer-use, sessions and completion each become a build_<group>_parser() builder. Closure handlers that only closed over their own parser moved verbatim; sessions/completion take the handler by injection. --help/usage/defaults byte-identical for all 399 parsers in the tree (in-process dump before/after).
237 lines
11 KiB
Python
237 lines
11 KiB
Python
"""``hermes computer-use`` subcommand parser.
|
|
|
|
Extracted from ``hermes_cli/main.py:main()`` (god-file Phase 2 follow-up).
|
|
Handlers are injected or imported lazily so this module never imports ``main``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
|
|
|
|
def build_computer_use_parser(subparsers) -> None:
|
|
"""Attach the ``computer-use`` subcommand to ``subparsers``."""
|
|
computer_use_parser = subparsers.add_parser(
|
|
"computer-use",
|
|
help="Manage the Computer Use (cua-driver) backend (macOS/Windows/Linux)",
|
|
description=(
|
|
"Install or check the cua-driver binary used by the\n"
|
|
"`computer_use` toolset. Supported on macOS, Windows, and\n"
|
|
"Linux.\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).\n\n"
|
|
"Use `hermes computer-use doctor` to run cua-driver's\n"
|
|
"`health_report` MCP tool and surface its check matrix\n"
|
|
"(TCC, bundle identity, version, platform support, ...)\n"
|
|
"in human-readable form."
|
|
),
|
|
)
|
|
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/Windows/Linux)",
|
|
)
|
|
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."
|
|
),
|
|
)
|
|
computer_use_sub.add_parser(
|
|
"status",
|
|
help="Print whether cua-driver is installed and on PATH",
|
|
)
|
|
computer_use_doctor = computer_use_sub.add_parser(
|
|
"doctor",
|
|
help="Run cua-driver `health_report` and surface the check matrix",
|
|
description=(
|
|
"Drive cua-driver's stable `health_report` MCP tool and render\n"
|
|
"its check matrix (TCC permissions, bundle identity, version,\n"
|
|
"platform support, screenshot probe, …) as human-readable\n"
|
|
"output. cua-driver owns the health model; this command stays\n"
|
|
"thin so new checks added upstream surface here without code\n"
|
|
"changes. Exits 0 when overall=ok, 1 when degraded/failed, 2\n"
|
|
"when the binary is missing or unreachable."
|
|
),
|
|
)
|
|
computer_use_doctor.add_argument(
|
|
"--include",
|
|
action="append",
|
|
default=[],
|
|
metavar="CHECK",
|
|
help=(
|
|
"Run only the listed checks. Repeat for multiple "
|
|
"(e.g. --include tcc_accessibility --include bundle_identity). "
|
|
"Unknown names are reported by cua-driver."
|
|
),
|
|
)
|
|
computer_use_doctor.add_argument(
|
|
"--skip",
|
|
action="append",
|
|
default=[],
|
|
metavar="CHECK",
|
|
help="Skip the listed checks. Repeat for multiple. Wins over --include.",
|
|
)
|
|
computer_use_doctor.add_argument(
|
|
"--json",
|
|
action="store_true",
|
|
help="Emit the raw structured payload as JSON (same shape as `tools/call`).",
|
|
)
|
|
computer_use_perms = computer_use_sub.add_parser(
|
|
"permissions",
|
|
help="Check or grant macOS Accessibility + Screen Recording (macOS)",
|
|
description=(
|
|
"Computer Use drives the Mac through cua-driver, whose TCC grants\n"
|
|
"attach to cua-driver's own identity (com.trycua.driver) — not the\n"
|
|
"terminal or the Hermes app. `status` reports the driver's grant\n"
|
|
"state; `grant` launches CuaDriver via LaunchServices so the macOS\n"
|
|
"permission dialog is attributed to the process that does the work."
|
|
),
|
|
)
|
|
computer_use_perms_sub = computer_use_perms.add_subparsers(
|
|
dest="computer_use_perms_action"
|
|
)
|
|
computer_use_perms_status = computer_use_perms_sub.add_parser(
|
|
"status",
|
|
help="Report Accessibility + Screen Recording grant state (read-only)",
|
|
)
|
|
computer_use_perms_status.add_argument(
|
|
"--json",
|
|
action="store_true",
|
|
help="Emit the normalized permission payload as JSON.",
|
|
)
|
|
computer_use_perms_sub.add_parser(
|
|
"grant",
|
|
help="Request the grants (opens the dialog attributed to CuaDriver)",
|
|
)
|
|
def cmd_computer_use(args):
|
|
action = getattr(args, "computer_use_action", None)
|
|
if action == "install":
|
|
from hermes_cli.tools_config import (
|
|
_cua_driver_contract_status,
|
|
install_cua_driver,
|
|
)
|
|
if not install_cua_driver(upgrade=bool(getattr(args, "upgrade", False))):
|
|
return 1
|
|
return 0 if _cua_driver_contract_status().get("ready") else 1
|
|
if action == "status":
|
|
import os as _os
|
|
import subprocess
|
|
from hermes_cli.tools_config import _cua_driver_contract_status
|
|
from tools.computer_use.cua_backend import (
|
|
cua_driver_update_check,
|
|
resolve_cua_driver_cmd,
|
|
)
|
|
# Must match the runtime resolver: Desktop/TUI processes can omit
|
|
# ~/.local/bin even though the official installer put the driver there.
|
|
path = resolve_cua_driver_cmd()
|
|
override = _os.environ.get("HERMES_CUA_DRIVER_CMD", "").strip()
|
|
if path:
|
|
version = ""
|
|
try:
|
|
from hermes_cli.tools_config import _cua_driver_env
|
|
version = subprocess.run(
|
|
[path, "--version"],
|
|
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5,
|
|
env=_cua_driver_env(),
|
|
).stdout.strip()
|
|
except Exception:
|
|
pass
|
|
from hermes_cli.tools_config import _cua_version_summary
|
|
version = _cua_version_summary(version)
|
|
# Name the override here too. Without it the operator is told
|
|
# to repair an install that `hermes computer-use install` will
|
|
# (correctly) refuse to touch, with nothing pointing at the
|
|
# env var that actually selected the binary.
|
|
origin = " [custom binary from HERMES_CUA_DRIVER_CMD]" if override else ""
|
|
if version:
|
|
print(f"cua-driver: installed at {path}{origin} ({version})")
|
|
else:
|
|
print(f"cua-driver: installed at {path}{origin}")
|
|
contract = _cua_driver_contract_status(path)
|
|
if not contract.get("ready"):
|
|
print(
|
|
" ⚠ Repair required: "
|
|
+ (contract.get("reason") or "runtime contract is incomplete")
|
|
)
|
|
if override:
|
|
print(
|
|
" Update the binary selected by HERMES_CUA_DRIVER_CMD, or unset "
|
|
"the override and run: hermes computer-use install --upgrade"
|
|
)
|
|
else:
|
|
print(" Run: hermes computer-use install")
|
|
return 1
|
|
try:
|
|
st = cua_driver_update_check()
|
|
if st and st.get("update_available"):
|
|
latest = st.get("latest_version") or "?"
|
|
print(f" ⬆ Update available: cua-driver {latest}.")
|
|
print(" Run: hermes computer-use install --upgrade")
|
|
elif st:
|
|
print(" ✓ Up to date.")
|
|
else:
|
|
# Older driver (no check-update verb) or offline.
|
|
print(" Refresh to latest: hermes computer-use install --upgrade")
|
|
except Exception:
|
|
print(" Refresh to latest: hermes computer-use install --upgrade")
|
|
return 0
|
|
print("cua-driver: not installed")
|
|
print(" Run: hermes computer-use install")
|
|
return 1
|
|
if action == "doctor":
|
|
from tools.computer_use.doctor import run_doctor
|
|
code = run_doctor(
|
|
include=list(getattr(args, "include", []) or []),
|
|
skip=list(getattr(args, "skip", []) or []),
|
|
json_output=bool(getattr(args, "json", False)),
|
|
)
|
|
sys.exit(code)
|
|
if action == "permissions":
|
|
perms_action = getattr(args, "computer_use_perms_action", None)
|
|
if perms_action == "grant":
|
|
from tools.computer_use.permissions import request_permissions_grant
|
|
sys.exit(request_permissions_grant())
|
|
if perms_action == "status":
|
|
import json as _json
|
|
from tools.computer_use.permissions import computer_use_status
|
|
st = computer_use_status()
|
|
if bool(getattr(args, "json", False)):
|
|
print(_json.dumps(st, indent=2, sort_keys=True))
|
|
sys.exit(0 if st["ready"] else 1)
|
|
if not st["platform_supported"]:
|
|
print(f"Computer Use is not supported on {st['platform']}.")
|
|
sys.exit(1)
|
|
if not st["installed"]:
|
|
print("cua-driver: not installed. Run: hermes computer-use install")
|
|
sys.exit(1)
|
|
glyph = lambda v: "✅" if v is True else ("❌" if v is False else "•") # noqa: E731
|
|
print(f"cua-driver: {st['version'] or 'installed'} ({st['platform']})")
|
|
if st["can_grant"]: # macOS TCC permissions
|
|
print(f" {glyph(st['accessibility'])} Accessibility")
|
|
print(f" {glyph(st['screen_recording'])} Screen Recording")
|
|
if not st["ready"]:
|
|
print(" Grant: hermes computer-use permissions grant")
|
|
else: # no TCC model — readiness is driver health
|
|
print(f" {glyph(st['ready'])} driver health (no permission toggles on {st['platform']})")
|
|
for c in st["checks"]:
|
|
if c["status"] != "ok":
|
|
print(f" ⚠ {c['label']}: {c['message']}")
|
|
if st["error"]:
|
|
print(f" ⚠ {st['error']}")
|
|
sys.exit(0 if st["ready"] else 1)
|
|
computer_use_perms.print_help()
|
|
return
|
|
# No subcommand → show help
|
|
computer_use_parser.print_help()
|
|
|
|
computer_use_parser.set_defaults(func=cmd_computer_use)
|