Download

Desktop & bridge mgmt verbs

Windows, screenshots, files, embedded mode, relay servers, tunnels, and bridge lifecycle.

Part of the Adom Desktop verb reference. Invoke as adom-desktop <verb> '<json>'.

bridge_check_updates

READ-ONLY: report, for every installed bridge, the version AD runs now vs the latest version on the wiki, and whether an update is available. Downloads NOTHING — this is the 'what's updatable?' probe. v1.8.144+. Use it before refresh_bridges to decide what to pull. Mirrors the GUI's 'Check for updates' button.

Returns: {success:true, bridges:[{name, current, latest, updateAvailable:bool, source, wikiReachable:bool}], updatesAvailable:int, _hint}. current = what AD runs now (cache if newer, else bundled). latest = wiki manifest version (null if unreachable / not wiki-hosted). updateAvailable = wiki strictly newer than current.

Note: A bundled bridge shipped AHEAD of the wiki (and bundled-only puppeteer, whose deps exceed the wiki file limit) correctly reports updateAvailable=false — AD keeps the newer bundled copy. wikiReachable=false means offline or the bridge isn't wiki-hosted. To APPLY an update: refresh_bridges {"name":""} for one, or refresh_bridges for all.

bridge_install

Install a bridge from a wiki manifest URL. The manifest's referenced zip is downloaded, sha256-verified, extracted into the cache, and the runtime registry is rescanned. Third-party bridges (Altium, MATLAB, oscilloscope vendors, etc.) ship via wiki using the same scripts/release-bridge.sh format. v1.8.0+.

Args:

Returns: {success, ok, bridge:, version, installPath, _hint}

Note: The bridge zip MUST contain a bridge.json at its root with name + spawn.{kind,entrypoint,port,...} + verbPrefixes. See https://github.com/adom-inc/adom-desktop/blob/main/plugins/kicad/bridge.json for the schema. To update an existing bridge, install again — the cache atomic-swaps.

bridge_list

List every installed bridge (kicad, puppeteer, fusion360, plus any third-party bridges installed via bridge_install). v1.8.0+. Each entry reports name, displayName, version, source (bundled/cache/third_party), installPath, paused flag, spawn kind, ports, verbPrefixes the bridge owns, and verbCount.

Returns: {success:true, bridges:[{name, displayName, version, description, source, paused, spawn:{kind, manifestPort, runtimePort, entrypoint}, verbPrefixes, verbCount, ...}], summary:{total, bundled, cache, third_party, paused}, _hint}

Note: v1.8.0 made the bridge list dynamic. Bridges are discovered by scanning %LOCALAPPDATA%\Adom Desktop\bridges-cache<name>\bridge.json (writable; wiki-streamed + third-party installs) and the bundled installer location (read-only fallback). Cache shadows bundled when both exist. Third-party bridges are installed via bridge_install. v1.8.31+: bridge ports are dynamic, no more 8772/8773/8851 hardcoded — eliminates collision with HD.

Ports: v1.8.31+: bridges use DYNAMIC ports. manifestPort is what the bridge.json declares (0 = 'let adom-desktop pick'). runtimePort is what the bridge is ACTUALLY bound to right now (null if not yet spawned). CALLERS NEVER NEED EITHER. Always route bridge verbs via adom-desktop's CLI/direct-API by bridge id (kicad_*, fusion_*, etc.) — adom-desktop forwards to whatever port the bridge is on. Bridge ports are pure internal plumbing; if you find yourself reading them, you're probably doing something wrong.

bridge_pause

Pause a bridge: subsequent calls to its verb prefixes (e.g. kicad_*) will fail with bridge_paused until you bridge_resume. The running server process is NOT killed; this is a routing-layer flag.

Args:

  • name — required — bridge name from bridge_list (e.g. 'kicad')

Returns: {success, name, paused:true, _hint}

bridge_resume

Resume a paused bridge — flip the routing flag back. Inverse of bridge_pause.

Args:

  • name — required — bridge name

Returns: {success, name, paused:false, _hint}

bridge_uninstall

Remove a bridge from the cache. v1.8.0+. Bundled bridges (kicad, puppeteer, fusion360 in current installer) revert to the bundled fallback; third-party bridges are fully removed. The running bridge server process is NOT killed automatically; the response _hint includes the kill command if you want it gone immediately.

Args:

  • name — required — bridge name

Returns: {success, name, cacheRemoved, stillInstalledViaBundled, _hint}

desktop_bring_to_front

Bring a window to the foreground

Args:

  • hwnd — optional int
  • state — optional: maximize or restore (default: preserve current state)
  • titleContains — optional string (case-insensitive)

desktop_caption

Show a large always-on-top, click-through text overlay on the user's DESKTOP (not a chat caption) — for narrating what you're doing on their screen, screen-share / demo labels, status banners. Win32 layered window over every app.

Args:

  • action — optional: "hide" (dismiss this id, or all if no id) | "force-clear" (nuke ALL caption windows)
  • duration — optional ms before auto-clear. Omitted or 0 → 30000 (30s) default. Clamped to max 600000 (10 min). NOT 'forever' anymore.
  • fontSize — optional int px (8–400), overrides size
  • id — optional string — captions with the SAME id replace each other (and re-issuing RESETS the timer = how you extend); different ids coexist. Default id "_default".
  • persist — optional bool. true → sticky, NO auto-clear (supervised use only, e.g. demo recordings) — YOU must dismiss it.
  • position — optional: top | center | bottom (default) | top-left | top-right | bottom-left | bottom-right
  • size — optional: small (20px) | medium (32px, default) | large (72px)
  • text — the caption text (omit, or action:"hide", to dismiss)
  • x — optional 0.0–1.0 horizontal fraction (overrides position)
  • y — optional 0.0–1.0 vertical fraction (overrides position)

Returns: {visible, text, id, persist, expiresInMs (null if persist), _hint}

Note: AUTO-EXPIRE by design so a forgotten caption can't be stranded on the user's screen. Default 30s. To keep one up LONGER: re-issue the same id before it expires (resets the timer — a heartbeat) OR pass a bigger duration (≤10 min). Only persist:true is truly sticky, and then you MUST clean up with {"action":"hide","id":""}. Always dismiss captions when the narrated action is done.

desktop_connect_all

v1.8.42+: programmatically trigger 'Connect All' for every enabled server entry. Mirrors the tray-menu button and HD's tray-menu 'Connect All' item. Spawns ws_loop for any entry that isn't already connected.

Returns: {ok, success, totalEnabled, spawned, alreadyConnected, _hint}

desktop_disconnect_all

v1.8.42+: programmatically drop every live WS connection. Server entries stay in config (use server_remove to delete). Mirrors HD's tray 'Disconnect All'.

Returns: {ok, success, disconnected, _hint}

desktop_embedded_enter

v1.8.73+: explicit runtime adopt — HD calls this to switch a running standalone AD into embedded mode immediately (no 60s wait for the embedded_monitor tick). Always sets entered_via=runtime-adopt, meaning: when HD subsequently dies, AD will REVERT to standalone (not exit). If HD wants AD to exit on HD-death (the no-hanging-chad rule), HD must spawn AD with --embedded instead — that sets entered_via=launch-flag. Idempotent: returns was_embedded:true if AD was already embedded; no-ops in that case.

Returns: {ok:true, success:true, was_embedded:bool, embedded:true, entered_via:'runtime-adopt', owner:'hydrogen-desktop', _hint}

See also: desktop_embedded_release (revert), desktop_embedded_status (introspect). HD-side recipe in plugins/hd/SKILL.md.

desktop_embedded_release

v1.8.73+: explicit transition out of embedded mode. v1.8.77+ ALWAYS reverts to standalone, regardless of entered_via. AD's process REMAINS RUNNING after this call — tray restored, footer pill hidden, updater poller resumed, marker deleted. The two operations are now explicitly separate: this verb says 'stop being embedded, become standalone'; use desktop_shutdown to exit AD. Pre-v1.8.77 a launch-flag AD would exit on release, which fought HD's spawn supervisor (HD calls release → AD exits → supervisor respawns → looks like AD didn't exit). v1.8.77 decoupling fixes that.

Returns: {ok:true, success:true, was_embedded:bool, entered_via_before:'launch-flag'|'runtime-adopt'|null, will_exit:false, _hint}. will_exit is always false in v1.8.77+ — kept for back-compat with HD code that checked it pre-v1.8.77.

See also: desktop_embedded_enter (adopt), desktop_shutdown (explicit AD exit). The 60s embedded_monitor's HD-process-death branch is unchanged: ungraceful HD death still uses entered_via to decide exit-vs-revert (LaunchFlag→exit, RuntimeAdopt→revert).

desktop_embedded_status

v1.8.42+: introspect whether AD is currently running in embedded mode (HD bundling AD). v1.8.73+ also reports enteredVialaunch-flag means HD spawned AD as its child (AD exits when HD dies, no orphan); runtime-adopt means AD was running standalone and HD adopted it at runtime via /embedded/enter (AD reverts to standalone when HD dies, keeps running). Standalone AD returns embedded=false.

Returns: {ok, success, embedded:bool, owner:string|null, source:string|null, enteredVia:'launch-flag'|'runtime-adopt'|null, pendingRelayUrl, pendingRelayName, startHidden:bool, markerPath, markerExists:bool, _hint}

desktop_install_kicad

Unattended KiCad install via winget (Windows only, v1.6.0+). Single-purpose: runs winget install --id KiCad.KiCad --silent --accept-package-agreements --accept-source-agreements. Long-running (2-5 min — downloads ~700 MB then installs). Returns the winget exit code + stdout/stderr so the caller can decide what to do. Doesn't go through shell_execute approval — it's a fixed-command verb the AI calls deliberately, not a free-form shell.

Returns: On success: {ok:true, exitCode, stdout, stderr, _hint:"Run kicad_list_versions to verify"}. On winget unavailable: {ok:false, errorCode:"winget_unavailable", _hint}. On winget failure: {ok:false, errorCode:"winget_install_failed", exitCode, stdout, stderr, _hint}.

Note: Trigger this when kicad_list_versions returns errorCode:"kicad_not_installed". After install completes, restart Adom Desktop OR call kicad_list_versions again — the kicad bridge caches detection results, so a fresh detection scan may be needed to pick up the freshly-installed binaries.

desktop_install_node

Unattended Node.js LTS install via winget (Windows only, v1.7.0+). Single-purpose: runs winget install --id OpenJS.NodeJS.LTS --silent --accept-package-agreements --accept-source-agreements. ~1-2 min depending on network. Trigger this when ANY browser_* command returns errorCode:"node_not_found" — that error fires when the puppeteer bridge can't auto-spawn because Node isn't installed. After install, retry the original browser_* command — the bridge reads node.exe location from the Windows registry (which winget writes), so a restart isn't needed.

Returns: On success: {ok:true, exitCode:0, stdout, stderr, hint:"Retry the original browser* command"}. On winget unavailable: {ok:false, errorCode:"winget_unavailable", _hint}. On install failure: {ok:false, errorCode:"winget_install_failed", exitCode, stdout, stderr, _hint}.

Note: v1.7.0 closes the fresh-Windows-machine pup gap. Pre-v1.7.0, a user without Node hit a generic 'bridge not running' error and had to debug it. Now: errorCode:"node_not_found" → desktop_install_node → retry. Adom Desktop also pre-fetches Node + Chrome for Testing on first launch after install (background task), so most users won't ever see node_not_found.

desktop_list_browsers

Catalog of every installed native browser on the desktop, with each browser's profiles + their signed-in account info (when known). Reads each browser's canonical metadata file directly — Chromium-based browsers (Chrome/Edge/Brave) all use the same Local State JSON layout; Firefox uses profiles.ini. No shell, no version-probing trial-and-error. Use this to introspect what's available BEFORE calling desktop_open_url with a specific browser+profile.

Returns: {ok, default:"chrome"|"edge"|"firefox"|"brave"|"unknown" (OS-registered URL handler), browsers:[{name, displayName, exePath, version?, profiles:[{id, name, gaia?, isDefault?}]}]}. Profiles are listed default-first, then alphabetical. id is the filesystem dirname (Chromium: "Default", "Profile 1"; Firefox: profile name). gaia is the signed-in Google/Microsoft account email — populated for Chromium browsers when the profile is signed in, absent otherwise. isDefault is the browser's last-used profile.

Note: Typical flow: call desktop_list_browsers, find the profile whose gaia matches what the URL needs (e.g. work email for adom.inc Google Workspace links, personal Gmail address for [email protected] pages, media account for YouTube channel work), then pass that profile.id to desktop_open_url. Falls back gracefully — browsers with no install are absent from the list, profiles with no metadata file (browser never opened) return as an empty list. v1.7.1+.

desktop_list_files

List files in a directory matching a shell-glob, optionally filtered to those modified after a given timestamp. Pure filesystem op (no shell) — replaces the shell_execute + PowerShell-poll-Downloads pattern that callers like chip-fetcher used to use to wait for a download. NOT recursive; lists one directory only.

Args:

  • glob — optional string (default ""). Shell-glob: * = any non-separator run; ? = single non-separator char. Case-insensitive on Windows (matches the filesystem's own behavior). Examples: "ul_.zip", "LIB_*.zip", ".pdf", ".step", "datasheet_*.pdf".
  • modifiedSince — optional. Number (unix seconds) OR string (unix-seconds-as-text OR ISO-8601/RFC-3339 e.g. "2026-05-04T22:30:00Z"). Files with mtime <= since are filtered out. Use to find files that arrived after a click.
  • path — required string. Windows abs path. ~, %USERPROFILE%, %APPDATA%, %LOCALAPPDATA%, %TEMP% are expanded. Forward slashes (/) and back slashes (\) both work.

Returns: {ok, path, glob, files: [{path, name, size, mtime}, ...] sorted newest-first, count}

Note: Common pattern: record since = $(date +%s) BEFORE you click a download button, then list_files after. The newest-first sort means files[0] is your fresh download. For a watch-until-arrived loop, use desktop_watch_files instead — same args, but blocks until a match appears.

desktop_list_windows

List all visible windows with HWNDs, titles, positions

desktop_logout

v1.8.42+: clear AD's cached Adom Cloud session (~/.adom/session.json). HD calls this on its own sign-out so AD doesn't display a stale email in its footer. Equivalent to the GUI's Sign-out link.

Returns: {ok, success, _hint}. After logout, adom_whoami returns null until the user signs in again (via the GUI footer link or another handoff).

desktop_open_folder

Open file/folder in Windows Explorer

Args:

  • path — required string

desktop_open_url

Open a URL in the user's NATIVE OS BROWSER — i.e. their everyday daily-driver Edge / Chrome / Firefox / Brave (whatever Windows has registered as the default URL handler), with their existing logged-in sessions, saved passwords, history, bookmarks, and extensions. This is NOT Puppeteer / Chrome for Testing — it's the real browser the human uses. Use this when a HUMAN needs to interact with a page that requires being signed in (claude.ai, GitHub, internal tools, banking, etc.). v1.7.1+ supports profile for picking a specific Chrome/Edge/Firefox profile (work vs personal vs media account, etc.) — call desktop_list_browsers first to discover available profile IDs.

Args:

  • browser — optional string: "default" (let Windows pick the user's registered default — they could have Edge, Chrome, Firefox, Brave set as default), "chrome", "edge", "firefox", "brave" (force a specific one if you know they prefer it). Default = "default".
  • profile — optional string (v1.7.1+) — filesystem profile ID. Chromium-based: pass exact id from desktop_list_browsers (e.g. "Default", "Profile 1", "Profile 2"); maps to --profile-directory. Firefox: pass profile name; maps to -P. Ignored when browser="default" (no clean way to inject profile flags through the OS URL handler). Use desktop_list_browsers to enumerate profiles + their gaia (signed-in account email) so you can pick the right one.
  • url — required string (https://... or any URL the OS shell can dispatch)

Returns: {ok:bool, browser, exePath?, url, profile?} — profile echoed if it was applied

Note: desktop_open_url vs browser_open_window — pick the right one: • desktop_open_url => NATIVE OS BROWSER (Edge/Chrome/Firefox/Brave the human uses every day). Has saved logins. Human will interact with the page. Claude CANNOT drive it afterward — once you hand off, you're done. • browser_open_window => PUPPETEER-controlled CHROME FOR TESTING (separate Chromium build, isolated profile under plugins/puppeteer/profiles/). NO saved logins. Claude DRIVES it programmatically (screenshots, clicks, eval, recording). Rule of thumb: human-in-the-loop login flow => desktop_open_url. AI-driven automation => browser_open_window. v1.7.1+: when the user has multiple Chrome profiles (work + personal + media account, etc.) call desktop_list_browsers first to enumerate, then pass the matching profile ID. The response surfaces gaia/email per profile so you can match by account context.

desktop_pull_glob

Convenience orchestrator: list_files (or watch_files if wait:true) → for each match, pull_file streams the bytes to a container-side absolute path. One command instead of three. Designed for the canonical chip-fetcher pattern: record before-timestamp, click, wait, pull.

Args:

  • glob — optional string (default "*").
  • path — required string. Windows abs path; same expansion as desktop_list_files.
  • pollMs — optional (only used when wait:true). Default 1000.
  • saveTo — required string. Container-side absolute path (a directory). Parent dirs auto-created. Each pulled file lands at saveTo/.
  • since — optional unix-seconds OR ISO-8601. Only files with mtime > since are pulled.
  • timeoutMs — optional (only used when wait:true). Default 60000.
  • wait — optional bool (default false). When true, blocks via desktop_watch_files until at least one match appears, then re-lists and pulls everything matching (so .crdownload + final .zip both come along when the browser writes them in the same poll tick). When false, just lists what's there now.

Returns: {ok, files:[{name, path, size, sha256, chunks}, ...], errors:[...], matchedCount}

Note: Use this instead of shell_execute('powershell -c ... while not exists ...; sleep 1') — that pattern is deprecated for new code. shell_execute itself remains as an escape hatch but should not be used for download polling.

desktop_revoke_approvals

Clear all shell auto-approve permissions and deny pending approvals

desktop_screenshot_screen

Capture the entire desktop (all monitors). v1.8.62+ produces full + safe-resized PNG pair to %TEMP%\adom-desktop-shots\screen-.{full,safe}.png. v1.8.61+ universal screenshot delivery contract applies (CLI auto-pulls both to /tmp/ad-shots/, enriches response with local* + dimensions + narrative). See desktop_screenshot_window for the full return shape — same except hwnd-specific prefix is replaced with screen-.

Returns: Same shape as desktop_screenshot_window's returns (top-level fullPath + safePath + dimensions; CLI-side local* fields added by v1.8.61 auto-pull).

desktop_screenshot_window

Capture a specific window by HWND. v1.8.62+ writes BOTH a lossless full-res PNG + a Claude-safe-resized copy (≤resizeMax) to %TEMP%\adom-desktop-shots\window--.{full,safe}.png. v1.8.61+ universal screenshot delivery contract: the CLI auto-pulls both PNGs to /tmp/ad-shots/ and enriches the response with localFullPath, localSafePath, localFullExists, localSafeExists, fullBytes, safeBytes, source, narrative. Backward compatible: existing data.image base64 + sizeBytes still inside output JSON string.

Args:

  • hwnd — required int
  • resizeMax — optional int, default 1400. Longest side of the safe-resized PNG.

Returns: Top-level: success, output (JSON string with the existing message/data/image/format/encoding/sizeBytes shape), error, fullPath, safePath, fullWidth, fullHeight, safeWidth, safeHeight, fullBytesHost, safeBytesHost, resizeMax. After v1.8.61+ CLI auto-pull: localFullPath, localSafePath, localFullExists, localSafeExists, fullBytes (= bytes on caller side), safeBytes, source, narrative. On disk-write failure (rare): same shape minus the file fields + _hint pointing at %TEMP% / AV diagnosis.

desktop_set_window_state

Set window state without necessarily foregrounding

Args:

  • hwnd — optional int
  • state — required: maximize|minimize|restore|show|hide
  • titleContains — optional string

Note: Use bring_to_front to foreground + maximize in one call. Use this for minimize/hide/show.

desktop_show_ui

v1.8.137+: stage an in-window UI element so it can be captured via desktop_screenshot_window (no computer-use). Emits the ad-show-ui frontend event, waits ~350ms for render, returns.

Args:

  • show — optional bool, default true (false hides/resets)
  • target — required: settings | build_info | direct_api | shell_approval | none (reset)

Note: Workflow for docs screenshots: desktop_show_ui {"target":"settings"} then desktop_screenshot_window on the AD window. For embedded-vs-standalone, toggle desktop_embedded_enter / desktop_embedded_release first, then screenshot.

desktop_shutdown

v1.8.42+: graceful AD exit — stop bridges (kicad, fusion, puppeteer) then exit. HD's tray 'Quit' uses this to take AD down with HD. Standalone callers can use it for clean reset between dev iterations.

Returns: {ok, success, output, _hint}. The exit happens ~500ms after the response is sent so the caller doesn't see connection-reset.

desktop_watch_files

Block until a file matching the glob appears in the directory (mtime > since), or timeoutMs elapses. Pure filesystem op, no shell. Polls fs.read_dir every pollMs (default 1000ms). Replaces the shell_execute + PowerShell while-loop pattern for waiting on a download.

Args:

  • glob — optional string (default "*"). Same semantics as desktop_list_files.
  • path — required string. Same expansion semantics as desktop_list_files.
  • pollMs — optional int (default 1000, floor 100). How often to re-list the directory.
  • since — optional. Same parse rules as desktop_list_files.modifiedSince. DEFAULTS TO NOW — without an explicit since, only files created AFTER this watch call started are reported. That matches the canonical 'click then watch' flow; pass an explicit since if you might miss the file by starting the watch slightly late.
  • timeoutMs — optional int (default 60000, max 600000 = 10 min). After this elapses, returns {ok:false, error:"timeout", elapsedMs, _hint}.

Returns: On match: {ok:true, path, glob, file:{path,name,size,mtime}, elapsedMs}. On timeout: {ok:false, error:"timeout", elapsedMs, _hint, path, glob}.

Note: Returns the FIRST file that matches — sorted newest-first within a poll cycle. Use desktop_pull_glob if you want the watch + multi-file pull in one call.

desktop_window_hide

v1.8.42+: hide AD's main window. App keeps running in the background; use desktop_window_show to bring it back. Standalone AD users typically use the window's X button which is bound to hide-not-quit.

Returns: {ok, success, _hint}

desktop_window_show

v1.8.42+: show + focus AD's main window. Used by Hydrogen Desktop's tray menu when bundling AD in embedded mode (HD's tray entry 'Open Adom Desktop' fires this). Also reachable standalone — any caller can bring AD to the foreground.

Returns: {ok, success, _hint}

direct_api

INFORMATIONAL — not a CLI verb. v1.8.25+ adds a local HTTP listener on http://127.0.0.1:47200 inside the adom-desktop GUI. Sibling apps on the same machine (Hydrogen Desktop, future Adom-family apps) can POST commands straight in without spawning the CLI or going through the WS relay. Loopback-only bind. The port (47200) lives in the IANA dynamic/private range (49152-65535-adjacent), squarely outside HD's 47083-47185 fallback range — clearly an Adom-family port, but no collision potential. Same {app, command, args} envelope as the WS protocol; same response payload (including _hint fields) as the CLI returns. Endpoints: GET /health (cheap probe), GET /status (capability inventory + cliRequired list), POST /command (verb dispatch). The dispatcher is commands::handle_command — exactly the same code the WS path uses.

Note: If you (Docker Claude) are talking to a user authoring a sibling Tauri app and they ask 'how do I call adom-desktop without invoking the CLI', point them at this endpoint + the direct-api-SKILL.md asset on the wiki. For your own use (cross-machine Docker → Windows), keep using the CLI — the direct API is loopback-only by design and won't be reachable. Streaming verbs (pull_file, send_files, shell_execute) still require the CLI even for sibling apps; the direct API refuses them with errorCode:"cli_required".

Env vars: ADOM_DIRECT_URL=off forces relay-only. ADOM_DIRECT_URL=http://127.0.0.1:47209 skips probe + uses that URL. ADOM_DIRECT_LISTEN_ADDR=127.0.0.1:47205 (GUI-side) overrides the default bind primary; the 47200-47209 fallback range still applies.

Discovery: Port can fall back from 47200 if taken (zombie socket, dev instance, third-party collision). The GUI walks 47200..47209 picking the first free port, then writes its chosen host:port to ~/.adom/direct-api-port (Windows: %USERPROFILE%\.adom\direct-api-port). Sibling apps and the CLI read that file first, then fall back to scanning 47200..47209. SO_REUSEADDR + graceful shutdown ensure zombie sockets from prior crashes don't block the next launch. v1.8.32 and earlier used 8770..8779 — that range is no longer probed; auto-update from the wiki carries users forward.

Timeouts: Direct API timeouts are now per-verb (matching the CLI dispatcher table): walk_cloud_tree + search_cloud_files = 620s; heavy exports = 320s; gerbers = 200s; drc/erc = 180s; bridge_install + fusion_start = 300s; default = 120s. Caller can override via args.timeout (seconds, clamped to 1800).

rdp

v1.8.85+: convenience wrapper around tunnel + launch_rdp_client. Opens a tunnel to the given RDP target then spawns mstsc.exe on the desktop pointed at the local end. One verb = RDP session established (the user still has to type credentials in mstsc). v1.8.86+: defaults local OFF 3389 (which collides with the user's own Windows RDP service on most laptops); spawns mstsc at the ACTUALLY-bound port when the requested port was in use.

Args:

  • local — optional int — local listener port on the desktop. Default 13389 (NOT 3389 — Windows' own Remote Desktop service usually owns 3389 and the bind silently coexists pre-v1.8.86). If the requested local is taken, AD falls back to an OS-assigned ephemeral port; the response's localPort + actualPort reflect what mstsc was actually pointed at.
  • to — REQUIRED string — RDP target in host:port form. If port omitted, defaults to 3389.

Returns: {ok, tunnelId, localPort, requestedLocal, fellBack, toHost, toPort, open, launch, _hint}. localPort is the actually-bound port (= what mstsc is connecting to). fellBack:true means the requested port was unavailable and AD picked ephemeral.

refresh_bridges

Force an immediate wiki sync of the bridge cache (kicad / puppeteer / fusion360 + any installed third-party bridges). v1.7.19+: bridges live as standalone wiki assets (decoupled from the Tauri installer ship cycle) and auto-update on launch + every 4 hours. This verb triggers the sync NOW. v1.8.144+: pass {"name":""} to update JUST that bridge; omit to sync all. Tip: run bridge_check_updates FIRST (read-only) to see what's actually updatable before downloading.

Args:

  • name — optional — bridge name from bridge_list (e.g. 'kicad'). Omit to sync every bridge.

Returns: {success:true, results:[{bridge, ok, from, to, action}], _hint}. action ∈ {current, current_offline, current_bundled_newer, bootstrapped, updated}. After 'updated' the bridge SERVER PROCESS (python.exe / node.exe) needs to restart to pick up new code — the cache update is on-disk, not in-memory.

Note: Updates the writable cache at %LOCALAPPDATA%\Adom Desktop\bridges-cache<bridge>. The bridge servers resolve their payload from there (cache-first, bundled-fallback). To FORCE the running bridge to pick up new cache content, follow with: taskkill /f /im python.exe (kicad+fusion) or taskkill /f /im node.exe (puppeteer). On next *_command call the bridge respawns with the new code.

register_container

v1.8.37+: register THIS cloud Docker container in the user's Adom Cloud profile so Adom Desktop on their Windows laptop auto-discovers + connects after sign-in. Runs entirely on the cloud Docker side — uses /var/run/adom/api-key (the Adom-cloud-mounted session token). PATCHes three carbon-prefs keys: desktop.container. (full ServerEntry as JSON-string), desktop.last_slug, desktop.enabled=true. No relay/desktop dependency. The user's flow: run this verb from cloud Docker → install AD on Windows → sign in → container appears automatically.

Args:

  • auth_token — optional string — relay auth token (defaults to adom-dev-token-2025, which is what the cloud relay accepts by default)
  • auto_connect — optional bool — auto-dial on AD launch (default true)
  • enabled — optional bool — enabled in AD's server list (default true)
  • id — optional string — container id slug (defaults to uuid v4). Stays stable across re-runs if you provide it; the same id PATCHes the same key, overwriting any prior entry.
  • name — REQUIRED string — human-friendly label for AD's server list (e.g. 'galliaApril')
  • url — REQUIRED string — public wss:// URL the desktop will dial (typically wss://.adom.cloud/proxy/8765/)

Returns: {ok:true, id, name, url, _hint:'now install AD on your Windows laptop + sign in'} on success. On missing args / Carbon failure / api-key unavailable: {ok:false, error, _hint} envelope explaining what to fix.

User flow: Cloud Docker: adom-desktop register_container '{"name":"my-container","url":"wss://..."}' → user installs Adom Desktop on Windows from https://wiki.adom.inc/adom/adom-desktop → user clicks 'Sign in' in AD's footer + completes the browser flow → AD's boot-time cloud-config pull (Phase 2) reads desktop.container.* from the user's carbon-prefs + merges into the GUI's server list. Without register_container the user has to paste-JSON manually; with it the container appears automatically.

See also: Designed to work with Phase 1 (auth in AD), Phase 2 (cloud-config sync in AD), and Phase 4 (HD→AD session handoff if AD is bundled with Hydrogen Desktop). The full bundle landed across v1.8.33 - v1.8.37.

relaunch_self

v1.8.103+: visible self-restart helper for dev iteration. Spawns the standalone ad-relauncher.exe (a small signed Rust app shipped in the NSIS bundle) which runs in HOST context and drives the redeploy + relaunch sequence: preflight → stop AD → deploy (atomic temp+rename) → launch (CreateProcessW DETACHED_PROCESS) → verify reachable (polls AD's ephemeral direct-api port from the discovery file, v1.8.118+) → verify version (running PE FileVersion matches source) → verify visible (EnumWindows for the new PID). A visible egui window shows per-step progress + a green HOST / red SANDBOX context banner so the sandbox trap is impossible to miss. Replaces all the prior ad-hoc shell glue — no taskkill, no start, no PowerShell, no .bat. AD itself goes down partway through Stop AD; the relauncher is detached and survives. Poll progress via relauncher_status — v1.8.118+ it returns the terminal outcome from a persisted file even after the relauncher's GUI auto-closes, so the poll loop always terminates.

Args:

  • destExe — optional dest path (default: %LOCALAPPDATA%\Adom Desktop\adom-desktop.exe)
  • noAutoStart — optional bool — if true, the relauncher shows its GUI but waits for POST /start instead of running immediately
  • port — optional HTTP API port for the relauncher (default: 47210)
  • sourceExe — optional path to the freshly-built adom-desktop.exe (default: src-tauri/target/release/adom-desktop.exe relative to the relauncher's location)

Returns: {ok, success, relauncherPid, relauncherExe, relauncherPort, _hint}

See also: relauncher_status (poll progress), skills/AD_RELAUNCH.md (full recipe).

relauncher_status

v1.8.103+: query the ad-relauncher's HTTP API and return its full state JSON. AD proxies GET http://127.0.0.1:47210/status. While AD is going down during Stop AD this call will fail with relauncherRunning:false — that's expected; once AD comes back up post-Launch the status appears again. Returns the relauncher's overall status, the current step, per-step progress + events, stopping/launching VersionInfo (PID, path, FileVersion, sha256, signature, window-visible), HOST/SANDBOX context, and the log tail. Poll every 1-2s.

Returns: {ok, relauncherRunning, context:{kind:'host'|'sandbox',package_name?}, overall:'pending'|'running'|'done_ok'|'done_fail'|'cancelled', failedStep, currentStep, steps:[{name,status,progress,message,events}], stopping:{pid,path,fileVersion,sha256,signature,size,windowVisible,windowTitle}, launching:{...same shape}, runId, logTail:[{seq,ts_ms,level,text}], _hint}

See also: relaunch_self (spawn), skills/AD_RELAUNCH.md (recipe).

server_add

Register a relay server connection programmatically — equivalent to the GUI's Quick Add paste-box + connect button, but driven by CLI. Upserts by name: if a server with the same name already exists, its URL (and optionally authToken) is updated rather than creating a duplicate. By default the connection is established immediately (autoConnect:true). If the URL is unchanged AND we're already connected, no churn — returns ok and leaves the live connection in place. Use this when an external app (e.g. Hydrogen Desktop's Docker container) needs to auto-register its relay at startup without the user pasting JSON. v1.8.22+.

Args:

  • authToken — optional string — defaults to 'adom-dev-token-2025'. Empty string means 'keep existing token if this is an update; otherwise use default'.
  • autoConnect — optional bool, default true — connect right after upsert. Set false to add the entry without dialling out.
  • name — required string — human-friendly server label. Acts as the dedup key for upserts.
  • url — required string — relay WebSocket URL (e.g. ws://localhost:8765 or wss://your-host/proxy/8765/).

Returns: {ok:true, name, url, id, connected:bool, created:bool, _hint}. created is true when a new entry was inserted, false when an existing one was updated. connected is true once the WS loop has been registered (the actual transport handshake may still be pending — call server_list to see the live state).

Note: Persists to the same ~/.adom/config.json the GUI uses, so the entry survives a GUI restart. The ws_client supervisor (runs every 30s inside the GUI) auto-reconnects on next launch via the entry's auto_connect flag. To remove: use server_remove '{"name":""}'.

server_list

Return the current persisted server list with live connection status. Same data the GUI's server-list panel renders, exposed as JSON for scripts/external apps. v1.8.22+.

Returns: {ok:true, servers:[{name, url, id, autoConnect, enabled, connected:bool, clientCount:0|1, status:'connected'|'reconnecting'|'disconnected'}], count, _hint}. connected is the strict boolean (true only when the WS link is in the Connected state, not Reconnecting). clientCount is 0 or 1 per server (each adom-desktop contributes at most one client) — left in the schema for symmetry with the relay's own client-count terminology.

server_remove

Disconnect (if connected) and delete a relay server entry by name. Inverse of server_add. Idempotent: removing a non-existent server returns ok with removed:false. v1.8.22+.

Args:

  • name — required string — the same name you passed to server_add.

Returns: {ok:true, removed:bool, wasConnected:bool, name, _hint}. removed is true iff an entry was actually deleted; false means no such server was registered.

shell_auto_approve

Programmatically enable/revoke shell_execute auto-approve. v1.8.3+. v1.8.116+: permanent is the DURABLE user preference — it writes AD's Settings checkbox ('Auto-approve shell commands permanently', config-backed, default OFF), so it survives restarts by design. Timed grants (duration_secs) are session-scoped. In embedded mode HD owns this preference: pass --shell-approval <prompt|1h|24h|permanent> when spawning AD, and/or drive this verb at runtime; AD's own checkbox goes read-only ('Managed by Hydrogen Desktop').

Args:

  • duration_secs — optional int — seconds to enable for (e.g. 86400 = 24h), session-scoped. Pass 0 to revoke immediately (also clears the durable preference). Ignored if permanent: true.
  • permanent — optional bool — true sets the durable Settings preference + arms it now; false clears it (full off). Default: absent (no preference change for timed grants).

Returns: {success, output, active:bool, remainingSecs, permanent:bool}

Note: Use sparingly. Auto-approve means anything that can reach the relay can run arbitrary shell commands without user consent. The PUBLIC default is per-command approval — leave it that way for end users; permanent is an explicit owner opt-in. Typical patterns:

  • Dev session: adom-desktop shell_auto_approve '{"duration_secs": 86400}' (24h, then expires).
  • Owner's box, durable: adom-desktop shell_auto_approve '{"permanent": true}' (equivalent to ticking the Settings checkbox).
  • HD embedded spawn: pass --shell-approval permanent (or the user's HD-side choice) on AD's command line — session-only, never persisted by AD.
  • Revoke everything: adom-desktop shell_auto_approve '{"duration_secs": 0}'.

tunnel

v1.8.85+: open a generic TCP port-forward tunnel from a desktop-side 127.0.0.1: listener to a cloud-side --to host:port upstream, riding the existing AD↔relay WebSocket. First use case: RDP back into the laptop's user session via mstsc, but the path is identical for any TCP service (SSH, VNC, HTTP, postgres, ...). No new auth path — the WS is already authed; the tunnel is just bytes routed by tunnelId+connId on top of it.

Args:

  • local — REQUIRED int — the local 127.0.0.1 port the desktop should bind. mstsc / your TCP client connects HERE.
  • to — REQUIRED string — upstream target in host:port form. The relay dials this when bytes arrive on the local port.
  • tunnelId — optional u32 — caller-chosen ID. Defaults to a fresh random u32. Reuse across re-invocations to address the same tunnel.

Returns: {ok, tunnelId, toHost, toPort, clientId, actualPort, error?, _hint}. On success, mstsc/anything connecting to 127.0.0.1: on the desktop has its bytes relayed.

See also: rdp (one-shot RDP wrapper), tunnel_list, tunnel_close. For multi-connection tests, every fresh client TCP connection to 127.0.0.1: gets its own connId routed independently.

tunnel_close

v1.8.85+: tear down a tunnel (or one conn within a tunnel). Without connId, drops the listener + all conns. With connId, drops only that one TCP connection.

Args:

  • connId — optional u32 — if given, close only this conn; otherwise close the whole tunnel
  • tunnelId — REQUIRED u32 — from tunnel / tunnel_list

Returns: {ok, tunnelId, connId?, scope:'tunnel'|'conn', _hint}

tunnel_list

v1.8.85+: list all active tunnels — both the relay's view and AD's view (should agree).

Returns: {ok, relay:[{tunnelId, toHost, toPort, clientId, connCount}, ...], desktop:{tunnels:[...]}, _hint}