app
Adom Bridge
Public Made by Adomby adom
Adom Bridge unleashes your AI onto your full PC: full power, full safety. The AI breaks out of the container it runs in and onto your real machine, managing and running your entire laptop as you: launch apps, move files, drive any window, control KiCad, Fusion 360 or your real signed-in browser. Works with any AI, cloud or local (Claude Desktop, Claude Code, Codex), no Hydrogen required. Bridge and its bridges are an operating system for AI, with managed Node and Python runtimes, and a human-onl
← Commit history
SKILL.md+5−6@@ -83,12 +83,11 @@ don't exist or miss ones that do. own HTTP API on `:47210`. Don't run shell glue for AD relaunches; use this. Full recipe: [`skills/AD_RELAUNCH.md`](AD_RELAUNCH.md). -- **Auto-update via wiki manifest** (v1.3.39+, V2-first since v1.8.111).- AD polls a `version.json` manifest every 4h + on launch. V2-first- fallback chain: tries `git-wiki-ktqxite5iglh.adom.cloud` (V2 git-wiki),- falls back to `wiki-ufypy5dpx93o.adom.cloud` (V1). Dead-end guard probes the+- **Auto-update via wiki manifest** (v1.3.39+).+ AD polls a `version.json` manifest every 4h + on launch from+ `wiki.adom.inc`. Dead-end guard probes the `installer_url` before flipping `update_available` (HEAD + Range-GET- fallback for V2's HEAD-404 bug). Footer banner Install Now / Later;+ fallback). Footer banner Install Now / Later; user picks `prompt` / `auto` / `off` in Settings. SHA256-verified silent NSIS install + autostart relaunch. @@ -1084,7 +1083,7 @@ adom-desktop watch '{"command":"fusion_walk_cloud_tree","args":{...},"interval": - `fusion_search_cloud_files` -- **Long-running.** Recursive substring search on file names across cloud folders. v1.0.2+ of the add-in adds: per-folder timeout, doEvents() every 50 files, per-file try/except, iterative BFS, and a stale-lock watchdog — Fusion stays responsive throughout (no "Not Responding" freeze). - **Args (no hard upper caps in v1.0.2+):** `query` (required, substring, case-insensitive), `projectName` (optional, defaults to active), `folderPath` (optional starting subfolder — **narrow with this**), `recursive` (default false), `maxDepth` (default 2), `maxFolders` (default 10), `maxResults` (default 20), `folderTimeout` (default 30s), `searchTimeout` (default 120s), `timeout` (HTTP envelope, default 620s). - **Before calling:** if the user knows roughly where the file is (e.g. customer-named folder), **ask them**. Naming a subfolder cuts 5-40 min searches down to ~10s.- - **Why slow:** Autodesk's free Fusion 360 Python API has no indexed file-search endpoint. We walk the Data API one folder at a time — each = one HTTP round-trip to Autodesk. **This is an Autodesk API limitation, not adom-desktop's.** When telling the user the search is taking a while, attribute it to Autodesk / Fusion 360, not to adom-desktop's bridge. Autodesk's PAID Autodesk Platform Services (APS, formerly Forge) Data Management API has indexed search; teams with APS credentials can fork the bridge at `plugins/fusion360/addin/AdomBridge/` to add an `aps_search` verb and PR it back to https://wiki-ufypy5dpx93o.adom.cloud/apps/adom-desktop — the community benefits.+ - **Why slow:** Autodesk's free Fusion 360 Python API has no indexed file-search endpoint. We walk the Data API one folder at a time — each = one HTTP round-trip to Autodesk. **This is an Autodesk API limitation, not adom-desktop's.** When telling the user the search is taking a while, attribute it to Autodesk / Fusion 360, not to adom-desktop's bridge. Autodesk's PAID Autodesk Platform Services (APS, formerly Forge) Data Management API has indexed search; teams with APS credentials can fork the bridge at `plugins/fusion360/addin/AdomBridge/` to add an `aps_search` verb and PR it back to https://wiki.adom.inc/apps/adom-desktop — the community benefits. - **Interpreting results (critical for no-false-negatives):** the response contains `searchComplete` (bool), `foldersSkipped`, `filesSkipped`, `truncated`, `folderLimitReached`, `searchTimedOut`. `searchComplete:true` ONLY when **all five** are clean — that's the confidence flag. If `searchComplete:false`, the search hit a cap before exhausting the scope; **do NOT tell the user "file not found"** — re-run with broader caps OR narrower `folderPath`. - **Case sensitivity:** fully case-insensitive both directions (query and file names lowercased). Substring match, not whole-word — `"cosm"` matches `COSMIIC`, `COSMOCOIL`, etc. - **Cost feedback in response:** `costAnalysis: {elapsedSeconds, foldersPerSecond, estimatedSecondsPer100Folders}` so the AI can budget the next search realistically.
package.json+1−2@@ -1,6 +1,6 @@ { "slug": "adom-desktop",- "version": "1.8.137",+ "version": "1.8.148", "type": "app", "visibility": "public", "description": "Laptop bridge: file transfer, screenshots, notifications, KiCad + Fusion 360 control, real-Chrome (Puppeteer) automation, and Hydrogen Desktop proxy. One install gives Claude every desktop verb it needs.",@@ -29,7 +29,6 @@ "SKILL.md", "README.md", "package.json",- "page.json", "install.sh", "uninstall.sh", "skills/**",
page.json+41−10@@ -1,16 +1,47 @@ { "slug": "adom-desktop",- "title": "Adom Desktop",+ "version": "1.8.148", "type": "app",- "brief": "Laptop bridge: screenshots, file transfer, notifications, KiCad + Fusion 360 control, real-Chrome (pup) automation. One install gives Claude the main + pup + kicad + fusion skills.",- "visibility": {- "public": true+ "visibility": "public",+ "description": "Laptop bridge: file transfer, screenshots, notifications, KiCad + Fusion 360 control, real-Chrome (Puppeteer) automation, and Hydrogen Desktop proxy. One install gives Claude every desktop verb it needs.",+ "keywords": [+ "adom-desktop",+ "tauri",+ "claude-code",+ "bridge",+ "kicad",+ "fusion360",+ "puppeteer",+ "chrome",+ "websocket",+ "cli",+ "screenshot",+ "desktop-automation"+ ],+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh" },- "hero": {- "type": "image",- "path": "hero-billboard-v2.png"+ "install": {+ "binary_name": "adom-desktop" },- "author_name": "John Lauer",- "updated_at": "2026-06-17T11:07:44.428Z",- "content": "# Adom Desktop\n\nA Tauri v2 desktop application for Windows that bridges Adom's cloud AI tools to local desktop applications: KiCad, Fusion 360, real Chrome (Puppeteer), native shell, file transfer, screenshots, notifications — plus a Hydrogen Desktop proxy. One install gives Claude every desktop verb it needs.\n\n## Install (in an Adom container)\n\n```\nadompkg install adom-desktop\n```\n\nThat installs the **container-side** pieces:\n\n- the `adom-desktop` CLI binary (downloaded from this page's blob store, SHA256-verified against the bundled `version.json`)\n- the Claude skill bundle into `~/.claude/skills/`:\n\n| Skill | What Claude learns |\n|---|---|\n| `adom-desktop` | the main verb catalog — screenshots, files, shell, windows, notifications |\n| `adom-desktop-pup` | real-Chrome (Puppeteer) automation: sessions, tabs, eval, recording |\n| `adom-desktop-kicad` | KiCad control: open/edit schematics + PCBs, exports, 3D viewer |\n| `adom-desktop-fusion` | Fusion 360 control: designs, exports, BOM/CPL |\n| `adom-desktop-direct-api` | the loopback HTTP API for sibling apps |\n| `adom-desktop-installer` | how to drive the Windows installer (silent flags, logs) |\n| `adom-desktop-discovery` | how a fresh container finds + refreshes the CLI |\n\n## What it looks like\n\nThe desktop app, connected to two containers, with the bridge bar and live activity log:\n\n\n\nDriving KiCad end-to-end from the container — symbol, footprint, 3D model, schematic, board, gerbers:\n\n\n\nThe **laptop-side GUI** installs separately on the user's Windows machine — a signed NSIS installer served from this page (the package manifest's `assets` map / `version.json` carry the exact URL + SHA256; Hydrogen Desktop's setup installs it automatically). Once installed, the GUI auto-updates itself from the wiki's `version.json` manifest (prompt by default, every 4h + on launch).\n\n## How it fits together\n\n```\nAdom container (this package) User's laptop\n┌──────────────────────────┐ ┌──────────────────────────┐\n│ adom-desktop CLI + skills│ ⇆ relay ⇆ │ Adom Desktop GUI (Tauri) │\n│ (Claude drives verbs) │ │ KiCad · Fusion · Chrome │\n└──────────────────────────┘ │ shell · files · screens │\n └──────────────────────────┘\n```\n\nThe CLI talks to the user's GUI over the Adom relay (or the direct HTTP API when co-resident). Verbs: `desktop_*`, `browser_*` (pup), `kicad_*`, `fusion_*`, `hd_*`, `shell_execute`, file transfer, and more — run `adom-desktop help` after install.\n\n\n## Versioning\n\nThis adompkg version tracks the Adom Desktop release version (`X.Y.Z`). The package itself is a small scaffold; the multi-MB binaries live as blobs on this page and are fetched at install time with SHA256 verification — the package manifest's `assets` map carries server-computed checksums per platform.\n\n- Source: <https://github.com/adom-inc/adom-desktop> (private during beta)\n\n\n\n## Command reference\n\nEvery verb and subcommand `adom-desktop` supports — **210 across 12 categories** — inlined below (the same names are the dispatch keys the cloud AI uses over the relay). Invoke as `adom-desktop <verb> '<json args>'`; bridge verbs (`kicad_*`, `fusion_*`, `browser_*`, `hd_*`) auto-spawn their bridge on first call. Every *failing* response also carries a `_hint` telling you the next step — surface it verbatim. This list is generated from the live help-map; run `adom-desktop commands` for the machine-readable source.\n\n**Categories:** [Connectivity](#connectivity) · [Setup](#setup) · [Shell & process](#shell-process) · [File transfer](#file-transfer) · [Desktop & bridge mgmt](#desktop-bridge-mgmt) · [Screen recording](#screen-recording) · [Notifications](#notifications) · [Project watch](#project-watch) · [Browser (Puppeteer)](#browser-puppeteer) · [KiCad bridge](#kicad-bridge) · [Fusion 360 bridge](#fusion-360-bridge) · [Hydrogen Desktop](#hydrogen-desktop)\n\n### Connectivity\n\n_Liveness, status, multi-AD targeting._ (5 verbs)\n\n#### `health`\n\nCheck if the relay server HTTP API is reachable\n\n**Returns:** Relay server health status\n\n\n#### `kick_all`\n\nForce-disconnect all WebSocket clients (they auto-reconnect in ~5s)\n\n**Note:** Use when connections are stale or duplicated\n\n\n#### `ping`\n\nRound-trip connectivity test (5s timeout)\n\n**Args:**\n- `message` — optional string, echoed back\n\n**Returns:** echo, roundTripMs, status\n\n**Note:** Always run this first to verify desktop connection\n\n\n#### `status`\n\nQuery connected clients, capabilities, and app installation status\n\n**Returns:** clients count, appName, hostname, capabilities[], desktop app status (kicad/fusion/browser installed/running)\n\n\n#### `targets`\n\nv1.8.133+: list the Adom Desktops connected to this relay so you can address one by name. Each AD has a name (its --client-name / Settings value, defaulting to the OS hostname). Use this to discover names, then run any verb with the global --target <name> selector, or --target all to fan out to every AD.\n\n**Returns:** {count, targets:[{name, hostname, version, os, connectedAt}], _hint}\n\n**Note:** See _targeting_note at the top of this map for how --target works.\n\n\n### Setup\n\n_One-time desktop setup._ (1 verbs)\n\n#### `setup_desktop`\n\nGenerate installer URL and server config JSON for first-time desktop app setup\n\n**Returns:** installer_url, server_config JSON, steps[]\n\n\n### Shell & process\n\n_Run shell commands, scripts, WSL, and launch/locate apps on the laptop._ (6 verbs)\n\n#### `find_exe`\n\nFind an executable without launching it. Searches: (1) absolute path as-is, (2) glob expansion for webdeploy-style version directories, (3) Start Menu .lnk targets, (4) PATH. Returns the resolved absolute path or an error.\n\n**Args:**\n- `name` — required: bare name ('Fusion360.exe'), glob ('.../webdeploy/production/*/Fusion360.exe'), or full path\n- `searchPaths` — optional array of extra dirs to scan\n\n**Returns:** path, source (absolute|glob|startmenu|path)\n\n\n#### `args`\n\n\n#### `run_script`\n\nv1.8.46+: Execute a base64-encoded script via a chosen interpreter (cmd / powershell / bash). The script bytes never touch a shell-string parser, so NO quoting/escaping survives to be mangled by cmd.exe, PowerShell's parser quirks, or MSYS path conversion — caller never hand-escapes anything. Host-side: cmd / powershell get a temp file (.cmd / .ps1); bash gets the script via stdin (no temp file). Output decoded as UTF-8 (UTF-16LE detection for PowerShell 5.1 output). Returns stdout/stderr/exitCode and cleans up any temp file.\n\n**Args:**\n- `allowPopupRisk` — optional bool (v1.8.117+) — bypass the popup-risk pre-flight on cmd scripts. AD refuses (errorCode `popup_risk`, failedAt `popup_scan`, scriptLine N) any `start <target>` line whose target doesn't resolve — that flashes a modal 'Windows cannot find' dialog on the user's desktop. powershell/bash scripts aren't scanned (no ShellExecute popup failure mode).\n- `interpreter` — required string: 'cmd' | 'powershell' | 'bash'. PowerShell prefers pwsh (PS 7+) if on PATH, falls back to powershell.exe (5.1). Bash uses whichever is first on PATH — on Windows that's typically WSL2's System32\\bash.exe (the intended interpreter; stdin piping means no path-form mismatch). For explicit distro / user control on WSL, use wsl_exec.\n- `scriptB64` — required string: base64-encoded script content (no length limit beyond practical WS message size — keep under a few MB for sanity).\n- `timeoutSeconds` — optional int (default 60). The CLI's WS timeout is auto-sized to scriptTimeout + 15s margin.\n\n**Returns:** On success: {success:true, ok:true, stdout, stderr, exitCode, durationMs, interpreter, [_hint?]}. On failure: {success:false, ok:false, stdout:'', stderr:'', exitCode:-1, error, failedAt, _hint}. failedAt is one of: 'args' (interpreter unknown / scriptB64 empty), 'decode' (base64 not parseable), 'tempfile' (cmd/powershell temp-file write failed — see tempPath in payload), 'spawn' (interpreter exe couldn't launch — see spawn_failure_hint output), 'timeout' (script ran longer than timeoutSeconds). EVERY failure path carries a _hint with the specific recovery recipe — READ the hint, don't guess. **v1.8.60+ ADVISORY**: success responses may also include an optional `_hint` field when the script looks like a direct HD build (raw `cargo`/`pnpm` against `hydrogen-desktop`/`crates/hd-*` paths). The hint is additive — exit code / stdout / stderr are UNCHANGED — and points at the dedicated `hd_build_*` / `hd_ship` verbs.\n\n**Note:** Requires shell auto-approve to be active — same trust level as shell_execute. Enable with `shell_auto_approve '{\"duration_secs\": 86400}'` (24h) or `'{\"permanent\": true}'` (no expiry). Embedded mode defaults to permanent. Refuses cleanly with an actionable error if not approved (no per-call modal — these are programmatic verbs, not interactive).\n\n\n#### `shell_execute`\n\nExecute a shell command on the user's desktop (requires user approval). Escape hatch for things that don't have a structured verb yet — most common operations are covered by desktop_install_node / desktop_install_kicad / desktop_list_files / desktop_watch_files / desktop_list_browsers etc., which DON'T require per-call approval. Use shell_execute only when no structured verb exists for the task.\n\n**Args:**\n- `allowPopupRisk` — optional bool (v1.8.117+) — bypass the popup-risk pre-flight. AD refuses (errorCode `popup_risk`) any `start <target>` whose target doesn't resolve, because that flashes a modal 'Windows cannot find' dialog on the user's desktop. Verify targets first (find_exe / launch / desktop_open_url); only pass true when the command creates its own target and the scan misjudged it.\n- `command` — required string — full shell command (Windows: runs through `cmd /c`; macOS/Linux: through `sh -c`)\n- `cwd` — optional working directory\n- `timeoutSeconds` — optional int (default 30, max effectively unbounded — pass whatever your task needs). v1.7.3+ this is actually honored end-to-end. Pre-v1.7.3 the CLI accepted it for its own HTTP socket but dropped it before forwarding, so the desktop side defaulted to 120s and killed long-running tasks regardless. Long rust builds / ffmpeg renders / docker pulls etc. should set 600-1800s.\n\n**Returns:** stdout, stderr, exitCode\n\n**Note:** User must approve via in-app dialog unless auto-approve is active. v1.7.3+ timeout forwarding bug fixed — your timeoutSeconds is now respected by the desktop-side shell killer. v1.7.16+ approval-polling fixed — the CLI now polls the relay's deferred-result endpoint with the original requestId instead of re-sending the command every second (which v1.7.15 did, causing infinite 'Another shell command is already waiting for approval' loops because every retry created a fresh approval the user could never out-click).\n\n\n#### `shell_kill_all`\n\nKill all running shell command processes\n\n\n#### `wsl_exec`\n\nv1.8.46+: Execute a base64-encoded script inside a WSL distro via the canonical `wsl -d <distro> -u <user> -- bash -lc <decoded>` argv array. Eliminates the --, nested-quote, and $_ pitfalls because Rust's std::process::Command on Windows applies proper CommandLineToArgvW quoting per argv element — wsl.exe sees `bash -lc <decoded>` as three distinct argv items, then bash's -c sees the entire script as ONE string with no outer-shell interpretation. The exact 50×-per-session call pattern.\n\n**Args:**\n- `distro` — required string: WSL distro name (e.g. 'Adom-Workspace', 'Ubuntu'). `wsl -l -v` enumerates installed distros.\n- `scriptB64` — required string: base64-encoded bash script. Decoded content must be valid UTF-8.\n- `timeoutSeconds` — optional int (default 60). The CLI's WS timeout auto-sizes to scriptTimeout + 15s margin.\n- `user` — required string: WSL user to run as (e.g. 'adom', 'root'). Must exist in the distro.\n\n**Returns:** On success: {success:true, ok:true, stdout, stderr, exitCode, durationMs, interpreter:'wsl', distro, user, [_hint?]}. wsl.exe's UTF-16LE output is decoded as text automatically (BOM-detect + odd-position-NULL heuristic, fall back to UTF-8 lossy). On failure: {success:false, ok:false, stdout:'', stderr:'', exitCode:-1, error, failedAt, _hint}. failedAt is one of: 'args' (distro/user/scriptB64 empty), 'decode' (base64 invalid), 'utf8' (decoded bytes not UTF-8), 'spawn' (wsl.exe couldn't launch — see wsl_error_hint), 'timeout' (script exceeded timeoutSeconds). EVERY failure carries an actionable _hint. The most common spawn failure is 'distro not installed' — the hint will tell you to run `wsl -l -v` to enumerate, or fall back to run_script interpreter:bash for the default distro. **v1.8.60+ ADVISORY**: success responses may carry an additive `_hint` when the script looks like a direct HD build (raw `cargo`/`pnpm` targeting `hydrogen-desktop`/`crates/hd-*`); the hint points at the dedicated `hd_*` verbs that handle the exe lock + SHA verification.\n\n\n### File transfer\n\n_Move files between the cloud container and the laptop (streamed, SHA-verified)._ (2 verbs)\n\n#### `pull_file`\n\nPull files from the desktop back to Docker. Streams as binary WS frames (1 MiB chunks) — works for files up to several hundred MB. The legacy 30s base64-JSON path is gone; pull_file v1.4.3+ has no practical size limit.\n\n**Args:**\n- `filePaths` — required array of absolute Windows paths (e.g. \"C:\\Users\\\\me\\\\Downloads\\\\big.pdf\")\n- `saveTo` — optional directory on Docker (default /tmp). Created if missing.\n\n**Returns:** files: [{name, path, size, sha256, chunks}] — sha256 verified before file is finalized; mismatch deletes the partial and reports failure. chunks is the number of 1 MiB binary frames received.\n\n**Note:** Per-file timeout is 600s (enough for ~600 MB at 1 MB/s). For 75 MB files expect ~12s on a typical home connection. errors[] is included alongside files[] when at least one but not all files succeed; success is true iff at least one file landed.\n\n\n#### `send_files`\n\nSend files from Docker container to the desktop\n\n**Args:**\n- `destinationFolder` — optional relative subfolder\n- `filePaths` — required array of absolute paths\n- `targetApp` — optional: general|kicad|fusion360\n\n\n### Desktop & bridge mgmt\n\n_Windows, screenshots, files, embedded mode, relay servers, tunnels, and bridge lifecycle._ (44 verbs)\n\n#### `bridge_check_updates`\n\nREAD-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.\n\n**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.\n\n**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\":\"<bridge>\"} for one, or refresh_bridges for all.\n\n\n#### `bridge_install`\n\nInstall 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+.\n\n**Args:**\n- `manifestUrl` — required URL pointing at a <bridge>-bridge-manifest.json (e.g. https://wiki.adom.inc/api/v1/pages/adom-desktop/files/altium-bridge-manifest.json)\n\n**Returns:** {success, ok, bridge:<name from manifest>, version, installPath, _hint}\n\n**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.\n\n\n#### `bridge_list`\n\nList 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.\n\n**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}\n\n**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.\n\n**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.\n\n\n#### `bridge_pause`\n\nPause 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.\n\n**Args:**\n- `name` — required — bridge name from bridge_list (e.g. 'kicad')\n\n**Returns:** {success, name, paused:true, _hint}\n\n\n#### `bridge_resume`\n\nResume a paused bridge — flip the routing flag back. Inverse of bridge_pause.\n\n**Args:**\n- `name` — required — bridge name\n\n**Returns:** {success, name, paused:false, _hint}\n\n\n#### `bridge_uninstall`\n\nRemove 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.\n\n**Args:**\n- `name` — required — bridge name\n\n**Returns:** {success, name, cacheRemoved, stillInstalledViaBundled, _hint}\n\n\n#### `desktop_bring_to_front`\n\nBring a window to the foreground\n\n**Args:**\n- `hwnd` — optional int\n- `state` — optional: maximize or restore (default: preserve current state)\n- `titleContains` — optional string (case-insensitive)\n\n\n#### `desktop_caption`\n\nShow 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.\n\n**Args:**\n- `action` — optional: \"hide\" (dismiss this id, or all if no id) | \"force-clear\" (nuke ALL caption windows)\n- `duration` — optional ms before auto-clear. Omitted or 0 → 30000 (30s) default. Clamped to max 600000 (10 min). NOT 'forever' anymore.\n- `fontSize` — optional int px (8–400), overrides size\n- `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\".\n- `persist` — optional bool. true → sticky, NO auto-clear (supervised use only, e.g. demo recordings) — YOU must dismiss it.\n- `position` — optional: top | center | bottom (default) | top-left | top-right | bottom-left | bottom-right\n- `size` — optional: small (20px) | medium (32px, default) | large (72px)\n- `text` — the caption text (omit, or action:\"hide\", to dismiss)\n- `x` — optional 0.0–1.0 horizontal fraction (overrides position)\n- `y` — optional 0.0–1.0 vertical fraction (overrides position)\n\n**Returns:** {visible, text, id, persist, expiresInMs (null if persist), _hint}\n\n**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\":\"<id>\"}. Always dismiss captions when the narrated action is done.\n\n\n#### `desktop_connect_all`\n\nv1.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.\n\n**Returns:** {ok, success, totalEnabled, spawned, alreadyConnected, _hint}\n\n\n#### `desktop_disconnect_all`\n\nv1.8.42+: programmatically drop every live WS connection. Server entries stay in config (use server_remove to delete). Mirrors HD's tray 'Disconnect All'.\n\n**Returns:** {ok, success, disconnected, _hint}\n\n\n#### `desktop_embedded_enter`\n\nv1.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.\n\n**Returns:** {ok:true, success:true, was_embedded:bool, embedded:true, entered_via:'runtime-adopt', owner:'hydrogen-desktop', _hint}\n\n**See also:** desktop_embedded_release (revert), desktop_embedded_status (introspect). HD-side recipe in plugins/hd/SKILL.md.\n\n\n#### `desktop_embedded_release`\n\nv1.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.\n\n**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.\n\n**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).\n\n\n#### `desktop_embedded_status`\n\nv1.8.42+: introspect whether AD is currently running in embedded mode (HD bundling AD). v1.8.73+ also reports `enteredVia` — `launch-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.\n\n**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}\n\n\n#### `desktop_install_kicad`\n\nUnattended 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.\n\n**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}.\n\n**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.\n\n\n#### `desktop_install_node`\n\nUnattended 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.\n\n**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}.\n\n**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.\n\n\n#### `desktop_list_browsers`\n\nCatalog 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.\n\n**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.\n\n**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+.\n\n\n#### `desktop_list_files`\n\nList 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.\n\n**Args:**\n- `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\".\n- `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.\n- `path` — required string. Windows abs path. ~, %USERPROFILE%, %APPDATA%, %LOCALAPPDATA%, %TEMP% are expanded. Forward slashes (/) and back slashes (\\\\) both work.\n\n**Returns:** {ok, path, glob, files: [{path, name, size, mtime}, ...] sorted newest-first, count}\n\n**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.\n\n\n#### `desktop_list_windows`\n\nList all visible windows with HWNDs, titles, positions\n\n\n#### `desktop_logout`\n\nv1.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.\n\n**Returns:** {ok, success, _hint}. After logout, adom_whoami returns null until the user signs in again (via the GUI footer link or another handoff).\n\n\n#### `desktop_open_folder`\n\nOpen file/folder in Windows Explorer\n\n**Args:**\n- `path` — required string\n\n\n#### `desktop_open_url`\n\nOpen 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.\n\n**Args:**\n- `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\".\n- `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.\n- `url` — required string (https://... or any URL the OS shell can dispatch)\n\n**Returns:** {ok:bool, browser, exePath?, url, profile?} — profile echoed if it was applied\n\n**Note:** desktop_open_url vs browser_open_window — pick the right one:\n • 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.\n • 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).\n Rule of thumb: human-in-the-loop login flow => desktop_open_url. AI-driven automation => browser_open_window.\n 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.\n\n\n#### `desktop_pull_glob`\n\nConvenience 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.\n\n**Args:**\n- `glob` — optional string (default \"*\").\n- `path` — required string. Windows abs path; same expansion as desktop_list_files.\n- `pollMs` — optional (only used when wait:true). Default 1000.\n- `saveTo` — required string. Container-side absolute path (a directory). Parent dirs auto-created. Each pulled file lands at saveTo/<basename>.\n- `since` — optional unix-seconds OR ISO-8601. Only files with mtime > since are pulled.\n- `timeoutMs` — optional (only used when wait:true). Default 60000.\n- `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.\n\n**Returns:** {ok, files:[{name, path, size, sha256, chunks}, ...], errors:[...], matchedCount}\n\n**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.\n\n\n#### `desktop_revoke_approvals`\n\nClear all shell auto-approve permissions and deny pending approvals\n\n\n#### `desktop_screenshot_screen`\n\nCapture the entire desktop (all monitors). v1.8.62+ produces full + safe-resized PNG pair to %TEMP%\\adom-desktop-shots\\screen-<ts>.{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-`.\n\n**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).\n\n\n#### `desktop_screenshot_window`\n\nCapture 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-<hwnd>-<ts>.{full,safe}.png. v1.8.61+ universal screenshot delivery contract: the CLI auto-pulls both PNGs to /tmp/ad-shots/<basename> 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.\n\n**Args:**\n- `hwnd` — required int\n- `resizeMax` — optional int, default 1400. Longest side of the safe-resized PNG.\n\n**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.\n\n\n#### `desktop_set_window_state`\n\nSet window state without necessarily foregrounding\n\n**Args:**\n- `hwnd` — optional int\n- `state` — required: maximize|minimize|restore|show|hide\n- `titleContains` — optional string\n\n**Note:** Use bring_to_front to foreground + maximize in one call. Use this for minimize/hide/show.\n\n\n#### `desktop_show_ui`\n\nv1.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.\n\n**Args:**\n- `show` — optional bool, default true (false hides/resets)\n- `target` — required: settings | build_info | direct_api | shell_approval | none (reset)\n\n**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.\n\n\n#### `desktop_shutdown`\n\nv1.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.\n\n**Returns:** {ok, success, output, _hint}. The exit happens ~500ms after the response is sent so the caller doesn't see connection-reset.\n\n\n#### `desktop_watch_files`\n\nBlock 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.\n\n**Args:**\n- `glob` — optional string (default \"*\"). Same semantics as desktop_list_files.\n- `path` — required string. Same expansion semantics as desktop_list_files.\n- `pollMs` — optional int (default 1000, floor 100). How often to re-list the directory.\n- `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.\n- `timeoutMs` — optional int (default 60000, max 600000 = 10 min). After this elapses, returns {ok:false, error:\"timeout\", elapsedMs, _hint}.\n\n**Returns:** On match: {ok:true, path, glob, file:{path,name,size,mtime}, elapsedMs}. On timeout: {ok:false, error:\"timeout\", elapsedMs, _hint, path, glob}.\n\n**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.\n\n\n#### `desktop_window_hide`\n\nv1.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.\n\n**Returns:** {ok, success, _hint}\n\n\n#### `desktop_window_show`\n\nv1.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.\n\n**Returns:** {ok, success, _hint}\n\n\n#### `direct_api`\n\nINFORMATIONAL — 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.\n\n**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\"`.\n\n**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.\n\n**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.\n\n**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).\n\n\n#### `rdp`\n\nv1.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.\n\n**Args:**\n- `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.\n- `to` — REQUIRED string — RDP target in `host:port` form. If port omitted, defaults to 3389.\n\n**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.\n\n\n#### `refresh_bridges`\n\nForce 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\":\"<bridge>\"} 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.\n\n**Args:**\n- `name` — optional — bridge name from bridge_list (e.g. 'kicad'). Omit to sync every bridge.\n\n**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.\n\n**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.\n\n\n#### `register_container`\n\nv1.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.<id> (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.\n\n**Args:**\n- `auth_token` — optional string — relay auth token (defaults to adom-dev-token-2025, which is what the cloud relay accepts by default)\n- `auto_connect` — optional bool — auto-dial on AD launch (default true)\n- `enabled` — optional bool — enabled in AD's server list (default true)\n- `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.\n- `name` — REQUIRED string — human-friendly label for AD's server list (e.g. 'galliaApril')\n- `url` — REQUIRED string — public wss:// URL the desktop will dial (typically wss://<container-slug>.adom.cloud/proxy/8765/)\n\n**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.\n\n**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.\n\n**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.\n\n\n#### `relaunch_self`\n\nv1.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.\n\n**Args:**\n- `destExe` — optional dest path (default: %LOCALAPPDATA%\\Adom Desktop\\adom-desktop.exe)\n- `noAutoStart` — optional bool — if true, the relauncher shows its GUI but waits for POST /start instead of running immediately\n- `port` — optional HTTP API port for the relauncher (default: 47210)\n- `sourceExe` — optional path to the freshly-built adom-desktop.exe (default: src-tauri/target/release/adom-desktop.exe relative to the relauncher's location)\n\n**Returns:** {ok, success, relauncherPid, relauncherExe, relauncherPort, _hint}\n\n**See also:** relauncher_status (poll progress), skills/AD_RELAUNCH.md (full recipe).\n\n\n#### `relauncher_status`\n\nv1.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.\n\n**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}\n\n**See also:** relaunch_self (spawn), skills/AD_RELAUNCH.md (recipe).\n\n\n#### `server_add`\n\nRegister 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+.\n\n**Args:**\n- `authToken` — optional string — defaults to 'adom-dev-token-2025'. Empty string means 'keep existing token if this is an update; otherwise use default'.\n- `autoConnect` — optional bool, default true — connect right after upsert. Set false to add the entry without dialling out.\n- `name` — required string — human-friendly server label. Acts as the dedup key for upserts.\n- `url` — required string — relay WebSocket URL (e.g. ws://localhost:8765 or wss://your-host/proxy/8765/).\n\n**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).\n\n**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\":\"<the-name>\"}'.\n\n\n#### `server_list`\n\nReturn 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+.\n\n**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.\n\n\n#### `server_remove`\n\nDisconnect (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+.\n\n**Args:**\n- `name` — required string — the same `name` you passed to server_add.\n\n**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.\n\n\n#### `shell_auto_approve`\n\nProgrammatically 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').\n\n**Args:**\n- `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`.\n- `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).\n\n**Returns:** {success, output, active:bool, remainingSecs, permanent:bool}\n\n**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:\n - Dev session: `adom-desktop shell_auto_approve '{\"duration_secs\": 86400}'` (24h, then expires).\n - Owner's box, durable: `adom-desktop shell_auto_approve '{\"permanent\": true}'` (equivalent to ticking the Settings checkbox).\n - 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.\n - Revoke everything: `adom-desktop shell_auto_approve '{\"duration_secs\": 0}'`.\n\n\n#### `tunnel`\n\nv1.8.85+: open a generic TCP port-forward tunnel from a desktop-side 127.0.0.1:<local> 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.\n\n**Args:**\n- `local` — REQUIRED int — the local 127.0.0.1 port the desktop should bind. mstsc / your TCP client connects HERE.\n- `to` — REQUIRED string — upstream target in `host:port` form. The relay dials this when bytes arrive on the local port.\n- `tunnelId` — optional u32 — caller-chosen ID. Defaults to a fresh random u32. Reuse across re-invocations to address the same tunnel.\n\n**Returns:** {ok, tunnelId, toHost, toPort, clientId, actualPort, error?, _hint}. On success, mstsc/anything connecting to 127.0.0.1:<actualPort> on the desktop has its bytes relayed.\n\n**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:<local> gets its own connId routed independently.\n\n\n#### `tunnel_close`\n\nv1.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.\n\n**Args:**\n- `connId` — optional u32 — if given, close only this conn; otherwise close the whole tunnel\n- `tunnelId` — REQUIRED u32 — from `tunnel` / `tunnel_list`\n\n**Returns:** {ok, tunnelId, connId?, scope:'tunnel'|'conn', _hint}\n\n\n#### `tunnel_list`\n\nv1.8.85+: list all active tunnels — both the relay's view and AD's view (should agree).\n\n**Returns:** {ok, relay:[{tunnelId, toHost, toPort, clientId, connCount}, ...], desktop:{tunnels:[...]}, _hint}\n\n\n### Screen recording\n\n_Record the desktop/monitors to video; manage the recorder window._ (7 verbs)\n\n#### `desktop_list_monitors`\n\nList available monitors. v1 returns the primary only; full enumeration is v1.1.\n\n**Returns:** monitors: [{index, name, x, y, width, height, primary}]\n\n\n#### `desktop_record_list`\n\nList completed desktop recording .webm files on disk with their sidecar metadata.\n\n**Returns:** recordings: [{recordingId, filePath, sizeKB, durationMs, reason, mtime}]\n\n\n#### `desktop_record_start`\n\nStart a WHOLE-DESKTOP recording (every pixel on screen, including unrelated apps) via Chrome MediaRecorder + getDisplayMedia. Opens HUD if needed. Reason is REQUIRED. HUD persists across multiple clips in one session — use desktop_recorder_close to dismiss.\n\n**Args:**\n- `audio` — optional bool (default false)\n- `confirmDesktopNotTabRecording` — REQUIRED bool — must be true. Acknowledges that you understand this is full-desktop recording, NOT pup tab recording. If user said 'record a pup' / 'record this Chrome window' / 'record the page', use browser_record_start instead. If user wants generic 'record my screen', Hydrogen's built-in `recording start --share screen` is usually preferable. desktop_record_* is mainly for parallel desktop video alongside Hydrogen tab recording (e.g. ralph-loop screenshot tests).\n- `fps` — optional int (default 30)\n- `maxDurationMs` — optional int (default 600000)\n- `monitor` — optional 'primary'|'all'|int (default 'primary')\n- `reason` — REQUIRED string — explain to the user why you're recording (renders in the HUD title and sidecar JSON)\n- `videoBitsPerSecond` — optional int (default 4000000)\n\n**Returns:** recordingId, filePath (.webm), startedAt, monitor, reason\n\n**Note:** PICK THE RIGHT VERB: 'record a pup' / 'record the tab' / 'record this Chrome page' => browser_record_start (tab-scoped, ~50fps, no foreground requirement). 'record my whole desktop' / 'record across multiple apps' => desktop_record_start with confirmDesktopNotTabRecording:true. Prefer Hydrogen's `recording start --share screen` for casual desktop recording — adom-desktop's desktop recording is mainly for the parallel-with-Hydrogen-tab-capture case.\n\n**Error code:** desktop_record_needs_confirmation when confirmDesktopNotTabRecording is missing/false — read the error and decide if you really want full-desktop or pup-tab recording\n\n\n#### `desktop_record_status`\n\nStatus of the desktop recorder HUD and any active clip.\n\n**Returns:** hudOpen, reason, currentRecording, clipsThisSession\n\n\n#### `desktop_record_stop`\n\nStop the active desktop clip. Finalizes the .webm. HUD stays open in Ready state for the next clip in this session.\n\n**Args:**\n- `recordingId` — optional (active clip if omitted)\n\n**Returns:** recordingId, filePath, sizeKB, durationMs, stopReason\n\n\n#### `desktop_recorder_close`\n\nClose the desktop recorder HUD (ends the session). Auto-stops any active clip first.\n\n**Returns:** sessionSummary: {clipCount, totalSizeKB, reason, clips}\n\n\n#### `desktop_recorder_open`\n\nOpen the WHOLE-DESKTOP recorder HUD (visible bottom-right always-on-top window) without starting a clip. Reason is REQUIRED. Same scope as desktop_record_start — the HUD will record the entire desktop, NOT a pup tab.\n\n**Args:**\n- `reason` — required string explaining why you're recording\n\n**Returns:** opened, reason\n\n**Note:** PICK THE RIGHT VERB: for pup tab recording use browser_record_start (no HUD needed, tab-scoped). For generic 'record my screen' use Hydrogen's `recording start --share screen`. Use desktop_recorder_open only when you want a visible HUD on screen during a multi-clip session (e.g. parallel desktop video alongside Hydrogen tab capture).\n\n\n### Notifications\n\n_Native desktop toast notifications._ (1 verbs)\n\n#### `notify_user`\n\nSend a Windows desktop toast notification\n\n**Args:**\n- `actions` — optional array of button labels\n- `body` — required string\n- `level` — optional: info|warning|error|emergency (default: info)\n- `title` — required string\n\n\n### Project watch\n\n_Watch a local folder + sync changes._ (1 verbs)\n\n#### `args`\n\n\n### Browser (Puppeteer)\n\n_Drive Chrome-for-Testing: navigate, eval, screenshot, record, tabs/windows, credentials._ (31 verbs)\n\n#### `browser_alert_window`\n\nFlash the Chrome window's taskbar icon (does NOT steal foreground)\n\n**Args:**\n- `sessionId` — required\n\n\n#### `browser_close`\n\nClose pup sessions. With sessionId: closes ONLY that session (Chrome window dies, others stay). Without sessionId: closes ALL sessions and Chromes (the bridge itself stays running). Use the sessionId form for targeted cleanup; use the no-arg form when you genuinely want a clean slate.\n\n**Args:**\n- `sessionId` — optional — close just this one session\n\n**Note:** If you want the SESSION's tab closed but the Chrome window kept alive (other sessions on the same profile share Chrome), use browser_close_window or browser_close_tab instead.\n\n\n#### `browser_close_tab`\n\nClose one tab; session + other tabs remain\n\n**Args:**\n- `sessionId` — required\n- `tabId` — required\n\n**Returns:** remaining tab count\n\n\n#### `browser_close_window`\n\nClose a session's Chrome window (closes ALL tabs in that session)\n\n**Args:**\n- `sessionId` — required\n\n\n#### `browser_errors`\n\nGet console errors and failed network requests. Optional tabId for per-tab errors. v1.4.10: a stale tabId now returns errorCode:\"tab_not_found\" instead of silently returning the active tab's errors.\n\n**Args:**\n- `clear` — optional bool (default true)\n- `sessionId` — optional\n- `tabId` — optional\n\n\n#### `browser_eval`\n\nEvaluate JavaScript in page context. Pass optional tabId to target a specific tab; omit to use the active tab. v1.4.10: passing a stale tabId now returns errorCode:\"tab_not_found\" with currentTabs + lastKnownUrl (when the missing tab was a recently-closed popup) instead of silently evaluating against the active tab.\n\n**Args:**\n- `expr` — required JS string\n- `sessionId` — optional\n- `tabId` — optional\n\n\n#### `browser_fetch_url`\n\nFetch an arbitrary URL with the session's cookies and return the raw bytes. Bypasses Chrome's PDF Viewer wrapper — critical for grabbing PDF binaries from popup tabs where in-page fetch(location.href) returns the viewer HTML wrapper (~200 KB stub) instead of the actual PDF. Also handles ZIPs, CAD bundles, anything served as a binary content-type.\n\n**Args:**\n- `body` — optional raw string body for POST etc.\n- `desktopSaveTo` — optional DESKTOP-side absolute path (Windows) — bridge writes via fs.writeFileSync. Use when the user wants the file local on the desktop (e.g. dropping a CAD bundle into Downloads). Returns `desktopSavedTo`. Independent of saveTo; you can pass both.\n- `headers` — optional object — Cookie auto-included from session\n- `method` — optional (default GET)\n- `saveTo` — optional CONTAINER-side absolute path. CLI handles the write after receiving bytes from the bridge; parent dirs auto-created. Returned `savedTo` is the canonical absolute path verified to exist on disk (v1.4.9+ — v1.4.8 had a bug where this could lie). saveTo and bodyBase64 are mutually exclusive in the response.\n- `sessionId` — optional (defaults to active session)\n- `tabId` — optional (picks cookie context — defaults to active tab; cookies are per-browser-context so any tab in the session works)\n- `url` — required\n\n**Returns:** {ok, bytes, contentType, status, sessionId, tabId, plus: savedTo + savedToFilesystem:\"container\" if saveTo was set; desktopSavedTo (or desktopSaveError) if desktopSaveTo was set; bodyBase64 if neither saveTo nor desktopSaveTo was set}\n\n**Note:** Use this when a click spawns a popup tab whose Content-Type is application/pdf — Chrome PDF Viewer wraps it and in-page fetch returns the wrapper HTML. browser_fetch_url re-issues the request through puppeteer's BrowserContext.request which doesn't go through the page render layer. RIGHT PATTERN with popup tabs: read the popup's url from browser_list_tabs (immediately — popups can auto-close), then browser_fetch_url with that URL. NEVER eval-against-popup-then-fetch (popup may have closed; eval would error with errorCode:tab_not_found and a hint pointing here).\n\n\n#### `browser_focus_window`\n\nActivate the session's tab WITHIN Chrome (Puppeteer page.bringToFront). Does NOT raise the OS window — for that, use browser_raise_os_window.\n\n**Args:**\n- `sessionId` — required\n\n\n#### `browser_input_dispatch`\n\nDispatch TRUSTED input via Chromium's CDP-backed page.mouse / page.keyboard. Events have isTrusted=true (vs page-side dispatchEvent which is isTrusted=false and silently no-ops on framework gates / anti-automation checks). USE THIS instead of browser_eval-clicks when a click 'lands but does nothing' — most modern frameworks (React/Vue/Svelte) and vendor anti-automation gates check event.isTrusted before running handlers.\n\n**Args:**\n- `button` — click only — left|right|middle (default left)\n- `clickCount` — click only — int (default 1; use 2 for double-click)\n- `delay` — click/type/key only — ms between mousedown+mouseup or keystrokes (default 0)\n- `firstMatch` — click only — bool (default false). When true, skips the visible+text+area filter and uses document-order first match. Useful when you've explicitly verified the first match is correct.\n- `key` — key only — Enter|Escape|Tab|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|F1|... (any Puppeteer KeyInput name)\n- `selector` — click only — CSS selector. v1.4.8+ uses smart-pick when the selector matches multiple elements: filters to visible elements with non-empty text, tiebreaks by largest visual area. v1.4.9 added modal-scoped restriction: if a modal is open, candidates are restricted to elements inside it. v1.4.10 added the isPlausibleModal filter: empty overlay containers (Vue-Toastification toast wrappers, notification mount points) no longer fool modal-detection — only roots containing at least one visible interactive element with text count as modals. Pass firstMatch:true to skip all this and use document-order first match.\n- `sessionId` — optional (defaults to active session)\n- `steps` — move only — number of intermediate positions for human-like movement (default 1)\n- `tabId` — optional (defaults to active tab)\n- `text` — type only — string to type into the focused element. Pass selector to focus that element first.\n- `type` — required: click | move | type | key\n- `x` — click/move only — viewport X coord\n- `y` — click/move only — viewport Y coord\n\n**Returns:** click via selector: {ok, sessionId, tabId, dispatched:\"click\", via:\"selector\", selector, matchedCount, chosenIndex, clickedRect:{x,y,w,h}, clickedText, insideModal, modalDetected, modalRoot:{tag,id,cls,z,role,ariaModal}|null, pickStrategy:\"only-match\"|\"first-match\"|\"modal-scoped-largest\"|\"visible-text-largest\"|\"visible-text-largest-no-modal-match\"|\"visible-largest\"|\"fallback-first-document-order\"}. click via coords: {ok, ..., via:\"coords\", x, y}. move/type/key return {ok, dispatched, ...}.\n\n**Note:** Common cases: vendor 'Generate' / 'Download' buttons that open popups (use type=click + selector); login forms (selector to focus + type=type for username/password + key=Enter); modal dismissal (key=Escape). After a click that should produce a popup tab, follow up with browser_list_tabs — popup tabs auto-track since v1.4.7 with opener=\"popup\" + openerTabId. To grab bytes from a PDF popup (Chrome PDF viewer otherwise serves only the wrapper HTML on in-page fetch), use browser_fetch_url with the popup's URL. v1.4.10: when smart-pick lands on the wrong element, inspect the response's `modalRoot` field to see which DOM node won the modal-detection heuristic — useful for diagnosing toast-container false-positives.\n\n\n#### `browser_list_tabs`\n\nList all tabs in a session, including auto-tracked popups. Tabs that the page itself opens via window.open() / target=_blank / form-submit-with-target=_blank are tracked automatically since v1.4.7 (Target.targetCreated listener) and appear here alongside Claude-opened tabs.\n\n**Args:**\n- `sessionId` — required\n\n**Returns:** tabs: [{tabId, url, title, active, errorCount, opener, openerTabId}], activeTabId, count. opener=\"user\" for tabs Claude opened (browser_open_window/browser_open_tab); opener=\"popup\" for tabs the page spawned (openerTabId names the originating tab — e.g. a product page that spawned a datasheet popup).\n\n\n#### `browser_list_windows`\n\nList all open browser sessions with URLs and titles\n\n\n#### `browser_lower_os_window`\n\nMinimize the OS window hosting this pup session (give the user back their desktop after Claude is done with a flow).\n\n**Args:**\n- `sessionId` — required\n\n**Returns:** sessionId, hwnd, lowered, error?\n\n\n#### `browser_navigate`\n\nNavigate to a new URL. Optional tabId to target a specific tab; omit to use the active tab. v1.4.10: passing a stale tabId now returns errorCode:\"tab_not_found\" with currentTabs + lastKnownUrl (when the missing tab was a recently-closed popup) instead of silently navigating the active tab.\n\n**Args:**\n- `sessionId` — optional\n- `tabId` — optional\n- `url` — required\n\n\n#### `browser_open_tab`\n\nAdd a tab to an existing session (same Chrome window as other tabs in that session — avoids new taskbar icon)\n\n**Args:**\n- `sessionId` — required\n- `url` — required\n\n**Returns:** tabId, url, title, active\n\n\n#### `browser_open_window`\n\nOpen a Chrome window with a URL (auto-starts Puppeteer bridge). v1.6.3+: every session opens in FULL CAPABILITY MODE by default — JS-triggered downloads are allowed (Page.setDownloadBehavior=allow on every page + framenavigated), clipboard read/write is granted for paste-into-form flows, notifications + geolocation are DENIED so prompts don't pop up under automation. Pup is a tool-driving surface, not a user-facing browser; nearly every chip-fetcher-style flow needs these defaults. Pass strictPermissions:true to opt out (rare).\n\n**Args:**\n- `downloadPath` — optional Windows abs path (default `%USERPROFILE%\\Downloads`). Where scripted downloads land. Defaults to the OS Downloads folder, which is what desktop_watch_files polls by default — the two ends meet without configuration.\n- `freshProfile` — optional bool\n- `profile` — required string\n- `sessionId` — required string\n- `strictPermissions` — optional bool (default false). When true, skip ALL v1.6.3+ defaults: no clipboard permissions, no scripted-download policy, no notification/geolocation suppression. Chromium's normal permission prompts apply, scripted downloads may be silently dropped. Use only when the user genuinely needs to gate everything (very rare).\n- `url` — required string\n\n**Returns:** {ok, sessionId, profile, url, tabId, defaultPermissionsApplied (true unless strictPermissions was passed), defaultPermissions (e.g. ['clipboard-read=granted','clipboard-write=granted','notifications=denied','geolocation=denied','midi=denied']), downloadsEnabled (true unless strictPermissions), downloadPath (the resolved absolute path for downloads), restoredTabsClosed, ...}\n\n**Note:** v1.6.3+ unblocks the silent-download-failure scenario. Before v1.6.3, JS-triggered downloads (clicks on `<a download>`, fetch+blob saves, vendor 'Download Symbol' buttons that do `window.location = url`) could be silently dropped by Chrome — no error, no UI, no file in ~/Downloads. The fix: applyDownloadBehavior() sets Page.setDownloadBehavior on every page + framenavigated. Combined with desktop_watch_files for the wait-for-arrival pattern, downloads now Just Work without any caller setup.\n\n\n#### `browser_raise_os_window`\n\nRaise the OS window hosting this pup session above all desktop apps (real foreground, defeats Windows foreground lock). Use BEFORE recording / screenshots / animations so Chrome doesn't throttle the page (document.hidden=true → ~1Hz rAF). Internally: focuses the tab, then EnumWindows-finds the Chrome window by title containing '(session: <sessionId>)' and runs the AttachThreadInput + keybd_event bypass.\n\n**Args:**\n- `sessionId` — required\n\n**Returns:** sessionId, hwnd, title, raised, error?\n\n\n#### `browser_record_list`\n\nList completed tab recording .webm files on disk with sidecar metadata.\n\n**Returns:** recordings: [{recordingId, filePath, sizeKB, durationMs, fps, sessionId, tabId, mtime}]\n\n\n#### `browser_record_start`\n\nRecord a pup tab via CDP Page.startScreencast + Page.screencastFrameAck (Chrome's native compositor pipeline). Tab-scoped at the protocol level — no cross-tab/cross-Chrome leakage. Output is a single finished VP9 .webm file (no tar bundle, no Docker-side mux). Real-time playback via VFR concat-mux. Concurrent: one recording per tab; many tabs OK. CALL browser_raise_os_window FIRST so Chrome doesn't paint-throttle the page (anti-throttle flags help when occluded but a foregrounded window is always best).\n\n**Args:**\n- `fps` — optional int (default 30) — actual fps may exceed target on rAF-animated pages\n- `maxDurationMs` — optional int (default 600000 = 10 min)\n- `quality` — optional int 0-100 JPEG quality (default 85)\n- `sessionId` — required\n- `tabId` — optional (defaults to active tab)\n\n**Returns:** recordingId, sessionId, tabId, filePath (.webm), startedAt\n\n\n#### `browser_record_status`\n\nList active tab recordings with live capture stats.\n\n**Args:**\n- `sessionId` — optional (filter to one session)\n\n**Returns:** active: [{recordingId, sessionId, tabId, filePath, fpsTarget, fpsActual, frameCount, durationMs, sizeBytesApprox, startedAt}]\n\n\n#### `browser_record_stop`\n\nStop a tab recording. Mux runs synchronously on stop (ffmpeg concat demuxer with per-frame wall-clock durations). Returns a single finished .webm — pull_file directly, no extraction step.\n\n**Args:**\n- `recordingId` — required\n- `sessionId` — optional\n\n**Returns:** recordingId, sessionId, tabId, filePath (.webm), sizeKB, durationMs, frameCount, fpsTarget, fpsActual, stopReason\n\n**Note:** fpsActual reports the measured rate. If it falls below ~20 when target was 30, the pup window was occluded — call browser_raise_os_window before the next start, and verify the pup Chrome was launched with the anti-throttle flags (sessions opened before bridge v1.3.32 don't have them — close + reopen the session to refresh).\n\n\n#### `browser_reload`\n\nReload page and clear error log. Optional tabId. v1.4.10: a stale tabId now returns errorCode:\"tab_not_found\" instead of silently reloading the active tab.\n\n**Args:**\n- `sessionId` — optional\n- `tabId` — optional\n\n\n#### `browser_rescan`\n\nRecover orphaned Chrome windows whose CDP socket dropped (network blip, sleep/wake, puppeteer-side hiccup). Walks every known profile (in-memory + on-disk session files), reconnects via the persisted DevToolsActivePort, then walks every page and rebuilds session entries by parsing the (session: X) tag from each page's title. v1.5.1+ — paired with strict resolveSession (passing a known-but-disconnected sessionId now returns errorCode:\"session_disconnected\" instead of silently retargeting an unrelated window).\n\n**Args:**\n- `adoptOrphans` — optional bool (default false). When true, pages with no (session: X) title tag get attached under a generated sessionId so the caller can drive them. Useful when Chrome has tabs the bridge has never seen (user opened them manually). Default false to avoid surprising callers with random tabs in their session list.\n\n**Returns:** {ok, rescanned, profilesReconnected, profilesUnreachable, reattached, orphansAdopted, liveSessions, disconnectedSessions, _hint}\n\n**Note:** Idempotent — safe to call repeatedly. The 30s health check now also auto-attempts reconnect, so most transient blips recover before you notice. Use this manually when you suspect the bridge has lost a window (e.g. browser_navigate suddenly errors with errorCode:\"session_disconnected\").\n\n\n#### `browser_screenshot`\n\nCapture lossless PNG screenshot (auto-resized to <=1568px at the bridge, then capped to <=1500px in the CLI's defense-in-depth layer). Optional tabId for per-tab capture. Safe to Read into Claude — the two-layer resize is mandatory so the image always fits the many-image API limit.\n\n**Args:**\n- `fullPage` — optional bool\n- `maxWidth` — optional int (default 1568)\n- `sessionId` — optional\n- `tabId` — optional\n\n**Returns:** savedTo (file path), sizeKB, width (final on-disk px), height, origWidth (pre-resize, what Chrome captured), origHeight, resized (bool — true if the cap fired), tabId. v1.8.40+ adds the width/height/origWidth/origHeight/resized fields so the caller knows exactly what dims they got without having to probe the file.\n\n\n#### `browser_screenshot_full_res`\n\nCapture FULL RESOLUTION screenshot (NO resize). WARNING: Do NOT Read the resulting file in Claude — images >2000px crash many-image sessions at the Claude API limit. Use browser_screenshot for AI-readable screenshots. This verb is for saving to disk / wiki / external export only (e.g. handing a designer a high-DPI render). v1.8.40+.\n\n**Args:**\n- `fullPage` — optional bool\n- `sessionId` — optional\n- `tabId` — optional\n\n**Returns:** savedTo (file path), sizeKB, width, height, tabId, _warning. Width/height are the actual capture dimensions (typically 2559x1398 on a standard 1920x1080 viewport with hi-DPI). _warning field repeats the do-not-Read advisory so it's visible in the response payload.\n\n**Note:** Implementation symmetry with browser_screenshot: same path resolution, same disk-save flow at the bridge. Only difference is the sharp resize+recompress step is skipped + the Rust-side save_screenshot's resize cap (defense-in-depth) is bypassed.\n\n\n#### `browser_status`\n\nCheck all sessions: URLs, error counts, alive status\n\n\n#### `browser_switch_tab`\n\nMake a specific tab the active one (calls bringToFront within Chrome)\n\n**Args:**\n- `sessionId` — required\n- `tabId` — required\n\n\n#### `browser_switch_window`\n\nSwitch the bridge's active session (which window commands without explicit sessionId target)\n\n**Args:**\n- `sessionId` — required\n\n\n#### `browser_wait`\n\nWait for content to settle\n\n**Args:**\n- `ms` — optional int (default 3000)\n\n\n#### `credential_delete`\n\nRemove a credential entry by exact host pattern. Drops the password from the OS keychain and the entry from the index.\n\n**Args:**\n- `host` — required (must match the host pattern used at credential_set time, exactly)\n\n**Returns:** {ok, host, removedFromKeychain, removedFromIndex}\n\n\n#### `credential_list`\n\nList stored host patterns + their associated usernames. Passwords are NEVER returned.\n\n**Returns:** {ok, credentials: [{host, username, addedAt, updatedAt}]}\n\n\n#### `credential_set`\n\nStore HTTP Basic Auth credentials for a host pattern. The password goes into the OS keychain (encrypted at rest by the user's session key); the index file holds host+username only. Subsequent pup navigations to a matching host auto-authenticate.\n\n**Args:**\n- `host` — required (e.g. \"*.componentsearchengine.com\" or exact \"nxp.componentsearchengine.com\")\n- `password` — required (NEVER echoed back via list/get)\n- `username` — required\n\n**Returns:** {ok, host, username}\n\n\n### KiCad bridge\n\n_Drive KiCad 7/8/9/10: open designs, DRC/ERC, libraries, 3D, screenshots, lint._ (27 verbs)\n\n> **Note:** All kicad_* commands are forwarded to the KiCad bridge (port 8772). Use kicad_<command> format.\n\n#### `kicad_bridge_call`\n\nGeneric passthrough: call any RPC method on the adom_bridge plugin running inside a specific KiCad exe. Picks the newest alive instance for that exe automatically (or use pid arg to pin to a specific PID). For export_svg / export_pdf use a larger timeout (default 10s; plot can take 5-15s).\n\n**Args:**\n- `exeName` — required (kicad, pcbnew, eeschema, kicad-cli)\n- `method` — required — see methods list\n- `params` — optional dict — method-specific\n- `pid` — optional int — pin to a specific process PID (multi-instance disambiguation, v1.7.6+)\n- `timeout` — optional float seconds (default 10; bump to 30-60 for exports)\n\n**Returns:** {ok, result:<rpc result>, exeName, pid, port} OR {ok:false, errorCode:\"plugin_not_running\"|\"rpc_error\", ...}\n\n**Note:** Methods (v1.7.9): introspection — ping, list_frames, get_menu_ids, wm_command{frame_index,menu_id}, get_board_info / get_schematic_info / get_open_editors. PCB graph (pcbnew only) — get_pcb_footprints, get_pcb_layers, get_pcb_nets, get_pcb_design_rules, get_pcb_tracks (start/end/width/layer/netName), get_pcb_pads (per-footprint pads with position/size/drill/netName), get_pcb_vias (position/diameter/drill/viaType/top+bottom layer), get_pcb_drawings (silk text + edge cuts + dimensions), get_pcb_zones (copper pour inventory with priority/isFilled/layers/boundingBox), get_drc_markers (existing markers from last DRC run, grouped by severity), get_footprint_connections{ref} (net-graph: pin → netName → connected (ref,pin) list). Export (pcbnew only) — export_svg / export_pdf via in-process PLOT_CONTROLLER. Library nav — navigate_symbol{symbolName} / navigate_footprint{footprintName} (filter only — does NOT load). select_and_load_symbol{symbolName, useSendInput?:false} / select_and_load_footprint{footprintName, useSendInput?:false}: filter + Down+Enter keystroke. v0.7.0+ (Phase 2.ac) adds optional useSendInput:true that escalates from wx.PostEvent to real Win32 SendInput — more reliable but briefly steals foreground. v0.8.0+ (Phase 2.ad, bridge bundled in v1.7.17) hardens the foreground acquisition: AttachThreadInput dance + SendInput mouse-click at the search ctrl's screen center, with cursor pos snapshot/restore. Pre-v0.8.0 the naive SetForegroundWindow was frequently downgraded to a taskbar flash and SendInput keystrokes landed in the terminal instead of KiCad. Pair with verify_loaded{kind,expectedName?} to confirm load succeeded (reads frame title). DRC trigger — run_drc_inprocess (posts WM_COMMAND 20088 to PCB Editor; ~2-5s; then poll get_drc_markers). Length-matching — get_track_paths_by_net{netName}. Net graph — get_net_topology{netName} returns components on the net + their other-net bridges (1-hop graph).\n\n\n#### `kicad_bridge_status`\n\nEnumerate every running KiCad GUI process that has the adom_bridge.py reverse-bridge plugin loaded. v1.7.6+ supports multi-instance disambiguation: discovery files are per-PID (adom-kicad-bridge-<exe>-<pid>.json) so two pcbnew windows on two different boards each get their own row. Probes each /status to confirm live + returns the full inventory.\n\n**Args:**\n- `exeName` — optional — filter to one exe (kicad / pcbnew / eeschema / kicad-cli)\n- `pid` — optional int — filter to a specific PID\n- `pruneStale` — optional bool (default false) — delete discovery files whose plugin doesn't respond to /status\n\n**Returns:** {ok, plugins:[{exeName, pid, port, version, endpoint, alive, uptimeMs, requestCount, discoveryFile, pruned?}], summary:{total, alive}, pluginAutoInstall:{...}, _hint}\n\n**Note:** Phase 2 reverse bridge (v1.7.x+). If summary.alive=0 but you expect a running KiCad, the plugin isn't deployed — call kicad_install_plugin to force-deploy. Stale discovery files (process exited mid-session) show alive=false; pass pruneStale:true to clean them up.\n\n\n#### `kicad_click`\n\nClick at coordinates in a KiCad window\n\n**Args:**\n- `hwnd` — required\n- `relative` — optional bool (default true, 0.0-1.0 range)\n- `x` — required\n- `y` — required\n\n**Note:** Relative coords are scale-independent — estimate position as percentage from screenshot.\n\n\n#### `kicad_close`\n\nClose all KiCad windows\n\n\n#### `kicad_close_3d_viewer`\n\nClose 3D Viewer window\n\n\n#### `kicad_close_footprint_editor`\n\nClose Footprint Editor window\n\n\n#### `kicad_close_symbol_editor`\n\nClose Symbol Editor window\n\n\n#### `kicad_fix_keyboard`\n\nRelease stuck modifier keys + unlock foreground focus\n\n\n#### `kicad_format_upgrade`\n\nUpgrade a KiCad file to the current format via kicad-cli. v1.7.13+. Wraps `kicad-cli {pcb,sch,sym,fp} upgrade`. MUTATES the file in place (kicad-cli has no --dry-run). Use kicad_lint_library/board/schematic first for inspection without write.\n\n**Args:**\n- `filePath` — required file or directory\n- `force` — optional bool (default false) — passes --force to resave even when already current\n- `kind` — optional pcb|sch|sym|fp (auto-detected from extension if omitted)\n\n**Returns:** {success, exitCode, kind, stdout, stderr, source_path, _hint}\n\n**Note:** Common use: lint_library reports fileFormatVersions includes old (pre-20231101) entries, surface that to the user, ask permission to upgrade, then call kicad_format_upgrade. After upgrade, ANY OPEN KiCad windows holding this file must be closed + reopened (they have stale in-memory state).\n\n\n#### `kicad_install_library`\n\nInstall a symbol or footprint library\n\n**Args:**\n- `kicadVersion` — optional (default = newest installed; library installs into THAT version's sym-lib-table)\n- `libraryName` — optional\n- `libraryPath` — required\n- `libraryType` — required\n\n\n#### `kicad_install_plugin`\n\nExplicitly (re)deploy the Phase 2 reverse-bridge plugin to every detected KiCad version's USER_SITE. Use when auto-install (which runs once per bridge boot) didn't fire or you need to force a fresh deploy after updating the plugin payload.\n\n**Args:**\n- `force` — optional bool (default true). If false, returns the cached auto-install result instead of redoing the work.\n\n**Returns:** {ok, result:{summary, payloadVersion, perVersion:[...], runningKiCad:[...], _hint}, _hint}\n\n**Note:** Idempotent — SHA256 hash compare skips a no-op deploy. If KiCad is currently running when this fires, the running processes still have the OLD plugin in memory until restart — surface result.runningKiCad to the user and tell them to close+reopen KiCad.\n\n\n#### `kicad_install_symbol`\n\nDecode base64 symbol, install as library, open editor\n\n**Args:**\n- `fileContent` — required base64\n- `fileName` — required\n- `kicadVersion` — optional (default = newest installed)\n\n\n#### `kicad_lint_board`\n\nHeadless lint pre-flight for a .kicad_pcb via kicad-cli. v1.7.12+. Comprehensive: file-existence + magic-byte + DRC with --severity-all + --all-track-errors + --schematic-parity (default on). USE THIS BEFORE kicad_open_board for any file you didn't just generate — catches DRC errors, parse issues, format-upgrade-needed BEFORE the GUI opens and possibly hangs on a modal dialog.\n\n**Args:**\n- `filePath` — required .kicad_pcb path\n- `schematicParity` — optional bool (default true) — set false if no matching .kicad_sch exists in the project dir\n\n**Returns:** {success, data:{violations[], summary:{total,errors,warnings,info}, fileFormat:{fileVersion}, schematicParityChecked, source_file}, _hint with tiered guidance}\n\n**Note:** Distinct from kicad_run_drc: lint_board adds file-format sanity + schematic-parity + an AI-friendly tiered _hint that EXPLICITLY tells you to proceed / fix / upgrade based on the result. For schematic + board cross-check during a full design pass: kicad_lint_schematic FIRST (catches ERC issues at the source), THEN kicad_lint_board (catches what made it to the board).\n\n\n#### `kicad_lint_library`\n\nValidate a KiCad library file or directory before install. v1.7.13+. Accepts a single .kicad_sym, single .kicad_mod, .pretty directory, or directory containing .kicad_sym files. Parses S-expression structure (no kicad-cli call — sym/fp upgrade has no --dry-run and would rewrite files). USE BEFORE kicad_install_library / kicad_install_symbol / kicad_install_footprint for any file you didn't just generate.\n\n**Args:**\n- `libraryPath` — required path to .kicad_sym / .kicad_mod / .pretty dir / dir with .kicad_sym files\n\n**Returns:** {success, data:{kind, summary:{symbolCount, footprintCount, symFileErrorCount, modFileErrorCount, fileFormatVersions}, symbols:[{name,pinCount,unitCount,fileVersion,generator,path}], footprints:[{name,padCount,layers,fileVersion,generator,path}]}, _hint with tiered guidance}\n\n**Note:** Detects outdated file formats (pre-KiCad-7 versions <20231101) and suggests kicad_format_upgrade. Magic-byte sniff catches wrong-file-type (e.g. .kicad_pcb mis-named .kicad_sym). Failed-parse files surface as _error rows so you can pinpoint which file in a bulk install is broken.\n\n\n#### `kicad_lint_schematic`\n\nHeadless lint pre-flight for a .kicad_sch via kicad-cli. v1.7.12+. File-existence + magic-byte + ERC with --severity-all. USE THIS BEFORE kicad_open_schematic for any file you didn't just generate.\n\n**Args:**\n- `filePath` — required .kicad_sch path\n\n**Returns:** {success, data:{violations[], summary:{total,errors,warnings,info}, fileFormat:{fileVersion}, source_file}, _hint with tiered guidance}\n\n**Note:** ERC catches schematic-only errors (unconnected pins, label mismatches, no-driver nets). For board-level validation use kicad_lint_board. Lint schematic FIRST so you fix issues at their source — DRC may surface them differently downstream.\n\n\n#### `kicad_list_versions`\n\nList every installed KiCad version with paths. Returns versions:[{version, base_dir, kicad_exe, default}], default, count.\n\n**Note:** Run this first if you're not sure which versions are installed. Pass the desired version as kicadVersion on any open_* command to override the default (newest).\n\n\n#### `kicad_open_3d_viewer`\n\nOpen KiCad 3D Viewer (board or footprint).\n\n**Args:**\n- `editor` — optional: \"auto\" (default) | \"pcb\"|\"board\" (the PCB's 3D view) | \"fp\"|\"footprint\". \"auto\" prefers the PCB/board viewer when BOTH the PCB and Footprint editors are open — so you do NOT need to close sub-editors to see the board in 3D. Pass editor=\"pcb\" to force the board, editor=\"fp\" for the active footprint.\n- `kicadVersion` — optional (default = newest installed)\n\n\n#### `kicad_open_board`\n\nOpen .kicad_pcb in PCB editor\n\n**Args:**\n- `filePath` — required\n- `kicadVersion` — optional (e.g. \"9.0\" or \"10.0\"; default = newest installed)\n\n\n#### `kicad_open_editors`\n\nCross-process inventory of every open KiCad editor frame, sourced via the reverse bridge (NOT screen scraping). Hits each alive plugin's get_open_editors method and concatenates. Replaces the kicad_window_info screenshot-poll pattern for the common case.\n\n**Returns:** {ok, editors:[{kind, exeName, pid, frameTitle, hwnd, isShown}], byKind:{schematic_editor:[...], pcb_editor:[...], symbol_editor:[...], footprint_editor:[...], project_manager:[...]}, summary:{totalEditors, probedPlugins, failedPlugins, byKindCounts}, _hint}\n\n**Note:** v1.7.6+. If totalEditors=0 but the user has KiCad open, the plugins likely aren't deployed or KiCad was launched before plugin install. Use kicad_bridge_status to confirm, kicad_install_plugin to deploy, then ask user to restart KiCad.\n\n\n#### `kicad_open_footprint_editor`\n\nOpen Footprint Editor\n\n**Args:**\n- `footprintName` — optional\n- `kicadVersion` — optional (default = newest installed)\n- `libraryName` — optional\n\n\n#### `kicad_open_schematic`\n\nOpen .kicad_sch in schematic editor\n\n**Args:**\n- `filePath` — required\n- `kicadVersion` — optional (e.g. \"9.0\" or \"10.0\"; default = newest installed)\n\n\n#### `kicad_open_symbol_editor`\n\nOpen Symbol Editor\n\n**Args:**\n- `kicadVersion` — optional (default = newest installed)\n- `libraryName` — optional\n- `symbolName` — optional\n\n\n#### `kicad_run_drc`\n\nRun Design Rule Check on a PCB via kicad-cli (headless, fast, structured JSON). Equivalent to right-clicking DRC in pcbnew but no GUI. Returns {data:{violations[], summary:{total,errors,warnings}}}.\n\n**Args:**\n- `filePath` — required\n- `kicadVersion` — optional (default = newest installed)\n\n**Note:** v1.7.12+ ships kicad_lint_board with the same DRC + file-format checks + schematic-parity in one structured response — prefer it over kicad_run_drc for new code.\n\n\n#### `kicad_run_erc`\n\nRun Electrical Rule Check on a .kicad_sch via kicad-cli. v1.7.12+. Mirrors kicad_run_drc but for schematics — catches unconnected pins, duplicate references, hierarchical-label mismatches, no-driver nets, etc. Headless, ~1-3s, structured JSON.\n\n**Args:**\n- `filePath` — required .kicad_sch path\n\n**Returns:** {success, data:{violations[], summary:{total,errors,warnings,info}, fileFormat:{fileVersion}, source_file}, _hint guiding next steps}\n\n\n#### `kicad_screenshot_all`\n\nScreenshot all KiCad windows in one shot\n\n**Returns:** Array of {type, title, hwnd, savedTo, sizeKB}\n\n**Note:** Images downscaled to max 1568px. Use relative coords (0.0-1.0) for clicking — they map to actual window size regardless of image scale.\n\n\n#### `kicad_send_key`\n\nSend keystroke to KiCad window. Supports modifier combos.\n\n**Args:**\n- `hwnd` — optional target HWND\n- `key` — required: a named key (enter/escape/tab/space/f1-f12), a single char, OR a modifier combo like \"ctrl+s\", \"alt+3\", \"shift+f10\", \"ctrl+shift+s\". Modifiers: ctrl/control, alt/menu, shift, win/super. Case-insensitive.\n\n**Note:** Menu hotkeys work via combos (e.g. \"alt+3\" = View>3D Viewer in pcbnew, \"ctrl+s\" = save). For dialogs prefer 'enter'/'escape'. Use click only if tab order doesn't reach the button.\n\n\n#### `kicad_window_info`\n\nList all KiCad windows (project manager, editors, dialogs) with HWNDs and rects\n\n\n### Fusion 360 bridge\n\n_Drive Fusion 360: CAD export, cloud docs, EAGLE electronics, manufacturing, UI._ (63 verbs)\n\n> **Note:** Two-tier architecture: bridge (port 8773) handles launch/detection; AdomBridge add-in (port 8774) handles exports/queries inside Fusion.\n\n> **Workflow:** fusion_start -> fusion_open_cloud_file or fusion_open_board -> work -> fusion_close\n\n#### `fusion_export_3mf`\n\nExport active design as 3MF\n\n**Args:**\n- `outputPath` — required\n\n**Prerequisite:** Active 3D design\n\n\n#### `fusion_export_dwg`\n\nExport active design as DWG\n\n**Args:**\n- `outputPath` — required\n\n**Note:** Not available via API (dialog only). Use fusion_export_step or fusion_export_dxf instead.\n\n**Prerequisite:** Active 3D design\n\n\n#### `fusion_export_dxf`\n\nExport active design as DXF\n\n**Args:**\n- `outputPath` — required\n\n**Note:** Not available via API (dialog only). Use fusion_export_step or fusion_export_iges instead.\n\n**Prerequisite:** Active 3D design\n\n\n#### `fusion_export_f3d`\n\nExport active design as Fusion 360 archive (.f3d)\n\n**Args:**\n- `outputPath` — required\n\n**Note:** Native Fusion format — preserves parametric features, history, component references\n\n**Prerequisite:** Active 3D design\n\n\n#### `fusion_export_fbx`\n\nExport active design as FBX\n\n**Args:**\n- `outputPath` — required\n\n**Note:** FBX only — no fallback. Not available in current Fusion builds. Use fusion_export_usdz or fusion_export_step instead.\n\n**Prerequisite:** Active 3D design\n\n\n#### `fusion_export_iges`\n\nExport active design as IGES (.igs)\n\n**Args:**\n- `outputPath` — required\n\n**Note:** Legacy CAD interchange. STEP is preferred for modern workflows.\n\n**Prerequisite:** Active 3D design\n\n\n#### `fusion_export_obj`\n\nExport active design as OBJ\n\n**Args:**\n- `outputPath` — required\n\n**Note:** Not available via API (dialog only). Use fusion_export_stl or fusion_export_usdz instead.\n\n**Prerequisite:** Active 3D design\n\n\n#### `fusion_export_sat`\n\nExport active design as SAT (.sat, ACIS solid model)\n\n**Args:**\n- `outputPath` — required\n\n**Note:** ACIS kernel format — opens in SolidWorks, SpaceClaim.\n\n**Prerequisite:** Active 3D design\n\n\n#### `fusion_export_skp`\n\nExport active design as SKP (SketchUp)\n\n**Args:**\n- `outputPath` — required\n\n**Note:** Not available via API (dialog only). Use fusion_export_step or fusion_export_stl instead.\n\n**Prerequisite:** Active 3D design\n\n\n#### `fusion_export_step`\n\nExport active design as STEP\n\n**Args:**\n- `outputPath` — required\n\n**Prerequisite:** Active 3D design (not Electronics)\n\n\n#### `fusion_export_stl`\n\nExport active design as STL\n\n**Args:**\n- `outputPath` — required\n\n**Prerequisite:** Active 3D design\n\n\n#### `fusion_export_usdz`\n\nExport active design as USDZ (best for digital twins + GLB)\n\n**Args:**\n- `outputPath` — required\n\n**Note:** Preserves component hierarchy, PBR materials, named nodes. Ideal for Blender→GLB or direct iOS/macOS AR viewing.\n\n**Prerequisite:** Active 3D design\n\n\n#### `fusion_get_design_info`\n\nGet active design metadata (bodies, components)\n\n**Prerequisite:** Active 3D design\n\n\n#### `fusion_get_parameters`\n\nList user parameters from active design\n\n\n#### `fusion_import_step`\n\nImport STEP/STL/IGES/SAT/OBJ/F3D file into Fusion\n\n**Args:**\n- `filePath` — required Windows path\n\n\n#### `fusion_set_parameter`\n\nModify a user parameter\n\n**Args:**\n- `name` — required\n- `value` — required\n\n\n#### `fusion_take_screenshot`\n\nCapture Fusion viewport as PNG with camera orientation control\n\n**Args:**\n- `height` — optional (default 1080)\n- `orientation` — optional: home, front, back, top, bottom, left, right\n- `outputPath` — required\n- `width` — optional (default 1920)\n\n**Note:** Uses Fusion's render API (Capture Image) — not a window screenshot. Supports all standard views for product images/icons.\n\n\n#### `fusion_check_recovery`\n\nCheck if a cloud file has crash recovery data\n\n**Args:**\n- `fileName` — required\n- `folderPath` — optional\n- `projectName` — optional\n\n**Note:** Check BEFORE opening files to avoid blocking recovery dialog\n\n\n#### `fusion_create_cloud_folder`\n\nCreate folder (idempotent)\n\n**Args:**\n- `folderName` — required\n- `parentPath` — optional\n- `projectName` — optional\n\n\n#### `fusion_delete_cloud_file`\n\nDelete a cloud file (must not be open)\n\n**Args:**\n- `fileName` — required\n- `folderPath` — optional\n- `projectName` — optional\n\n\n#### `fusion_export_cloud_file`\n\nExport active document to local file (step/stl/f3d/iges/sat/smt)\n\n**Args:**\n- `format` — optional (default: step)\n- `outputPath` — required\n\n\n#### `fusion_list_cloud_files`\n\nList files and subfolders in a cloud project/folder\n\n**Args:**\n- `folderPath` — optional\n- `projectName` — optional\n\n\n#### `fusion_list_cloud_projects`\n\nList all cloud projects in user's hub\n\n\n#### `fusion_open_cloud_file`\n\nOpen a cloud file by name. STOPS if recovery exists — you must specify recovery='open' or recovery='discard'\n\n**Args:**\n- `fileName` — required\n- `folderPath` — optional\n- `projectName` — optional\n- `recovery` — optional: open|discard\n\n\n#### `fusion_save_to_cloud`\n\nSave active document to Fusion cloud\n\n**Args:**\n- `description` — optional\n- `folderPath` — optional\n- `name` — required\n- `projectName` — optional\n\n\n#### `fusion_search_cloud_files`\n\nSearch a cloud project / subfolder by file-name substring. Returns matches + a detailed scope analysis. Use this for targeted lookups; for exhaustive whole-cloud walks prefer fusion_walk_cloud_tree.\n\n**Args:**\n- `folderPath` — optional starting subfolder (default: project root). NARROW THE SCOPE HERE — naming the right subfolder can reduce search time 10-50x.\n- `folderTimeout` — optional int seconds per-folder cloud-API timeout (default 30)\n- `maxDepth` — optional int (default 2, no upper cap)\n- `maxFolders` — optional int (default 10, no upper cap). For exhaustive 100+ folder searches consider walk_cloud_tree.\n- `maxResults` — optional int (default 20, no upper cap)\n- `projectName` — optional (defaults to active project)\n- `query` — required case-insensitive substring of the file name\n- `recursive` — optional bool (default false). false = root folder only (fast); true = descend into subfolders.\n- `searchTimeout` — optional int seconds overall wall-clock timeout (default 120). If you set this >180, also pass timeout:<N> to the bridge so the HTTP waiter doesn't give up first.\n\n**Returns:** data{files[], totalFound, foldersSearched, foldersSkipped, filesSkipped, searchComplete, truncated, folderLimitReached, searchTimedOut, elapsedSeconds, costAnalysis{foldersPerSecond, estimatedSecondsPer100Folders}, hint}\n\n**Note:** All defenses (per-folder timeout, doEvents, per-file try/except, watchdog) make hard caps unnecessary. Defaults are conservative; raise maxFolders/maxDepth/maxResults for thorough searches.\n\n\n#### `fusion_walk_cloud_tree`\n\nWalk a cloud folder tree iteratively (BFS) and return a flat list of folders + files with full paths.\n\n**Args:**\n- `extensions` — optional list of extensions to include, e.g. [\"f3d\",\"fprj\"]\n- `folderPath` — optional starting folder (defaults to root)\n- `includeFiles` — optional bool (default true) — set false for folder-only tree\n- `maxDepth` — optional int (default 10)\n- `maxFolders` — optional int (default 500, truncates if exceeded)\n- `nameContains` — optional case-insensitive substring filter on file names\n- `projectName` — optional (defaults to active)\n\n**Returns:** folders[], files[] (each with path), stats{foldersVisited, filesFound, truncated}\n\n**Note:** Single add-in call does the entire walk. No sleeps, no batching. For targeted finds use nameContains; for exports use extensions.\n\n\n#### `fusion_board_info`\n\nGet structured board data: components, nets, traces, DRC violations\n\n**Note:** Much richer than a screenshot — exact coordinates and connectivity\n\n**Prerequisite:** Board open in PCB Editor\n\n\n#### `fusion_electron_run`\n\nExecute EAGLE command (WINDOW FIT, DISPLAY ALL, RATSNEST, DRC, etc.)\n\n**Args:**\n- `command` — required EAGLE command string\n\n**Returns:** activeWorkspace, fusionOperations[] (diff of internal log), _hint\n\n**Note:** Fire-and-forget: never returns output. Use fusion_board_info for data. Full command reference with safe/blocking split: skills/eagle-commands.md (ships next to this CLI binary). Avoid blocking commands: WRITE, ADD, SHOW, MOVE, CHANGE, EDIT.\n\n\n#### `fusion_execute_text_command`\n\nLow-level Fusion app.executeTextCommand() access\n\n**Args:**\n- `command` — required Fusion text command\n\n\n#### `fusion_export_eagle_source`\n\nExport active electronics document as plain EAGLE .sch or .brd XML — extracts from Fusion's .fsch/.fbrd ZIP container\n\n**Args:**\n- `outputPath` — required — full path ending in .sch or .brd\n\n**Note:** Produces valid EAGLE XML (<?xml><eagle version=...>) parseable by standalone EAGLE, KiCad import, or any XML tool. For Fusion-native format use fusion_export_source instead.\n\n**Prerequisite:** MUST have correct editor active: PCB Editor / Board Layout for .brd, Schematic Editor for .sch\n\n\n#### `fusion_export_lbr`\n\nExport library as .scr script (no 3D refs)\n\n**Args:**\n- `outputPath` — optional\n\n\n#### `fusion_export_source`\n\nExport active electronics document as .fsch (schematic), .fbrd (board), or .flbr (library) source file\n\n**Args:**\n- `outputPath` — required — full path with extension (.fsch, .fbrd, or .flbr)\n\n**Note:** Always export .fbrd FIRST (from board view), then switch to schematic for .fsch. If file not created, wrong workspace is active — screenshot to check.\n\n**Prerequisite:** MUST have correct editor active: Board Layout for .fbrd, Schematic Editor for .fsch\n\n\n#### `fusion_import_electronics`\n\nImport Fusion-native .fsch/.fbrd/.flbr file as new local electronics project\n\n**Args:**\n- `filePath` — required Windows path to .fsch, .fbrd, or .flbr file\n\n**Note:** Creates a NEW local document via Document.newDesignFromLocal. Use fusion_save_to_cloud to persist afterward. For legacy EAGLE files use fusion_open_schematic (.sch), fusion_open_board (.brd), or fusion_open_lbr (.lbr).\n\n\n#### `fusion_list_text_commands`\n\nList available EAGLE text commands in current context\n\n\n#### `fusion_open_board`\n\nOpen .brd in Board Layout editor\n\n**Args:**\n- `filePath` — required\n\n\n#### `fusion_open_electronics`\n\nCheck if Electronics workspace is active\n\n\n#### `fusion_open_lbr`\n\nOpen .lbr library in Electronics Library editor\n\n**Args:**\n- `filePath` — required\n\n\n#### `fusion_open_schematic`\n\nOpen .sch in Schematic Editor\n\n**Args:**\n- `filePath` — required\n\n\n#### `fusion_show_2d_board`\n\nSwitch back to 2D board layout from 3D view\n\n**Note:** EAGLE commands only work in 2D view\n\n\n#### `fusion_show_3d_board`\n\nSwitch to 3D PCB view (board needs outline on layer 20)\n\n**Prerequisite:** Board must be open in PCB Editor\n\n\n#### `fusion_activate_document`\n\nSwitch to an open document tab by name or type\n\n**Args:**\n- `documentType` — optional: Electronics|PCB|FusionDesign|Drawing\n- `name` — optional substring match\n\n\n#### `fusion_close`\n\nClose Fusion 360 gracefully\n\n\n#### `fusion_close_all_documents`\n\nClose all open documents\n\n**Args:**\n- `saveChanges` — optional bool (default: false)\n\n**Note:** Call before force-killing Fusion to prevent recovery files\n\n\n#### `fusion_close_document`\n\nClose a document without save dialog (avoids modal blocking)\n\n**Args:**\n- `name` — optional (default: active)\n- `save` — optional bool (default: false)\n\n\n#### `fusion_close_window`\n\nClose a specific Fusion dialog window by sending WM_CLOSE (equivalent to clicking its X button). Works on dialogs that Escape doesn't close — e.g. 'Recovered Documents'.\n\n**Args:**\n- `hwnd` — required — HWND from fusion_window_info or fusion_dismiss_blocking_dialogs remaining[]\n\n**Note:** Does NOT force-kill — the dialog can still prompt for confirmation. Use when fusion_dismiss_blocking_dialogs reports remaining dialogs that Escape couldn't clear.\n\n\n#### `fusion_dismiss_blocking_dialogs`\n\nFirst-class blocking-dialog killer. Enumerates all Fusion Qt windows, matches them against a known list of blocking/picker patterns ('What do you want to design?', error toasts, recovery prompts, etc.), and sends Escape to each offending HWND until the add-in main thread reports responsive. Whenever any fusion_* command returns errorCode 'fusion_addin_not_responding', call this FIRST before retrying.\n\n**Args:**\n- `extraPatterns` — optional array of case-insensitive title substrings to also treat as blocking\n- `maxAttempts` — optional int (default 5) — retry loop cap\n\n**Returns:** dismissed[{hwnd,title,pattern}], remaining[], mainThreadResponsive, attempts\n\n**Note:** Idempotent. Safe to call any time. Returns immediately with empty dismissed[] when no blocking dialogs are present.\n\n\n#### `fusion_document_info`\n\nList all open document tabs with cloud metadata for active doc\n\n**Returns:** openDocuments[], activeDoc cloud details (project, folder, version)\n\n\n#### `fusion_get_app_state`\n\nQuery open documents, active workspace, Electronics status\n\n**Returns:** workspace, openDocuments[], electronicsActive\n\n\n#### `fusion_start`\n\nFirst-class Fusion 360 startup. Discovers Fusion360.exe via glob (so version-indirected webdeploy hashes don't matter), verifies it exists BEFORE spawning (no 'Windows cannot find' GUI dialogs), launches it if not already running, waits for the AdomBridge add-in to come online, and auto-dismisses any blocking dialogs via fusion_dismiss_blocking_dialogs. Returns when Fusion is fully ready.\n\n**Args:**\n- `dismissBlockingDialogs` — optional bool (default true) — auto-clear the 'What do you want to design?' picker, error toasts, and other known blocking modals\n- `timeoutSeconds` — optional int (default 120) — total wait for add-in + dialog dismissal\n\n**Returns:** resolvedPath, pid (null if already running), addinReady, dialogsDismissed[], elapsedMs, alreadyRunning\n\n**Note:** ALWAYS use this instead of launching Fusion by hand. It's idempotent — safe to call when Fusion is already running. Process-detection gate means it will NEVER trigger Autodesk's 'Multiple instances are not supported' dialog.\n\n\n#### `fusion_detect_layers`\n\nAuto-detect PCB copper layer count (2 or 4) using ULP + CAM comparison\n\n**Returns:** layerCount, is4Layer, method, copperLayers[], nextSteps[]\n\n**Note:** Run first — informs gerber CAM job selection and design rule choice\n\n\n#### `fusion_export_board_image`\n\nExport PNG image with layer presets (top_copper, assembly_top, fabrication, etc.)\n\n**Args:**\n- `dpi` — optional int (default: 300, max: 600)\n- `layers` — optional int[] — custom layer numbers (overrides preset)\n- `listPresets` — optional bool — returns preset list instead of exporting\n- `monochrome` — optional bool\n- `outputPath` — optional (default: C:/tmp/adom-board.png)\n- `preset` — optional: all|top_copper|bottom_copper|top_silkscreen|bottom_silkscreen|top_soldermask|bottom_soldermask|top_paste|bottom_paste|board_outline|drill|assembly_top|assembly_bottom|fabrication\n\n**Returns:** outputPath, dpi, preset, fileSize, nextSteps[]\n\n\n#### `fusion_export_bom`\n\nExport Bill of Materials as CSV (grouped by value+package)\n\n**Args:**\n- `grouped` — optional bool (default: true) — false for one row per component\n- `outputPath` — optional (default: C:/tmp/adom-bom.csv)\n\n**Returns:** outputPath, componentCount, uniquePartCount, nextSteps[]\n\n**Note:** Format: Comment, Designator, Footprint, Quantity, Library\n\n\n#### `fusion_export_cpl`\n\nExport Component Placement List (pick-and-place) as CSV\n\n**Args:**\n- `outputPath` — optional (default: C:/tmp/adom-cpl.csv)\n- `side` — optional: all (default) | top | bottom\n\n**Returns:** outputPath, totalPlacements, topCount, bottomCount, nextSteps[]\n\n**Note:** Format: Designator, Mid X, Mid Y, Layer, Rotation (mm). Compatible with JLCPCB/PCBWay.\n\n\n#### `fusion_export_gerbers`\n\nExport Gerber RS-274X + Excellon drill files as ZIP (auto-detects layers, selects CAM job)\n\n**Args:**\n- `boardName` — optional (default: from active doc name)\n- `layers` — optional: auto | 2 | 4\n- `outputDir` — optional (default: C:/tmp/adom-gerbers/)\n\n**Returns:** zipPath, zipSizeKB, layerCount, files[], nextSteps[]\n\n**Note:** ZIP contains GTL, GBL, GTS, GBS, GTP, GBP, GTO, GBO, GKO, XLN. Ready for fab upload.\n\n\n#### `fusion_set_design_rules`\n\nApply Adom/JLCPCB design rules (.edru files) — auto-detects 2/4 layer\n\n**Args:**\n- `action` — optional: apply (default) | export | show\n- `layers` — optional: auto (default) | 2 | 4\n- `outputPath` — optional, for export action\n\n**Returns:** edruFile, layerCount, description, nextSteps[]\n\n**Note:** Run DRC after applying rules to check for violations\n\n\n#### `fusion_dismiss_recovery`\n\nDismiss recovery dialogs + relocate recovery files to ~/.adom/recovery/\n\n**Note:** Handles both 'Recovered Documents' list and 'Open recovery instead?' prompts\n\n\n#### `fusion_relocate_recovery`\n\nMove crash recovery files to ~/.adom/recovery/fusion/ (call before launch to prevent dialogs)\n\n**Note:** Files preserved, not deleted. User can manually restore if needed.\n\n\n#### `fusion_click_fusion`\n\nClick at coordinates in Fusion window or a specific dialog\n\n**Args:**\n- `hwnd` — optional — target a specific dialog HWND instead of main window\n- `relative` — optional bool (default: true, 0.0-1.0)\n- `x` — required float\n- `y` — required float\n\n**Note:** Relative coords (0.0-1.0) are scale-independent — estimate position as percentage from screenshot. Prefer fusion_send_key for dialogs (enter/escape/tab covers most cases).\n\n\n#### `fusion_screenshot_all`\n\nScreenshot main window + list all dialog windows with HWNDs\n\n**Note:** Use fusion_screenshot_fusion with dialog HWND, or desktop_screenshot_window\n\n\n#### `fusion_screenshot_fusion`\n\nCapture Fusion main window or a specific dialog as PNG\n\n**Args:**\n- `hwnd` — optional — dialog HWND to screenshot instead of main window\n\n**Returns:** savedTo file path\n\n**Note:** Image is downscaled to max 1568px. Use relative coords (0.0-1.0) for clicking — they map to actual window size regardless of image scale.\n\n\n#### `fusion_send_key`\n\nSend keyboard input to Fusion or a specific dialog\n\n**Args:**\n- `hwnd` — optional — target a specific dialog HWND\n- `key` — required string\n\n**Note:** Preferred way to dismiss dialogs: 'enter' to confirm, 'escape' to cancel, 'tab' to cycle buttons. Use click only if tab order doesn't reach the right button.\n\n\n#### `fusion_window_info`\n\nGet main window HWND, title, rect, and all Qt dialog windows\n\n**Returns:** hwnd, title, rect, dialogs[{hwnd, title, className, rect}]\n\n**Note:** Essential for detecting blocking dialogs after operations\n\n\n### Hydrogen Desktop\n\n_Build, ship, screenshot, and drive the embedded Hydrogen Desktop._ (22 verbs)\n\n> **Note:** v1.8.15+: hd_* verbs proxy to Hydrogen Desktop's local API on 127.0.0.1:9001. HD is a sibling Tauri v2 app that runs alongside adom-desktop on the user's machine. These are built-in proxy handlers (NOT a separate bridge process) — when HD isn't running, hd_status returns {ok:false, error:'HD not running'} cleanly instead of crashing.\n\n#### `hd_api`\n\nGeneric HTTP pass-through to HD's control API. The canonical way to call ANY HD endpoint from a script/AI without shelling out to curl (which the relay mangles). Auto-discovers HD's control port from %APPDATA%/hydrogen-desktop/ports.json (key 'control'); falls back to 47084. Public-build denylist refuses destructive paths (e.g. /wsl/unregister, /setup/virgin-reset) — use the hd-dev internal build for those.\n\n**Args:**\n- `body` — optional object — JSON body for POST/PUT/PATCH. Passed through verbatim (no escaping).\n- `method` — optional string (default 'GET') — 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n- `path` — required string starting with '/' — e.g. '/setup/run-step', '/wsl/status', '/claude/start-auth'\n- `port` — optional int — override discovered port; for testing\n\n**Returns:** {success:bool, ok:bool, status:int, body:any, method:str, path:str, port:int} — `body` is the parsed JSON HD returned (or a string if non-JSON)\n\n**Example:** `adom-desktop hd_api '{\"method\":\"POST\",\"path\":\"/setup/run-step\",\"body\":{\"id\":\"welcome\"}}'`\n\n\n#### `hd_browser_profiles`\n\nEnumerate browsers + profiles HD detects on the user's machine (GET /browser-profiles). Use the returned profileDir values when calling hd_open_url.\n\n**Returns:** HD's /browser-profiles response — typically {browsers:[{name, profiles:[{dir, name, gaia?}, ...]}]}\n\n\n#### `hd_build`\n\nFull HD rebuild (async): git pull → pnpm build → cargo build (debug). Returns immediately with {pid, logPath}; the build runs detached so the relay's 30s HTTP timeout doesn't kill it. Each step appends to %TEMP%\\hd-build.log (overwritten at start); the script writes BUILD_OK or BUILD_FAILED:<reason> as its final line so callers can detect completion without scraping tool-specific output. v1.8.16+.\n\n**Args:**\n- `lingerSecs` — optional int (default 30 when show=true, 0 when show=false) — v1.8.23+. After the build's final BUILD_OK/BUILD_FAILED line, the visible window stays open for this many seconds before auto-closing. 30s default lets the user read the error; pass 0 to close immediately; pass a larger number for more reading time. Capped at 3600s. Ignored entirely when show=false (no window to linger).\n- `show` — optional bool (default false) — true opens a visible console window the user can watch the build scroll in; false runs hidden\n\n**Returns:** {ok, pid, logPath, flavor:'full', show, lingerSecs, _hint}\n\n**Note:** DO NOT call hd_launch until hd_build_status shows succeeded=true. Poll hd_build_status or stream output with hd_build_tail {offset:N}. Build PID is written to %TEMP%\\hd-build.pid by the PowerShell script itself so it's authoritative regardless of the wrapper process tree. v1.8.23+: visible build windows now auto-close after lingerSecs (default 30) so build-heavy sessions don't accumulate orphan consoles.\n\n\n#### `hd_build_frontend`\n\nFrontend-only HD build (async): `pnpm build` from the repo root. Same log/PID/sentinel conventions as hd_build. Use when only Svelte/TypeScript changed and the Rust binary is unchanged — much faster than a full build.\n\n**Args:**\n- `lingerSecs` — optional int (default 30 when show=true) — see hd_build for details. v1.8.23+.\n- `show` — optional bool (default false)\n\n**Returns:** On dispatch success: {ok, pid, logPath, flavor:'frontend', show, lingerSecs, builtSha, exeMtimeBefore, exeSizeBefore, _hint}. On lock contention (v1.8.57+): {ok:false, refused:true, failedAt:'lock', verb, holderPid, holderVerb, sha, elapsedSec, narrative, _next} — another hd_build_*/hd_ship is already running. NO QUEUE: just refused immediately. The lock auto-releases when the holder's PowerShell PID dies.\n\n**Note:** Skip when the Rust src-tauri/ side changed — those need hd_build or hd_build_rust to rebuild the binary. v1.8.57+ adds a global build lock — two concurrent hd_build_*/hd_ship calls cannot race.\n\n\n#### `hd_build_log`\n\nReturn the FULL build log from %TEMP%\\hd-build.log. Use this after hd_build_status reports failed to read the complete output and find the error line.\n\n**Returns:** {ok, log:'<full text>', lines, logPath, _hint}\n\n\n#### `hd_build_rust`\n\nRust-only HD build (async): `cargo build` from src-tauri/. Same log/PID/sentinel conventions as hd_build. Use when only Rust code changed (no frontend changes). v1.8.52+: dispatch response also carries builtSha + exeMtimeBefore + exeSizeBefore (captured at spawn time into %TEMP%\\hd-build-meta.json) so the subsequent hd_build_status / hd_launch don't need a separate git rev-parse + stat call.\n\n**Args:**\n- `lingerSecs` — optional int (default 30 when show=true) — see hd_build for details. v1.8.23+.\n- `show` — optional bool (default false)\n\n**Returns:** {ok, pid, logPath, flavor:'rust', show, lingerSecs, builtSha, exeMtimeBefore, exeSizeBefore, _hint}\n\n**Note:** Builds the debug binary at target/debug/hydrogen-desktop.exe. The frontend's build/ output is NOT regenerated — re-use whatever was last built by hd_build / hd_build_frontend.\n\n\n#### `hd_build_status`\n\nSynchronous build-state probe. v1.8.52+ enriched: returns compiledCrates (parsed from 'Compiling X' lines so you KNOW your crate recompiled vs cargo found nothing to do — eliminates the most common stale-relink trap), relinkOnly (true when succeeded but compiledCrates is empty), exeMtimeBefore/After + exeSizeBefore/After + exeChanged (proves the binary actually got rewritten), exeLocked + holderPids (when cargo failed because hydrogen-desktop.exe is held by another process — most often a still-running HD instance), builtSha (the SHA hd_build_rust / hd_build was dispatched at — compare to /buildinfo.build_sha after launch), elapsedSecs (from dispatch to now). State machine: no log → idle; log present without sentinel + PID alive → building; log ends with BUILD_OK → succeeded; log ends with BUILD_FAILED → failed; log present without sentinel + PID dead → treated as failed (orphaned). v1.8.16+.\n\n**Returns:** {ok:bool, building:bool, succeeded:bool, failed:bool, lastLines:[string] (last ~40 lines of stderr), logPath:string, pid:int (cargo PID or 0), pidAlive:bool, failureReason:string (when failed:true — e.g. 'compile_error', 'exe_locked', 'orphaned_no_sentinel'), compiledCrates:[string] (names parsed from 'Compiling X v...' lines — EMPTY means relink-only, your code didn't actually rebuild), relinkOnly:bool (true iff succeeded:true && compiledCrates empty), exeChanged:bool (mtime OR size after differs from before — definitive proof a fresh binary was written), exeMtimeBefore:int (unix seconds), exeMtimeAfter:int (unix seconds), exeSizeBefore:int (bytes), exeSizeAfter:int (bytes), exeLocked:bool (true when build failed because hydrogen-desktop.exe was held by another PID), holderPids:[int] (the PIDs holding the exe — taskkill these), builtSha:string|null (the git SHA hd_build was dispatched at — compare to /buildinfo.build_sha after launch), elapsedSecs:int, flavor:string ('rust'|'frontend'|'full'), _hint:string}\n\n**Note:** **relinkOnly:true on success is the #1 trap to look for** — your push didn't actually change the binary because cargo found nothing to recompile. Usually means your source changes weren't pulled, or you changed a file the workspace doesn't track. Use hd_ship with clean:true to force a fresh build. **exeLocked:true on failure** plus holderPids tells you exactly which PID to taskkill; or just use hd_ship which kills HD before building.\n\n\n#### `hd_build_tail`\n\nStreaming tail of the build log. Pass offset=0 on first call; the response includes newOffset which you pass back on the next call to get just the new bytes. Lines are split on \\n; a partial trailing line (no newline yet) is held back and re-included on the next call. Use this to follow a running build like `tail -f` without re-fetching the whole log every poll. v1.8.16+.\n\n**Args:**\n- `offset` — optional int (default 0) — byte offset into the log file from the previous call's newOffset\n\n**Returns:** {ok, lines:[], newOffset, totalBytes, done:bool, succeeded:bool, failureReason, _hint}\n\n**Note:** done=true means BUILD_OK or BUILD_FAILED has appeared in the log — stop polling. done=false means more output expected; sleep 2-3s and call again with newOffset.\n\n\n#### `hd_container_exec`\n\n[MOVED to hd-dev in v1.8.72] Run a command inside HD's Docker container. Public Adom Desktop refuses with verb_moved_to_hd_dev. Workaround: hd_api '{\"method\":\"POST\",\"path\":\"/container-exec\",\"body\":{\"command\":\"...\"}}'.\n\n**Args:**\n- `command` — required string\n\n**Returns:** Public build: refusal payload. hd-dev internal build: HD's /container-exec response.\n\n\n#### `hd_eval`\n\n[MOVED to hd-dev in v1.8.72] Evaluate JS in HD's main webview. Public Adom Desktop returns {error:'verb_moved_to_hd_dev'} with a _hint pointing at the internal build. Workaround: hd_api '{\"method\":\"POST\",\"path\":\"/eval\",\"body\":{\"js\":\"...\"}}'.\n\n**Args:**\n- `js` — required string — JS expression or statement\n\n**Returns:** Public build: refusal payload. hd-dev internal build: HD's /eval response.\n\n\n#### `hd_iframe_eval`\n\n[MOVED to hd-dev in v1.8.72] Evaluate JS inside HD's code-server iframe via CDP. Public Adom Desktop refuses with verb_moved_to_hd_dev. Workaround: hd_api '{\"method\":\"POST\",\"path\":\"/iframe-eval\",\"body\":{\"js\":\"...\"}}'.\n\n**Args:**\n- `contextIndex` — optional int\n- `js` — required string\n\n**Returns:** Public build: refusal payload. hd-dev internal build: HD's /iframe-eval response.\n\n\n#### `hd_launch`\n\nStart HD's debug binary detached. v1.8.52+ returns structured launch state so you NEVER mistake 'attached to a stale running process' for 'launched fresh code'. Three guards refuse the launch: (1) build_in_progress — a hd_build/hd_build_frontend/hd_build_rust is still running; (2) build_failed — last build ended with BUILD_FAILED; (3) already_running — hydrogen-desktop.exe is in the process list. The already_running guard can be overridden with `killExisting:true` (taskkills the existing instance then spawns fresh; killedFirst=true in the response). On success, sleeps briefly then reports HD's actual PID (looked up via tasklist) since the cmd-wrapper PID is gone immediately. v1.8.16+.\n\n**Args:**\n- `killExisting` — optional bool (default false). When false and HD is already running, refuse with reason=already_running so the caller doesn't accidentally attach to a stale process. When true, taskkill the existing instance with a 600ms grace period before launching fresh; killedFirst=true in the response.\n\n**Returns:** On launch: {ok:true, launched:true, wasAlreadyRunning:bool, pid:int (HD's actual PID, not the cmd-wrapper), killedFirst:bool (true iff killExisting was honored), exePath:string, builtSha:string|null (the SHA the binary was built at — compare to /buildinfo.build_sha to confirm fresh code), _hint}. On refusal: {ok:false, launched:false, wasAlreadyRunning:bool, pid:int|null, killedFirst:false, reason:string, _hint}. reason ∈ {'build_in_progress' (a hd_build/hd_build_rust/hd_build_frontend is still running — wait for hd_build_status.building:false), 'build_failed' (last build ended with BUILD_FAILED — fix and retry hd_build), 'already_running' (HD is up — pass killExisting:true to force-kill+relaunch, or use hd_restart), 'binary_missing' (target/debug/hydrogen-desktop.exe not on disk — run hd_build first)}.\n\n**Note:** **The launched + wasAlreadyRunning + killedFirst combination is the proof you need** — `launched:true, wasAlreadyRunning:false, killedFirst:false` means truly fresh code is running; any other combination requires you to think about whether your changes actually took effect. builtSha is included so you can immediately compare to /buildinfo.build_sha without a separate call.\n\n\n#### `hd_log`\n\nRead the tail of HD's log file at %APPDATA%\\hydrogen-desktop\\hydrogen-desktop.log. Doesn't require HD to be running — reads the file directly from disk. Use this when HD seems stuck or has crashed to see what it said last.\n\n**Args:**\n- `tail` — optional int (default 30) — number of trailing lines to return\n\n**Returns:** {ok, lines:[...], path, totalLines, returnedLines}\n\n\n#### `hd_open_url`\n\nOpen a URL in a specific browser profile via HD's /open-in-profile endpoint. HD knows which browsers + profiles are installed (see hd_browser_profiles) and dispatches to the right one with the right --profile-directory / -P flag.\n\n**Args:**\n- `browser` — optional (default 'chrome') — 'chrome', 'edge', 'firefox', 'brave'\n- `profileDir` — optional (default 'Default') — profile directory name from hd_browser_profiles\n- `url` — required string\n\n**Returns:** HD's /open-in-profile response (typically {ok, pid, profile})\n\n\n#### `hd_reload_vscode`\n\n[MOVED to hd-dev in v1.8.72] Reload the VS Code iframe inside HD. Public Adom Desktop refuses with verb_moved_to_hd_dev. Workaround: hd_api '{\"method\":\"POST\",\"path\":\"/reload-vscode\"}' (allowed by the public denylist).\n\n**Returns:** Public build: refusal payload. hd-dev internal build: HD's /reload-vscode response.\n\n\n#### `hd_restart`\n\nStop + launch in one call. Same guards as hd_launch (build_in_progress / build_failed / binary_missing all refuse). If HD is running it's taskkilled with a 600ms grace period before relaunch to release the exe lock. v1.8.16+.\n\n**Returns:** {ok, killedPid?, pid, exePath, _hint}\n\n**Note:** Use after `hd_build_status` shows succeeded=true to pick up the new binary. Don't call when build is in progress — the launch will fail.\n\n\n#### `hd_screenshot`\n\nCapture the Hydrogen Desktop window as a lossless PNG. Finds the window by title ('Hydrogen Desktop') via the same Win32 enum the desktop bridge uses, then captures via PrintWindow.\n\n**Returns:** {success, savedTo, sizeKB, hwnd, data:{image (base64), format:'png', encoding, sizeBytes}}\n\n**Note:** Requires HD to be running AND its window to be visible (PrintWindow can't capture a hidden window). If HD isn't found, returns 'Hydrogen Desktop window not found' — verify HD is up via hd_status.\n\n\n#### `hd_send_files`\n\nSend local files DIRECTLY into Hydrogen Desktop's WSL2 project. Relays the bytes to HD's control API, which writes them (as adom) into <project>/<subdir> (default 'downloads', created on first use). Lands in the WSL2 ext4 filesystem so the AI sees them IMMEDIATELY — avoids the plain send_files path (→ Windows Downloads → /mnt/c mount) whose ~5s WSL2 listing-cache makes freshly-dropped files look missing. HD owns the write (it knows its container/bind-mount layout + runs as adom), so ownership + visibility are correct. Requires HD running (hd_launch) and an HD build exposing POST /files/inject.\n\n**Args:**\n- `filePaths` — required array of absolute local file paths to send (same as send_files)\n- `subdir` — optional string (default 'downloads') — subfolder under HD's project dir to write into; sits alongside the project's screenshots/ folder\n\n**Returns:** send_files-style result: {success:bool, filesReceived:int, destinationPaths:[ '<project>/<subdir>/<name>', ... ], error}. destinationPaths are the WSL2 paths HD actually wrote, so the AI can find them straight away. HD down or endpoint missing → success:false + a clear error.\n\n**Note:** Use this instead of send_files whenever the destination is HD's WSL2 project (it's the cure for the /mnt/c cache-lag 'file isn't there' confusion).\n\n\n#### `hd_ship`\n\nv1.8.49+: atomic 'rebuild from the exact SHA I just pushed + relaunch + verify it's actually running' sequence. v1.8.57+ adds three guardrails BEFORE doing destructive work: (a) expectSha pre-validation — `git fetch origin main` then `git cat-file -t <sha>` must be `commit` and `git merge-base --is-ancestor <sha> origin/main` must succeed; (b) global build lock at `%TEMP%\\hd-build.lock` — refuses immediately if another hd_build_*/hd_ship is in flight (no queue); (c) single-flight lifecycle mutex — refuses if hd_launch/hd_stop/hd_restart/hd_ship is already running. Pipeline after guards pass: (1) taskkill HD + poll until zero (frees exe lock); (2) git fetch + reset --hard origin/main + assert HEAD == expectSha; (3) optional cargo clean; (4) cargo build; (5) launch + poll http://127.0.0.1:47084/buildinfo until SHA matches.\n\n**Args:**\n- `clean` — optional bool, default false. true forces `cargo clean -p hydrogen-desktop` before build to defeat the stale-relink trap — use when build said 'Finished' but the running SHA doesn't match expectSha on a prior hd_ship attempt.\n- `expectSha` — REQUIRED string — full or prefix SHA of the commit you want running. Matching is bidirectional-prefix (so 'abc123' matches the full SHA starting with abc123 and vice versa). v1.8.57+ pre-validates against origin/main; hallucinated/typo'd/not-yet-pushed SHAs are refused at zero cost before any destructive work.\n\n**Returns:** On success: {ok:true, builtSha (== expectSha), runningSha (== expectSha per /buildinfo), compiled:bool, elapsedSecs, _hint}. On pipeline failure: {ok:false, failedAt: 'args'|'kill'|'git'|'clean'|'build'|'launch'|'verify', error, builtSha|null, runningSha|null, compiled|null, elapsedSecs, _hint}. On v1.8.57+ guard refusal: {ok:false, refused:true, failedAt: 'sha_validate'|'git_fetch'|'lock'|'lifecycle_lock', error, narrative, _youProbablyMeant|_next}. Hallucinated SHA → 'sha_validate' + _youProbablyMeant tells you what real SHAs look like. Lock held → 'lock' + holderPid/holderVerb/elapsedSec tells you who's holding it.\n\n**Note:** Long-running: cold cargo build can take 2-3 min. CLI WS timeout is 12 min — call as a single sync invocation and wait. v1.8.57+: arg validation, SHA validation, and lock acquisition all happen BEFORE any destructive action — if you get `refused:true` at the top, the running HD was not touched. `compiled:false` on success means cargo found nothing to recompile (either cached or HEAD didn't actually change). The verify step requires HD to expose http://127.0.0.1:47084/buildinfo returning JSON with `build_sha` (or `buildSha` or `sha`).\n\n\n#### `hd_shot`\n\nv1.8.54+: Capture a named region of Hydrogen Desktop, produce BOTH a full-res PNG and a Claude-safe-resized copy, and return rich metadata so the AI never has to chain screenshot→pull→convert→crop→resize→Read manually. Regions are deterministic (no AI-supplied pixel coords) — they resolve via HD window bounds + CDP element rects queried through HD's /eval endpoint. Each shot is self-identifying: it includes the running build's SHA + local build time read from HD's /buildinfo so you can confirm you're looking at the build you expect before trusting what you see. v1.8.61+ universal screenshot delivery contract: the CLI auto-pulls BOTH PNGs to /tmp/ad-shots/<basename>, verifies each landed + is non-empty, and enriches the response with `localFullPath` + `localSafePath` + `localFullExists` + `localSafeExists` + `fullBytes` + `safeBytes` + `source:'hd_shot'` + `narrative` telling you which path to Read for analysis (the safe one).\n\n**Args:**\n- `format` — optional string, default 'png' (only PNG supported in v1).\n- `region` — REQUIRED string. One of: 'full' (whole HD window), 'titlebar' (top ~40px strip — build SHA / timestamp readout), 'vscode' (VS Code iframe pane via CDP element rect), 'setup-panel' (Setup Steps panel via .setup-panel or .setup-panel-output), 'wiki' (wiki/browser tab pane), 'claude' (Claude Code panel). If unknown, response is {ok:false, error, knownRegions:[...]}.\n- `resizeMax` — optional int, default 1400. Longest side of the safe-resized copy. Use lower (≈800) if you want to fit more shots in one Claude turn; higher (1568) for max fidelity.\n\n**Returns:** {ok, region, fullPath, safePath, fullSize:[w,h], safeSize:[w,h], clipRect:[x,y,w,h], capturedAtLocal, runningBuildSha, runningBuildLocal, hdWindowFound:bool, regionFound:bool, windowOuter:[w,h], capturedSize:[w,h], _hints:{pull, fullForArchive, staleWindow, regionMiss}}\n\n**Note:** Always pull_file the SAFE path and Read THAT — it's already sized for Claude's image analysis. The FULL path is meant for shotlog / disk archival, NOT direct Read (may exceed Claude's per-image budget). **regionFound:false** when the element rect wasn't in HD's DOM — the panel/iframe may not be mounted yet, OR HD's markup changed and region_selectors() in hd_bridge.rs is out of date. Either way, the shot is still produced (falls back to the full window) so you have something to look at. **runningBuildSha** can be null if HD's /buildinfo isn't exposed yet — same SHA HD's launch path uses, so you can compare to hd_launch's builtSha. All child spawns use CREATE_NO_WINDOW (no cmd flashes). v1.8.54+.\n\n\n#### `hd_status`\n\nv1.8.58+ COMPOSED + RELAYED snapshot of HD's state; v1.8.129+ churn-honest. AD owns host/process/repo facts; HD owns runtime state (relayed verbatim from HD's control API, port discovered from %APPDATA%/hydrogen-desktop/ports.json key 'control', fallback 47084 — NOT a hardcoded port). v1.8.129 reports THREE orthogonal signals instead of one boolean so you can reason about HD's constant build-relaunch churn: processPresent (hydrogen-desktop.exe exists, sampled with retry to ride the ~1s kill→respawn gap), controlApiReachable (control port actually listening), and embedded (AD's OWN mode — answers 'is AD embedded under HD?'). `running` = processPresent||controlApiReachable.\n\n**Returns:** {ok:true, running:bool (process||control), processPresent:bool, controlApiReachable:bool, controlPort:int (the dynamically-discovered port), embedded:bool (AD's own mode), embeddedOwner:string|null, embeddedVia:string|null ('LaunchFlag'|'RuntimeAdopt'), laptopHeadSha:string, originMainSha:string, shaMatchesOrigin:bool, runningSha:string|null, workspaceHealth:object|null (HD's /workspace/health relayed verbatim), buildinfo:object|null (HD's /buildinfo relayed verbatim), narrative:string (honest about up-serving vs up-churning vs down, plus AD's embedded mode), _next:string}\n\n**Note:** v1.8.129 fixes the misleading 'HD is NOT running' that fired when a single tasklist snapshot landed mid-relaunch. If you see running:true + controlApiReachable:false, HD is mid-build/relaunch — retry in a few seconds, don't treat it as down. Use this as the entry point for 'what's HD's state?' — it tells you which downstream verb to run next (hd_ship, hd_launch, hd_shot, etc.).\n\n\n#### `hd_stop`\n\nKill hydrogen-desktop.exe. `wasRunning` distinguishes 'killed it' from 'nothing to do'. v1.8.16+.\n\n**Returns:** {ok, wasRunning:bool, killedPid?, _hint}\n"+ "files": [+ "SKILL.md",+ "README.md",+ "package.json",+ "install.sh",+ "uninstall.sh",+ "skills/**",+ "screenshots/**",+ "bin/**"+ ],+ "metadata": {+ "platform": "windows-gui + linux-cli companion"+ },+ "dependencies": {},+ "org": "adom",+ "author": {+ "name": "Kyle Bergstedt",+ "email": "[email protected]"+ } }