Adom Bridge (macOS)
Public Made by Adomby adom
Adom Bridge for macOS: the menu-bar daemon that connects AI tools to your Mac.
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:
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:
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_controlfamily (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. Checkinstalled_bridges_on_targetincommands, or runbridge_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
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:
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 emptystdoutwithstatus:okis a real empty-output success, NOT a timeout.durationMs: the CLI's measured round-trip._hint: actionable guidance authored by Bridge core (recovery steps ontimeout/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 returnerrorCode:"timed_out"withstillRunning:true: the operation usually keeps running on the bridge. Poll the response'sstatusVerbinstead 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).
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
--targetis a global flag: works before or after the verb, case-insensitive, matchesclientNamethenhostname.ADOM_TARGET=<name>sets a default; the flag wins.- Exactly ONE desktop connected: no
--targetneeded. - Multiple connected, no
--target: the call fails witherror:"ambiguous_target"(exit 1) and lists the connected names. It never guesses. A bad name returnstarget_not_found. --target allreturns{broadcast:true, results:{<name>:...}, summary:{total,ok,failed}}; a per-desktop failure is isolated to that desktop's entry.- Caveat:
pinghas 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: macOSin thetargetslisting.
Local vs relay routing
- No
--targeton 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>(orADOM_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:
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:
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:
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. |
---
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. |