---
name: adom-desktop-cli-guide
description: Use when working with the adom-desktop CLI itself - how to run desktop verbs, discover commands (adom-desktop commands, help topics), address targets with --target, start the serve relay with --supervise, read the status/durationMs/timeoutSeconds response shape, stream long ops with watch, or sweep the screenshot cache with janitor.
---

# The adom-desktop CLI

One Rust binary, two jobs: it runs the relay server (`adom-desktop serve`) and it sends
verbs to the Adom Bridge app (`adom-desktop <verb> '<json>'`). Everything an AI does to
the user's Mac goes through this surface. (The app is named Adom Bridge, formerly Adom
Desktop; the CLI binary name is unchanged.)

## The two binaries (same CLI, two homes)

| Where | Binary | How it got there | How it reaches the desktop |
|---|---|---|---|
| Cloud Linux container | `adom-desktop` | `adom-wiki pkg install adom/adom-desktop` (installs the CLI + the skill set) | Over the relay it serves (WS :8765 / HTTP :8766) |
| The Mac, inside the app bundle | `adom-desktop-cli` at `Adom Bridge.app/Contents/Resources/` | Bundled by the Bridge installer (standalone: `/Applications`; Hydrogen-managed: `~/Library/Application Support/adom/bridge/`) | Auto-probes the local app's loopback direct API (`127.0.0.1:47200`, discovery file `~/.adom/direct-api-port`, fallback scan 47200-47209) |

Verify which build you have:

```bash
adom-desktop --version
# adom-desktop 1.9.x (<git-sha>, built <timestamp>)
```

## Discovering verbs: `commands` is THE catalog, not `--help`

`adom-desktop --help` lists only the real clap subcommands: `serve` and `install`. Every
actual capability (200+ verbs) is a dynamic verb forwarded to the desktop app, and the
authoritative machine-readable catalog is:

```bash
adom-desktop commands                    # full catalog: categories, args, returns, _hint per verb
adom-desktop --target <name> commands    # plus THAT box's installed bridges
adom-desktop help <topic>                # details for one verb or namespace (e.g. help pup, help shell)
```

Run `commands` FIRST when unsure what a verb is called. There is almost certainly a
structured verb with typed args (`process_list`, `process_kill`, `run_script`,
`desktop_list_files`) so you never hand-quote a command through JSON into a shell.
`shell_execute` is the escape hatch for things with no verb yet, not the default.

Two kinds of verbs, do not confuse them:

- **Built-in core verbs** are on every Adom Bridge, no install: screenshots, windows,
  input, files, shell, process, tunnel, update, notify, and the `desktop_ui_*` /
  `desktop_find_control` family (drive controls by NAME via macOS Accessibility;
  needs the Accessibility permission, granted to "Adom Bridge").
- **Bridge verbs** (`browser_*`, `kicad_*`, `fusion_*`, `nbrowser_*`, ...) need that bridge
  installed. Check `installed_bridges_on_target` in `commands`, or run `bridge_list`.

An unknown verb returns `errorCode:"unknown_verb"` with a fuzzy did-you-mean `_hint`.
Read it; do not conclude a capability is missing from a guessed name.

Recognized help namespaces (`adom-desktop help <topic>` or `adom-desktop <topic> help`):
`pup`/`browser`, `kicad`, `fusion`, `hd`, `shell`, `files`, `notifications`, `watch`,
`setup`, `connectivity`, `system`, `record`, `server`, `bridge`, `wiki`.

## Invoking a verb

```bash
adom-desktop <verb> '<json-args>'
adom-desktop ping
adom-desktop shell_execute '{"command":"echo hello","reason":"smoke-testing the shell path"}'
adom-desktop desktop_screenshot_window '{"hwnd": 12345}'
```

Output is pretty-printed JSON on stdout.

**Grouped verbs** take a bare-word subcommand before the JSON:

```bash
adom-desktop tunnel open '{"to":"127.0.0.1:5900"}'
adom-desktop tunnel list
```

**Gated verbs need a `reason`.** Verbs that require human approval over the relay
(`shell_execute`, `run_script`, and the rest of the permission class) require a `reason`
arg: one plain sentence the user reads verbatim in the approval prompt. A missing reason
returns `errorCode:"reason_required"`.

## The uniform response shape

Every result carries, on every path (relay, direct API, bundled Mac CLI):

- **`status`**: `ok` | `timeout` | `busy` | `error`. One authoritative field. An empty
  `stdout` with `status:ok` is a real empty-output success, NOT a timeout.
- **`durationMs`**: the CLI's measured round-trip.
- **`_hint`**: actionable guidance authored by Bridge core (recovery steps on
  `timeout`/`busy`, next-verb pointers on success). The CLI only authors a hint for the
  one case Bridge cannot: the call never reached the desktop at all.
- **`timeoutSeconds`**: the HTTP budget that governed this call. On a timeout, bridge
  verbs return `errorCode:"timed_out"` with `stillRunning:true`: the operation usually
  keeps running on the bridge. Poll the response's `statusVerb` instead of re-issuing.
- Exec verbs additionally carry `exitCode` / `stdout` / `stderr`.

`adom-desktop timeouts` returns the per-verb budget registry (long verbs, their budgets,
and the status verb to poll).

## Targeting: one relay, many desktops

One relay can hold several Adom Bridges (a Mac laptop + another machine + ...). Each has
a name (`--client-name` launch flag / Settings field / hostname default).

```bash
adom-desktop targets                              # {count, targets:[{name, hostname, version, os, connectedAt}]}
adom-desktop --target mymac desktop_screenshot_screen
adom-desktop --target all ping                    # fan out; results keyed by name + summary
```

- `--target` is a global flag: works before or after the verb, case-insensitive, matches
  `clientName` then `hostname`. `ADOM_TARGET=<name>` sets a default; the flag wins.
- Exactly ONE desktop connected: no `--target` needed.
- Multiple connected, no `--target`: the call fails with `error:"ambiguous_target"`
  (exit 1) and lists the connected names. It never guesses. A bad name returns
  `target_not_found`.
- `--target all` returns `{broadcast:true, results:{<name>:...}, summary:{total,ok,failed}}`;
  a per-desktop failure is isolated to that desktop's entry.
- Caveat: `ping` has its own direct-API path and does NOT honor `--target`. Smoke-test
  routing with a relay-path verb (`server_list`, `desktop_*`).
- Mac clients report `os: macOS` in the `targets` listing.

## Local vs relay routing

- **No `--target` on the Mac**: the bundled CLI short-circuits to the local app's
  loopback direct API. No relay required. This is the right path for anything on the
  same machine.
- **`--target <name>` (or `ADOM_TARGET`)**: the call traverses the relay, even from the
  desktop itself, so it can reach a named or broadcast set of machines.
- **In a container**: everything goes through the relay the container runs.

## The relay: `serve --supervise` in containers

The relay must be running before any desktop can connect. In a container, ALWAYS start
it supervised:

```bash
adom-desktop serve --supervise                       # detached + self-healing
adom-desktop serve --supervise --owner MyMac         # send restart toasts to one desktop
adom-desktop serve --stop                            # stop watchdog + relay
```

Why not `adom-desktop serve &`? A backgrounded relay is still a child of your session.
When the session is torn down, the relay dies with it, and every connected desktop loops
"Reconnecting in 30s" for hours until a human restarts it. `--supervise` detaches the
relay into its own session (reparented to init) and runs a watchdog that restarts it
with exponential backoff (2s up to a 5 min cap; a clean run resets it) and toasts the
connected desktops with the reason on each restart.

Plain foreground `serve` still works and prints a loud warning that it dies with the
session. Caveat: a full container reboot still needs a boot hook (cron `@reboot` or the
container's init); the watchdog covers session teardown, not host reboot.

What `serve` binds:

- WebSocket on `0.0.0.0:8765` (desktop apps connect here)
- HTTP API on `127.0.0.1:8766` (CLI commands go here)

Health check: `curl -sf http://127.0.0.1:8766/health`. If another relay is already
answering, `serve` skips with a friendly message; `--force-bind` overrides (only for
deliberate parallel relays).

## `watch`: stream long operations

For long-running walks, use the built-in `watch` wrapper instead of a polling loop. It
spawns the inner command, polls status internally, and emits one JSON event per line to
stdout as progress happens:

```bash
adom-desktop watch '{"command":"fusion_walk_cloud_tree","args":{"projectName":"Main","nameContains":"BQ25792"}}'
adom-desktop watch '{"command":"fusion_walk_cloud_tree","args":{...},"interval":1}'
```

Watchable commands (whitelist): `fusion_walk_cloud_tree`, `fusion_search_cloud_files`.
Others return `success:false` with a `_hint` listing the watchable set. Do not use it
for short operations that return in under 2 seconds. From Claude Code, pipe `watch`
into the Monitor tool for real-time events.

## `janitor`: the screenshot cache

Every screenshot verb auto-pulls its PNGs to a local cache on the caller's machine
(`~/project/screenshots/adom-desktop/` when `~/project` exists, else
`~/.adom/screenshots/`; never `/tmp`), bucketed into `window/` and `screen/` with a
JSON sidecar per shot. The CLI-local `janitor` verb (no relay) keeps it bounded:

```bash
adom-desktop janitor                     # status: managed dirs, sizes, policy, last sweep
adom-desktop janitor '{"sub":"run"}'     # force a sweep now
```

A sweep also runs opportunistically at most once per 24h on any CLI call. Policy is
age + size capped, newest files kept; sidecars follow their PNG. Do not build your own
cleaner: verify with `janitor` status instead.

## Environment variables

| Variable | Effect |
|---|---|
| `ADOM_API` | Relay HTTP API base URL the CLI talks to (default `http://127.0.0.1:8766`). Point it at a proxied relay API to reach a remote relay directly. |
| `ADOM_TARGET` | Default `--target` value; the flag wins. |
| `ADOM_WS_PORT` / `ADOM_HTTP_PORT` | `serve` port overrides (defaults 8765 / 8766). |
| `ADOM_AUTH_TOKEN` | `serve` auth token. |
| `ADOM_JANITOR=0` | Disable the opportunistic cache sweep. |
| `ADOM_SHOTS_DIR` | Override the screenshot cache directory. |
