← Commit history

Add adompkg installer files

Kyle Bergstedt ·0ea3ae34fe ·5mo ago ·parent 294d425
5 files changed +74−1275
README.md−3
@@ -196,6 +196,3 @@ The three official bridges live in their own repos: - [`adom-inc/adom-bridge-puppeteer`](https://github.com/adom-inc/adom-bridge-puppeteer)  (Private as of this version, going public after a review pass. Source is mirrored on each bridge's wiki page as a tarball + git bundle — no GitHub account required to fork.)---
SKILL.md+44−1272
@@ -7,8 +7,6 @@ description: Use when the user wants to send files to their desktop, control KiC  Bridge between Claude Code (running in an Adom Docker container) and the user's desktop applications via WebSocket. -**Install surface:** the canonical install page is `apps/adom-desktop` on the Adom wiki — it hosts the Linux CLI (`docker_binary`), the Windows installer (`.exe`), and the four sibling skills (pup, kicad-bridge, fusion-bridge, + this one). The gallia `adom-desktop-discovery` skill surfaces this page on any related user query. `adom-desktop setup_desktop` also returns a dynamic installer URL pulled from the latest GitHub release.- **First-time setup?** If the user hasn't installed the desktop app yet, see the Setup section below to walk them through downloading, installing, and connecting the app.  Quick check if desktop is connected:@@ -16,11 +14,6 @@ Quick check if desktop is connected: adom-desktop ping ``` -**Companion skills** (installed alongside this one from the wiki):-- `adom-desktop-kicad` — KiCad bridge: launch editors, open designs, install libraries, run DRC, window capture + keyboard/click automation (`plugins/kicad/SKILL.md`)-- `adom-desktop-fusion` — Fusion 360 bridge: launch, open designs, STEP/GLB/.lbr import-export, BOM + API queries, Fusion screenshots (`plugins/fusion360/SKILL.md`)-- `pup` — browser automation (Puppeteer-style): open URLs, screenshot, eval JS, multi-session Chrome (`gallia/skills/pup/SKILL.md`)- ## How It Works  ```@@ -74,9 +67,8 @@ If not running: `adom-desktop serve &`  Ask the user: **"What operating system is your desktop/laptop? (Windows, Mac, or Linux)"** -- **Windows 10/11** -- proceed to Step 4 (Windows installer)-- **macOS** -- proceed. Download the `.dmg` from the [wiki](https://wiki-ufypy5dpx93o.adom.cloud/apps/adom-desktop) or [GitHub Releases](https://github.com/adom-inc/adom-desktop/releases/latest). Mount, drag to Applications, then run `xattr -cr "/Applications/Adom Desktop.app"` (unsigned build). Launch and connect to the relay.-- **Linux (Ubuntu/Debian)** -- proceed. Download from the [wiki](https://wiki-ufypy5dpx93o.adom.cloud/apps/adom-desktop): either the `.deb` package (`sudo dpkg -i Adom-Desktop_1.7.1_amd64.deb`) or the standalone binary (`chmod +x adom-desktop-ubuntu-1.7.1 && ./adom-desktop-ubuntu-1.7.1`). Optional runtime deps: `sudo apt install wmctrl xdotool imagemagick scrot libnotify-bin`.+- **Windows 10/11** -- proceed+- **macOS / Linux** -- "Support is coming soon. The desktop app currently only runs on Windows."  ### Step 4: Install & connect @@ -129,7 +121,15 @@ Tell the user what you see. If `ping` succeeds:  > "Your desktop is connected! I can now send files to your machine, open browser windows for visual debugging, control KiCad and Fusion 360, take screenshots of your desktop, and send you notifications." -### Step 7: Optional -- Node.js for browser features+### Step 7: Offer the demo++After a successful first-time setup, offer the interactive demo:++> "Want to see a quick demo of everything I can do? It takes about 2 minutes and shows off notifications, screenshots, browser automation, and more — all live in a webview right here."++If yes → trigger the `adom-desktop-demo` skill.++### Step 8: Optional -- Node.js for browser features  Ask: **"Would you like to use the visual browser debugging feature? This opens Chrome windows on your desktop that I can control."** @@ -173,12 +173,6 @@ adom-desktop pull_file '{"filePaths":["C:\\Users\\john\\Downloads\\image.png"],"  ## Available Commands -**Get full structured command list with descriptions, args, and prerequisites:**-```bash-adom-desktop list_commands-```-Returns categorized JSON with every command, its required/optional args, return values, prerequisites, and workflow notes. Use this when you need to discover available commands or understand what a command expects.- ### Connection Management  - `ping` -- 5s round-trip test. Use BEFORE browser/shell commands to verify the desktop connection is alive.@@ -199,9 +193,6 @@ Returns categorized JSON with every command, its required/optional args, return - `pull_file` -- Pull files from the desktop to the container.   - `filePaths`: array of absolute Windows paths on the desktop   - `saveTo`: directory on the container to save files (default: `/tmp`)-  - **Streaming since v1.4.3.** Each file is transferred as 1 MiB binary WS frames straight to disk on the Docker side, with incremental SHA256 verification. The legacy 30s base64-JSON path is gone — large files (50 MB+ datasheets, 75 MB reference manuals) no longer time out. Per-file timeout is 600s.-  - Returns `files: [{name, path, size, sha256, chunks}]`. Use `sha256` to verify the transfer (the desktop side computes it during streaming and the container side verifies on completion; mismatch deletes the partial file and reports failure). `chunks` is the count of 1 MiB binary frames received.-  - When at least one but not all files succeed: `success` is `true`, `errors[]` lists the failures alongside `files[]`. When ALL fail: `success` is `false`.  ### Desktop Notifications @@ -210,58 +201,15 @@ Returns categorized JSON with every command, its required/optional args, return  ### KiCad Tools -KiCad supports **multi-version side-by-side installs** — KiCad 9 and KiCad 10 (and older versions) can coexist on the same machine. Every `kicad_open_*` (and `install_*`, `run_drc`) command accepts an optional `kicadVersion` arg (e.g. `"9.0"` or `"10.0"`). Omit it to use the **newest installed** version. The success response includes `kicadVersionUsed` so you can confirm which install actually ran.--- `kicad_list_versions` -- List every installed KiCad version with their paths and which is the default (newest). Run this first if you're unsure what's available.-  - Example: `adom-desktop kicad_list_versions`-  - Returns: `{versions: [{version, base_dir, kicad_exe, default}], default: "10.0", count: 2}`-- `kicad_install_symbol` -- Send a .kicad_sym file and install it as a library. Args include optional `kicadVersion`.-- `kicad_install_library` -- Install a symbol, footprint, or 3D model library. Each KiCad version has its own sym-lib-table / fp-lib-table — pass `kicadVersion` to target a specific install (otherwise installs into the newest version's tables).-- `kicad_open_board` -- Open a .kicad_pcb file. Args: `filePath`, optional `kicadVersion`. Example: `adom-desktop kicad_open_board '{"filePath":"C:/foo.kicad_pcb","kicadVersion":"9.0"}'`-- `kicad_open_schematic` -- Open a .kicad_sch file. Args: `filePath`, optional `kicadVersion`.-- `kicad_open_symbol_editor` -- Open the Symbol Editor. Optional `kicadVersion`.-- `kicad_open_footprint_editor` -- Open the Footprint Editor. Optional `kicadVersion`.-- `kicad_open_3d_viewer` -- Open the 3D Viewer (works from Footprint Editor or PCB Editor). Optional `kicadVersion`.-- `kicad_run_drc` -- Run Design Rule Check on a board. Optional `kicadVersion`.-- `kicad_close` -- Close all KiCad windows. Args: `force` (optional bool — skip graceful close, just taskkill)--**When to specify `kicadVersion`:**-- The user asks to open something "in KiCad 9" / "in KiCad 10" / "in the older version" — pass that explicitly.-- The user is regression-testing across versions (open in 9, screenshot, close, open in 10, screenshot, compare).-- A library was installed under a specific version's tables and the user wants to use it.--If you pass an invalid `kicadVersion`, the bridge returns `available_versions` and a `_hint` listing what IS installed — surface that to the user.--#### KiCad UI Interaction--Commands for detecting KiCad windows/dialogs, taking screenshots, clicking, and sending keyboard input. Essential for handling blocking dialogs and automating multi-window KiCad workflows.--- `kicad_window_info` -- Returns all KiCad windows grouped as `projectManager`, `editors[]`, `modalDialogs[]` with HWNDs and titles. Uses **process-based enumeration** (finds all PIDs running from KiCad's bin dir, then enumerates their windows) — catches every KiCad window regardless of title. Check `hasModalDialogs` to detect blocking save/error prompts.-  - Example: `adom-desktop kicad_window_info`-  - Returns: `{projectManager: {hwnd, title}, editors: [{hwnd, title}], modalDialogs: [{hwnd, title, ownerHwnd}], hasModalDialogs: bool}`--- `kicad_screenshot_all` -- Screenshots every KiCad window in one call (editors first, then project manager, then dialogs). Uses same process-based enumeration as `kicad_window_info`. Images downscaled to ≤1568px — use relative coords (0.0-1.0) for clicking, they're scale-independent. READ each screenshot to see what's on screen.-  - Example: `adom-desktop kicad_screenshot_all`-  - Returns: `{screenshots: [{type: "main"|"editor"|"dialog", title, hwnd, savedTo, sizeKB}]}`--- `kicad_send_key` -- Send a keystroke to a KiCad window. **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.-  - Args: `key` (required: "enter", "escape", "tab", "space", "f1"-"f12", or single char), `hwnd` (optional — if omitted, sends to foreground KiCad window)-  - Example: `adom-desktop kicad_send_key '{"key": "enter"}'` (dismiss dialog)-  - Example: `adom-desktop kicad_send_key '{"key": "escape", "hwnd": 12345}'` (target specific window)--- `kicad_click` -- Click at coordinates within a KiCad window. Relative coords (0.0-1.0) are scale-independent — estimate position as percentage from screenshot. To click a button at pixel (px,py) in an image of size (W,H), use `x=px/W, y=py/H`.-  - Args: `hwnd` (required), `x` (required), `y` (required), `relative` (optional bool, default true — 0.0-1.0 range)-  - Example: `adom-desktop kicad_click '{"hwnd": 12345, "x": 0.5, "y": 0.8}'`--#### KiCad Dialog Detection Workflow--KiCad has multiple independent windows (project manager, eeschema, pcbnew, footprint editor, symbol editor, 3D viewer). Unlike Fusion which has one main window, KiCad dialogs are owned by specific parent windows.--**After any open/install command**, check for blocking dialogs:-1. `kicad_window_info` → check `hasModalDialogs`-2. If true: `kicad_screenshot_all` → READ each dialog screenshot-3. Dismiss with `kicad_send_key {"key":"enter"}` (OK) or `{"key":"escape"}` (cancel)-4. For specific button clicks: identify position in screenshot → `kicad_click {"hwnd":<dialog_hwnd>, "x":0.5, "y":0.8}`+- `kicad_install_symbol` -- Send a .kicad_sym file and install it as a library+- `kicad_install_library` -- Install a symbol, footprint, or 3D model library+- `kicad_open_board` -- Open a .kicad_pcb file+- `kicad_open_schematic` -- Open a .kicad_sch file+- `kicad_open_symbol_editor` -- Open the Symbol Editor+- `kicad_open_footprint_editor` -- Open the Footprint Editor+- `kicad_open_3d_viewer` -- Open the 3D Viewer+- `kicad_run_drc` -- Run Design Rule Check on a board+- `kicad_close` -- Close all KiCad windows  ### Fusion 360 Tools @@ -269,644 +217,15 @@ Fusion 360 uses a two-tier architecture: - **External bridge** (port 8773, auto-started): handles launch, detection, file opening - **AdomBridge add-in** (port 8774, runs inside Fusion): required for export, design queries, Electronics/EAGLE commands -- `fusion_start` -- **PREFERRED.** First-class Fusion 360 startup. Idempotent — safe to call whether Fusion is running or not. Discovers `Fusion360.exe` via webdeploy glob (no hardcoded hashes, no "Windows cannot find" GUI dialogs), verifies path exists before spawn, polls the AdomBridge add-in until it's online, and calls `fusion_dismiss_blocking_dialogs` to clear the startup picker and any other blocking modals. **Auto-dismisses startup dialogs during add-in wait** — the "What do you want to design?" picker and "Recovered Documents" dialog are dismissed every 5 seconds while polling, so the add-in becomes responsive within ~10-15s instead of timing out at 60s. Returns `{addinReady, mainThreadResponsive, dialogsDismissed[], dialogsRemaining[], dismissHint, pid, resolvedPath, elapsedMs, alreadyRunning}`. **Has a hard process-detection gate** — will never spawn a second Fusion process, so the "Multiple instances are not supported" GUI dialog cannot reach an end user. Use this as the very first call before any other Fusion command.-- `fusion_dismiss_blocking_dialogs` -- **First-class blocking-dialog killer.** Whenever any fusion_* command returns `errorCode: fusion_addin_not_responding` (or you see `_hint` mentioning a blocked add-in), call this FIRST before retrying. Two-layer design: (1) deterministic pattern match against known blockers — "What do you want to design?" picker, error toasts ("New design cannot be created..."), "Multiple instances" warning, crash recovery prompts, save prompts — dismisses each with `fusion_send_key escape` targeted at the exact hwnd. (2) AI-in-the-loop fallback — for anything the patterns didn't match, auto-screenshots every remaining Fusion dialog and returns the image paths inline in `remaining[].screenshotPath`, plus an explicit per-hwnd recovery script in `_hint` that you follow mechanically: Read the screenshot with the Read tool, understand what the dialog says, then `fusion_send_key '{"key":"escape","hwnd":<hwnd>}'` (or `"enter"` for OK-only dialogs). Re-call `fusion_dismiss_blocking_dialogs` to verify `mainThreadResponsive=true`, then retry your original command. **Fusion updates break pattern matches constantly, so the AI fallback is the critical path, not the deterministic side.** If you identify a new recurring pattern, add its title substring to `BLOCKING_DIALOG_PATTERNS` in `cli/src/commands.rs` so future sessions handle it automatically.--- `fusion_addin_status` -- **Non-blocking** check of the AdomBridge add-in's busy state. Does NOT touch the Fusion main thread — queries the add-in's `/status` HTTP endpoint directly (~5ms). Returns `{busy, busyCommand, elapsedSeconds, requestId, walkProgress}`. Use this to check if a long-running command (walk, search) is in progress **from any bridge/session** before sending new commands. The CLI's auto-recovery path calls this before attempting dialog dismissal — if the add-in is just busy (not dialog-blocked), it returns `main_thread_busy` immediately instead of mashing Escape into a working Fusion.-  - Example: `adom-desktop fusion_addin_status '{}'`-  - When idle: `{"busy": false}`-  - During a walk: `{"busy": true, "busyCommand": "walk_cloud_tree", "elapsedSeconds": 42.3, "walkProgress": {"foldersVisited": 15, "filesFound": 87, "currentFolder": "Molecules/XRP", "queueSize": 8}}`--### Launching apps (generic)--Two generic CLI commands exist for launching any Windows executable safely — they verify the target exists BEFORE handing it to the OS, so you never trigger a "Windows cannot find" GUI dialog:--- `adom-desktop find_exe '{"name":"..."}'` -- Resolve an exe by absolute path, glob (e.g. `C:/.../webdeploy/production/*/Fusion360.exe` returns newest), bare name (searches PATH + Start Menu `.lnk` targets). Returns `{path, source}`. Does NOT launch.-- `adom-desktop launch '{"path":"...", "args":[...], "cwd":"...", "detached":true}'` -- Same resolution rules as `find_exe`, then spawns. Fails in terminal (exit 1) with a clear error if the path doesn't exist. **Always prefer this over raw `start`.**--For Fusion specifically, use `fusion_start` — it wraps `launch` plus the full startup-picker / add-in-readiness dance.+- `fusion_launch` -- Launch Fusion 360 and wait for it + AdomBridge add-in to be ready (~15-30s). Call this before any other Fusion command if `status` shows `fusion360.running: false`. Returns `addinConnected: true` when ready. - `fusion_import_step` -- Import a STEP/STL/IGES file into Fusion 360 - `fusion_open_lbr` -- Open an EAGLE .lbr library file - `fusion_open_electronics` -- Check if the Electronics workspace is active-- `fusion_electron_run` -- Execute any EAGLE command via Electron.run. Returns rich state: `activeWorkspace`, `activeDocument`, `commandType`, `_hint`, and `fusionOperations` (diff of Fusion's internal operation log showing what actually fired). **Avoid blocking commands** — see list below. **Full EAGLE command reference:** [`skills/eagle-commands.md`](./eagle-commands.md) — popular/safe commands split from blocking/modal ones, with layer reference and chaining syntax.-- `fusion_execute_text_command` -- Low-level app.executeTextCommand() access. Returns the command result plus workspace context.-- `fusion_board_info` -- Get structured board data from the open PCB layout. Returns: component placements (name, package, x, y, rotation), net names, copper traces, layer setup, board thickness, DRC violations. Much richer than a screenshot — gives exact coordinates and connectivity. Requires a .brd board open in PCB Editor.--#### `fusion_electron_run` — EAGLE Command Execution--Executes EAGLE commands inside Fusion 360's Electronics workspace. Works in **Schematic Editor**, **PCB Editor (Board Layout)**, and **Electronics Library** contexts.--**How it works:** Sends the command string via Fusion's `Electron.run` text command. EAGLE's `Electron.run` is fire-and-forget — it never returns output or throws on invalid commands. To compensate, the handler snapshots Fusion's internal operation log (`Diagnostics.RecentOperations`) before and after execution, returning a diff showing what actually fired.--**Usage:**-```bash-# Basic command-adom-desktop fusion_electron_run '{"command": "WINDOW FIT"}'--# Multiple commands in sequence (use semicolons)-adom-desktop fusion_electron_run '{"command": "DISPLAY NONE; DISPLAY 1 16 17 18 20 21"}'--# Navigate in library editor-adom-desktop fusion_electron_run '{"command": "EDIT SOIC8.pac"}'-adom-desktop fusion_electron_run '{"command": "EDIT RESISTOR.sym"}'-adom-desktop fusion_electron_run '{"command": "EDIT MYDEVICE.dev"}'-```--**Response fields:**-- `activeWorkspace` — Current workspace (Schematic Editor, PCB Editor, Electronics Library)-- `activeDocument` — Name of the open document-- `commandType` — Detected type: `view_control`, `layer_control`, `edit`, `design_rule`, etc.-- `editorType` — For EDIT commands: `package`, `symbol`, `device`-- `fusionOperations` — Array of Fusion operations that fired (diff of internal log)-- `hint` — Human-readable description of what happened-- `rawResult` — Raw return from Electron.run (usually empty)--**EAGLE Command Reference — Safe for automation:**--| Command | Context | Description |-|---------|---------|-------------|-| **View / Navigation** | | |-| `WINDOW FIT` | Any | Zoom to fit all content |-| `WINDOW (x1 y1 x2 y2)` | Any | Zoom to specific area (coordinates in current units) |-| `DISPLAY ALL` | Any | Show all layers |-| `DISPLAY NONE` | Any | Hide all layers |-| `DISPLAY 1 16 17 18 20 21` | Board | Show specific layers by number |-| **Grid** | | |-| `GRID MM 0.1` | Any | Set grid to 0.1mm |-| `GRID MIL 25` | Any | Set grid to 25mil |-| `GRID INCH 0.05` | Any | Set grid to 0.05 inch |-| **Board Layout** | | |-| `RATSNEST` | Board | Recalculate airwires (unrouted connections) |-| `RIPUP` | Board | Remove all routed traces |-| `RIPUP *` | Board | Remove all traces (same as RIPUP with no selection) |-| `ROUTE` | Board | Start auto-router |-| `DRC` | Board | Run design rule check |-| `BOARD` | Schematic | Switch to paired board layout |-| `SCHEMATIC` | Board | Switch to paired schematic |-| **Schematic** | | |-| `VALUE value` | Schematic | Set component value |-| `NAME name` | Any | Rename selected element |-| `SMASH` | Any | Detach name/value labels from components |-| **Library Editor** | | |-| `EDIT name.pac` | Library | Open a package (footprint) for editing |-| `EDIT name.sym` | Library | Open a symbol for editing |-| `EDIT name.dev` | Library | Open a deviceset for editing |-| `EXPORT SCRIPT 'path.scr'` | Library | Export entire library as EAGLE script |-| **Scripting / Settings** | | |-| `SET CONFIRM YES` | Any | Suppress confirmation dialogs |-| `SET CONFIRM OFF` | Any | Re-enable confirmation dialogs |-| `SCRIPT 'path.scr'` | Any | Run batch commands from a .scr script file |--**EAGLE Layer Numbers (commonly used):**--| Layer | Name | What it shows |-|-------|------|--------------|-| 1 | Top | Top copper |-| 16 | Bottom | Bottom copper |-| 17 | Pads | Through-hole pads |-| 18 | Vias | Via holes |-| 19 | Unrouted | Airwires (ratsnest) |-| 20 | Dimension | Board outline (required for 3D) |-| 21 | tPlace | Top silkscreen |-| 22 | bPlace | Bottom silkscreen |-| 25 | tNames | Top component names |-| 27 | tValues | Top component values |-| 29 | tStop | Top solder mask |-| 31 | tCream | Top stencil/paste |-| 51 | tDocu | Top documentation |--**Blocking commands — AVOID from automation:**--| Command | Why it blocks |-|---------|---------------|-| `WRITE` | Opens Save As dialog — use `fusion_save_lbr` or `fusion_close_document` instead |-| `ADD` | Opens component picker dialog |-| `SHOW name` | Opens interactive highlight mode |-| `SET` (no params) | Opens settings dialog |-| `GRID` (no params) | Opens grid settings dialog |-| `EDIT new.sym` | Opens "Create new?" confirmation if symbol doesn't exist |-| `CHANGE` | Opens interactive change mode |-| `MOVE` | Opens interactive move mode |--**Tips:**-- Always run `WINDOW FIT` after opening a file or switching views-- Use `DISPLAY NONE` then `DISPLAY <layers>` to show only specific layers-- Combine commands with `;` — e.g., `SET CONFIRM YES; RIPUP *; RATSNEST`-- For board screenshots: `DISPLAY NONE; DISPLAY 1 16 17 18 20 21; WINDOW FIT`-- Use `fusion_board_info` instead of EAGLE commands when you need structured data-- `fusion_export_lbr` -- Export the open Electronics library as an EAGLE .scr script (note: does NOT include 3D package references — those are cloud-linked only)+- `fusion_electron_run` -- Execute any EAGLE command via Electron.run+- `fusion_execute_text_command` -- Low-level app.executeTextCommand() access+- `fusion_export_lbr` -- Export the open Electronics library as an EAGLE .scr script - `fusion_save_lbr` -- Save the open Electronics library as a .flbr file--#### EAGLE Libraries with 3D Packages--Fusion 360's .lbr format supports `package3d` elements that link footprints to 3D models. Key facts:--- **3D models are cloud-hosted** — each `package3d` has a `wip_urn` (e.g., `urn:adsk.wipprod:fs.file:vf.xxxxx`) pointing to a Fusion cloud document. You cannot embed STEP files directly in .lbr XML.-- **Creating 3D packages requires the Fusion UI** — use `Package3DCreateCmd` in Electronics Library Editor, which opens a new Design workspace where you model/import the 3D shape, then save to link it.-- **`EXPORT SCRIPT` strips 3D references** — the .scr export only contains 2D data (symbols, footprints, devicesets). To preserve 3D links, keep the .lbr XML format.-- **Fusion's built-in examples** have 3D packages — 34 of 37 libraries in the EAGLE examples directory (e.g., `Connector_USB.lbr`, `Resistor.lbr`, `Capacitor.lbr`) include `package3d` references.-- **Example libraries location**: `%LOCALAPPDATA%/Autodesk/webdeploy/production/<hash>/Applications/Electron/LibEagle/examples/libraries/examples/`--To open a built-in example library for reference:-```bash-# Find the examples directory first-adom-desktop fusion_execute_text_command '{"command": "Python.RunScript C:/tmp/find_eagle_libs.py"}'-# Then open one-adom-desktop fusion_open_lbr '{"filePath": "<path>/Connector_USB.lbr"}'-```-- `fusion_open_schematic` -- Open a .sch schematic in Fusion's Schematic Editor. Args: `filePath`.-- `fusion_open_board` -- Open a .brd board layout in Fusion's Board Layout editor. Args: `filePath`.-- `fusion_show_3d_board` -- Switch to 3D PCB board view (must have a .brd open). Board MUST have an outline on layer 20 (Dimension) or 3D generation fails. Auto-zooms to fit after switching.-- `fusion_show_2d_board` -- Switch back to 2D board layout from 3D PCB view. EAGLE commands via `fusion_electron_run` only work in 2D.-- `fusion_close_document` -- Close a document without save dialog. Args: `name` (optional, defaults to active doc), `save` (optional, default false). Essential for automation — avoids modal save dialog that blocks Fusion.-- `fusion_document_info` -- List ALL open documents/tabs with name, type, and active status, plus detailed cloud info for the active document. Returns `openDocuments` array (every tab) and active doc details (cloud project, folder, file ID, version, save status). Lightweight — uses only in-memory data, no cloud API calls. Use this instead of `fusion_walk_cloud_tree` when you just need to know what's open.-- `fusion_activate_document` -- Switch to a specific open document tab. Args: `name` (substring match, case-insensitive), `documentType` ("Electronics", "PCB", "FusionDesign", "Drawing"). Essential for automation — switch between schematic, board, and library tabs without user interaction. If no match found, returns the list of open documents so you can refine. - `fusion_close` -- Close Fusion 360. **Always call this when done with Fusion 360 commands to clean up.**-- `fusion_dismiss_recovery` -- Dismiss recovery document dialogs (both "Recovered Documents" list and "Open recovery document instead?" prompts). Also relocates recovery files to `~/.adom/recovery/fusion/` for safekeeping.-- `fusion_relocate_recovery` -- Proactively move Fusion crash recovery files to `~/.adom/recovery/fusion/<timestamp>/` without dismissing any dialogs. Call this **before** launching Fusion to prevent recovery dialogs from appearing. Files are preserved (not deleted) so the user can manually restore them if needed.-- `fusion_close_all_documents` -- Close all open documents. Args: `saveChanges` (default: false). Use before force-killing Fusion to prevent recovery files.--#### Fusion 360 UI Interaction--Commands for interacting with Fusion 360's UI — detecting dialogs, taking screenshots, clicking, and sending keyboard input. Essential for handling blocking dialogs and automating CEF-based UI elements.--- `fusion_window_info` -- Returns the Fusion main window HWND, title, rect, and a list of all Qt dialog windows (recovery dialogs, wizards, file pickers). Essential for detecting blocking dialogs before/after operations.-  - Example: `adom-desktop fusion_window_info`-  - Returns: `{hwnd, title, rect, dialogs: [{hwnd, title, className, rect}]}`--- `fusion_screenshot_fusion` -- Captures the Fusion main window or a specific dialog by HWND. Uses PrintWindow (works without bringing to foreground). Saves WebP to `C:/tmp/conduit-screenshots/` (falls back to PNG if WebP unavailable). Downscaled to ≤1568px — use relative coords (0.0-1.0) for clicking, they're scale-independent. DPI-aware.-  - Args: `hwnd` (optional — dialog HWND to screenshot instead of main window)-  - Example: `adom-desktop fusion_screenshot_fusion` (main window)-  - Example: `adom-desktop fusion_screenshot_fusion '{"hwnd": 12345}'` (specific dialog)--- `fusion_screenshot_all` -- Screenshots the main Fusion window and lists all dialog windows with their HWNDs. Use `fusion_screenshot_fusion {"hwnd": ...}` to capture each dialog.-  - Example: `adom-desktop fusion_screenshot_all`--- `fusion_click_fusion` -- Click at coordinates within the Fusion window or a specific dialog. x/y are relative (0.0-1.0) by default. Set `"relative": false` for pixel offsets. Uses SendInput for CEF dialog compatibility. **Prefer `fusion_send_key` for dialogs** — enter/escape/tab covers most cases.-  - Args: `x` (required), `y` (required), `relative` (optional, default true), `hwnd` (optional — target a specific dialog HWND instead of main window)-  - Example: `adom-desktop fusion_click_fusion '{"x": 0.5, "y": 0.7}'` (main window)-  - Example: `adom-desktop fusion_click_fusion '{"hwnd": 12345, "x": 0.75, "y": 0.85}'` (dialog button)--- `fusion_send_key` -- Send keyboard input to Fusion or a specific dialog via SendInput. **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.-  - Args: `key` (required), `hwnd` (optional — target a specific dialog HWND)-  - Example: `adom-desktop fusion_send_key '{"key": "escape"}'` (dismiss CEF dialog)-  - Example: `adom-desktop fusion_send_key '{"key": "enter", "hwnd": 12345}'` (confirm dialog)--#### Fusion 360 Workflow Guide--##### Opening Electronics Projects-1. Open the `.fprj` file: `adom-desktop fusion_open_cloud_file '{"projectName":"Main","fileName":"DRV8411A","fileExtension":"fprj","folderPath":"Molecules/XRP/DRV8411A"}'`-2. Do NOT try to open `.fbrd` or `.fsch` directly — they fail or show a "Select Electronics Design File" dialog-3. Enter the board editor: `adom-desktop fusion_show_2d_board`-4. Enter the schematic editor: After step 3, use `adom-desktop fusion_electron_run '{"command":"EDIT .sch"}'`-5. Switch back to board: `adom-desktop fusion_electron_run '{"command":"EDIT .brd"}'`--##### Auto-Screenshot on Open Commands--**Open commands automatically screenshot Fusion and return the images in the response.** The following commands include `postOpenScreenshot` in their `data` field:-- `fusion_open_cloud_file`, `fusion_open_schematic`, `fusion_open_board`, `fusion_show_3d_board`, `fusion_show_2d_board`--The response `data.postOpenScreenshot` contains:-- `screenshots[]` — array of `{type, savedTo, sizeKB, title?, hwnd?}`. Type is `"main_window"` or `"dialog"`.-- `message` — human-readable instruction to READ each screenshot and check for blocking dialogs-- `dialogBlocking` — `true` if a modal dialog was detected--**After receiving the response, you MUST:**-1. **READ each screenshot file** using the Read tool to visually inspect what Fusion shows-2. Look for blocking dialogs in the screenshots:-   - "What to design?" wizard → dismiss with `fusion_send_key {"key": "escape"}`-   - "PCB out of date" banner → click Update or X-   - "Recovered Documents" → `fusion_dismiss_recovery`-   - "Save changes?" → `fusion_send_key {"key": "escape"}`-   - Any other modal → identify and dismiss-3. **Screenshot again** after dismissing to confirm it's clear--Screenshots are saved as WebP (lossless, ~20-40KB each, downscaled to ≤1568px) for token efficiency. Both the main Fusion window AND all Qt dialog windows are captured separately.--**Why this matters**: Fusion shows Qt dialogs and CEF overlays that are invisible to the API. The `success: true` response from an open command does NOT mean the UI is ready — a dialog may be blocking all further operations. The API cannot detect these. Only a screenshot can.--##### Detecting and Handling Blocking Dialogs-- The auto-screenshot captures dialog windows separately (look for `type: "dialog"` entries in `postOpenScreenshot.screenshots[]`)-- Common blocking dialogs: "What do you want to design?", "Recovered Documents", "Open recovery document instead?", "Select Electronics Design File"-- Dismiss recovery dialogs: `adom-desktop fusion_dismiss_recovery`-- Dismiss CEF dialogs (inside main window): `adom-desktop fusion_send_key '{"key": "escape"}'` or `fusion_click_fusion`-- Dismiss "What do you want to design?" wizard: `adom-desktop fusion_send_key '{"key": "escape"}'`-- For manual screenshot of specific dialogs: `adom-desktop desktop_screenshot_window '{"hwnd": <DIALOG_HWND>}'`--##### Preventing Recovery Documents-- Recovery files are at: `%LOCALAPPDATA%\Autodesk\Autodesk Fusion 360\<USER_ID>\CrashRecovery\`-- Before force-killing Fusion, close all documents: `adom-desktop fusion_close_all_documents '{"saveChanges": false}'`-- **Best practice**: Call `adom-desktop fusion_relocate_recovery` before launching Fusion. This moves recovery files to `~/.adom/recovery/fusion/<timestamp>/` — preserving them for the user while preventing modal dialogs.-- The `fusion_start` command also auto-relocates recovery files before starting Fusion.-- The `fusion_dismiss_recovery` command handles both "Recovered Documents" list and "Open recovery document instead?" prompts, and also relocates files.--##### EAGLE Export Limitations-- **Supported**: `EXPORT IMAGE`, `EXPORT NETLIST`, `EXPORT PARTLIST`-- **NOT supported** (fail silently): `EXPORT DXF`, `EXPORT SVG`, `EXPORT DRILL`-- Always verify export output: the updated `fusion_electron_run` now checks if the output file was created-- Use `fusion_export_bom` and `fusion_export_cpl` for manufacturing data (these use the add-in's XML parser, not EAGLE export)-- Use `fusion_export_gerbers` for Gerber files — produces a ZIP with all gerber layers (GTL, GBL, GTS, GBS, GTP, GBP, GTO, GBO, GKO, XLN). Auto-detects 2-layer vs 4-layer boards.--##### 3D Model Exports-- From the 3D view (activate the .f3d document): `fusion_export_step`, `fusion_export_stl`, `fusion_export_3mf`, `fusion_export_f3d`, `fusion_export_usdz`, `fusion_export_iges`, `fusion_export_sat`-- Switch to 3D view: `fusion_show_3d_board` or activate the .f3d document-- **STEP** — Industry-standard CAD interchange (SolidWorks, CATIA, Creo). Highest geometric fidelity.-- **IGES** — Legacy CAD interchange. Use STEP for modern workflows.-- **SAT** — ACIS solid model format. Used by SolidWorks, SpaceClaim.-- **STL** — Mesh format for 3D printing and visualization. Options: refinement "low"/"medium"/"high".-- **3MF** — Modern 3D printing with color/material support and multi-body.-- **F3D** — Native Fusion 360 archive. Preserves parametric features, sketches, timeline, component refs. Best for archival.-- **USDZ** — **Best for digital twins and GLB conversion.** Preserves full component hierarchy (Board, copper layers, soldermask, Packages), PBR materials, and named nodes. Each component becomes a toggleable node in GLB viewers. Also viewable directly on iOS/macOS (Apple Quick Look / AR).-  - Digital twin pipeline: `fusion_export_usdz` → pull_file → `blender --background --python-expr "import bpy; bpy.ops.wm.usd_import(filepath='board.usdz'); bpy.ops.export_scene.gltf(filepath='board.glb')"`-- **FBX** — Not available in current Fusion builds via the API. The command exists but fails clearly. Use `fusion_export_usdz` instead.-- **DXF/DWG/OBJ/SKP** — Not available via the Fusion API (dialog-only). Commands exist but return clear errors with alternatives.--##### 3D Viewport Captures-- `fusion_take_screenshot` — Capture the Fusion viewport at any resolution without opening a dialog. Uses Fusion's render API (`saveAsImageFile`).-  - Args: `outputPath` (required), `width` (default 1920), `height` (default 1080), `orientation` (optional)-  - Orientations: `home`, `front`, `back`, `top`, `bottom`, `left`, `right`-  - Example: Capture all 6 standard views for use as product images/icons:-    ```bash-    for orient in home front back top bottom left right; do-      adom-desktop fusion_take_screenshot "{\"outputPath\": \"C:/tmp/3d-${orient}.png\", \"width\": 1920, \"height\": 1080, \"orientation\": \"${orient}\"}"-    done-    ```--##### Electronics Source File Export (`fusion_export_source`)-- `fusion_export_source` — Export the active electronics document as `.fsch`, `.fbrd`, or `.flbr` source file.-  - Args: `outputPath` (required — full path with extension)-  - The extension determines the format: `.fsch` (schematic), `.fbrd` (board), `.flbr` (library)-  - Validates extension, creates output directory, verifies file was created, returns file size--- **Full workflow to export both board and schematic from a cloud project:**-  ```bash-  # Step 1: Open the .fprj (NOT .fbrd/.fsch directly — those fail)-  adom-desktop fusion_open_cloud_file '{"projectName":"Main", "fileName":"MyDesign", "fileExtension":"fprj", "folderPath":"Molecules/MyDesign"}'--  # Step 2: Screenshot to check for blocking dialogs (always do this after open)-  adom-desktop fusion_screenshot_fusion--  # Step 3: Enter board view (MUST be Board Layout workspace, not 3D)-  adom-desktop fusion_show_2d_board--  # Step 4: Export .fbrd (board first — it's already in board view)-  adom-desktop fusion_export_source '{"outputPath": "C:/tmp/exports/MyDesign.fbrd"}'--  # Step 5: Switch to schematic (EDIT .s1 = first schematic sheet)-  adom-desktop fusion_electron_run '{"command": "EDIT .s1"}'--  # Step 6: Export .fsch-  adom-desktop fusion_export_source '{"outputPath": "C:/tmp/exports/MyDesign.fsch"}'--  # Step 7: Pull files to Docker-  adom-desktop pull_file '{"filePaths":["C:/tmp/exports/MyDesign.fbrd","C:/tmp/exports/MyDesign.fsch"], "saveTo":"/tmp/exports"}'-  ```--- **Critical gotchas:**-  - You MUST open the `.fprj` first — opening `.fbrd`/`.fsch` directly fails or triggers blocking dialogs-  - You MUST call `fusion_show_2d_board` before exporting `.fbrd` (the Electronics Design overview won't work)-  - Always export `.fbrd` FIRST (from board view), then switch to schematic for `.fsch`-  - If export fails with "file not found", the wrong workspace is active — screenshot to verify-  - After `fusion_open_cloud_file`, always screenshot to check for "Select Electronics Design File" dialog-  - `WRITE` command is blocked — it opens a blocking save dialog. Use `fusion_export_source` instead--- Note: `WRITE` is blocked — it opens a "Version Description" dialog on cloud docs even with a path argument (tested 2026-04-10). Use `fusion_save_to_cloud` to save to cloud, `fusion_export_source` for Fusion-format, or `fusion_export_eagle_source` for plain EAGLE format.--##### Plain EAGLE Source Export (`fusion_export_eagle_source`)-- `fusion_export_eagle_source` — Export the active electronics document as plain EAGLE XML `.sch` or `.brd`.-  - Args: `outputPath` (required — full path ending in `.sch` or `.brd`)-  - Internally: exports `.fsch`/`.fbrd` via `Document.CopyToDesktop`, then extracts the EAGLE XML from the ZIP container, cleans up the temp file.-  - The output is valid EAGLE XML (`<?xml><eagle version="9.7.0">...`) parseable by standalone EAGLE, KiCad import, or any XML tool.-  - Same workflow/prerequisites as `fusion_export_source` — just use `.sch`/`.brd` extensions instead of `.fsch`/`.fbrd`.--  ```bash-  # Board (.brd) — must be in PCB Editor / Board Layout-  adom-desktop fusion_show_2d_board-  adom-desktop fusion_export_eagle_source '{"outputPath": "C:/tmp/exports/MyDesign.brd"}'--  # Schematic (.sch) — must be in Schematic Editor-  adom-desktop fusion_electron_run '{"command": "EDIT .s1"}'-  adom-desktop fusion_export_eagle_source '{"outputPath": "C:/tmp/exports/MyDesign.sch"}'-  ```--##### Dialog Dismissal (`fusion_close_window`)-- `fusion_close_window` — Close a specific Fusion dialog by sending WM_CLOSE (equivalent to clicking X).-  - Args: `hwnd` (required — from `fusion_window_info` or `fusion_dismiss_blocking_dialogs` remaining[])-  - Works on dialogs that Escape doesn't close — e.g. "Recovered Documents"-  - Does NOT force-kill — the dialog can still intercept WM_CLOSE--##### Electronics Import (Round-Trip)-- `fusion_import_electronics` — Import Fusion-native `.fsch`, `.fbrd`, or `.flbr` files as new local documents-  - Uses `Document.newDesignFromLocal` under the hood-  - Auto-screenshots after import (catches blocking dialogs)-  - Args: `filePath` (required Windows path to .fsch, .fbrd, or .flbr)-  - After import, use `fusion_save_to_cloud` to persist to Fusion cloud-  - **`.fsch` import works standalone** — creates a new schematic project-  - **`.fbrd` import may fail** — boards have external references to schematics/libraries. Error: "New design cannot be created from a local file containing external references"-  - **`.flbr` import works standalone** — creates a new library project-  - For legacy EAGLE files: use `fusion_open_schematic` (.sch), `fusion_open_board` (.brd), `fusion_open_lbr` (.lbr)--##### Library Round-Trip Workflow-Export and re-import Fusion electronics libraries:-```bash-# 1. Open library from cloud-adom-desktop fusion_open_cloud_file '{"projectName":"Main","fileName":"Adom Common Components","folderPath":"Molecules/Libraries"}'--# 2. Export as .flbr (Fusion-native binary) and .scr (EAGLE script text)-adom-desktop fusion_save_lbr '{"outputPath":"C:/tmp/library.flbr"}'-adom-desktop fusion_export_lbr '{"outputPath":"C:/tmp/library.scr"}'-adom-desktop fusion_close_document--# 3. Re-import the .flbr-adom-desktop fusion_import_electronics '{"filePath":"C:/tmp/library.flbr"}'--# 4. Verify symbols survived round-trip-adom-desktop fusion_export_lbr '{"outputPath":"C:/tmp/library-verify.scr"}'--# 5. Save to cloud with new name-adom-desktop fusion_save_to_cloud '{"name":"Library-copy"}'-```--##### Demo Projects-171+ electronics molecules are exported in the `adom-desktop-demo` repo (separate from adom-desktop).-Three reference boards with full export formats:--| Board | Cloud Path | Layers | Components | Files |-|-------|-----------|--------|------------|-------|-| DRV8411A | Main / Molecules / XRP / DRV8411A | 4 | 29 | 27 |-| DRV8323SR | Main / Molecules / Experiments / MotorControl / DRV8323SR | 2 | 43 | 27 |-| VL53L8BreakoutMolecule | Main / Molecules / XRP / TimeOfFlightVL53L8 / VL53L8BreakoutMolecule | 2 | 30 | 27 |--Each folder contains: .fsch, .fbrd, bom.csv, cpl.csv, gerbers.zip, 6 board images, 7 3D renders (home/front/back/top/bottom/left/right), STEP, IGES, SAT, STL, 3MF, F3D, USDZ, board screenshot, 3D board screenshot.--#### Cloud Document Management--Manage Fusion 360 cloud documents (hub projects, files, versions). Required for 3D package workflows since EAGLE library 3D models are stored as cloud documents.--- `fusion_save_to_cloud` -- Save the active Fusion document to the cloud. Args: `name` (required), `projectName` (optional, defaults to active project), `folderPath` (optional), `description` (optional). Returns: `fileId`, `versionNumber`, `wipUrn` (if cloud-hosted).-- `fusion_list_cloud_projects` -- List all cloud projects in the user's hub. Returns array of `{name, id}` per hub.-- `fusion_list_cloud_files` -- List files in a cloud project/folder. Args: `projectName` (optional), `folderPath` (optional). Returns: `files` array with `{name, id, versionNumber, fileExtension, dateModified}` and `subfolders` array.-- `fusion_create_cloud_folder` -- Create a folder in a cloud project. Args: `folderName` (required), `projectName` (optional), `parentPath` (optional). Returns `folderId`. Idempotent — returns existing folder if it already exists.-- `fusion_check_recovery` -- Check if a cloud file has a recovery document from a previous crash. Args: `fileName` (required), `projectName` (optional), `folderPath` (optional). Returns `hasRecovery: true/false`. Use this BEFORE opening files to avoid the blocking "Open recovery document instead?" dialog. If recovery exists, call `fusion_open_cloud_file` with `recovery: "open"` (restore unsaved work) or `recovery: "discard"` (delete recovery, open cloud version).-- `fusion_open_cloud_file` -- Open a cloud file in Fusion by name. **If a recovery document exists and no `recovery` arg is given, the command STOPS and reports the recovery instead of opening** — you must decide whether to preserve or discard unsaved work. Args: `fileName` (required), `projectName` (optional), `folderPath` (optional), `recovery` ("open" = restore unsaved work, "discard" = delete recovery and open cloud version).-- `fusion_export_cloud_file` -- Export the active Fusion document to a local file for transfer back to Docker. Args: `outputPath` (required), `format` (optional, default "step"). The exported file can then be pulled back to Docker via `pull_file`.-- `fusion_delete_cloud_file` -- Delete a cloud file by name. Args: `fileName` (required), `projectName` (optional), `folderPath` (optional). File must not be open in Fusion — close it first with `fusion_close_document`.-- `fusion_walk_cloud_tree` -- **Long-running.** BFS walk of a cloud folder tree. Returns a flat list of all files and folders. Runs entirely on the Fusion main thread — **blocks all other add-in commands** until done (check progress with `fusion_addin_status`).-  - Args: `projectName` (optional), `folderPath` (optional, starting folder), `maxDepth` (default 10), `maxFolders` (default 500), `extensions` (optional list, e.g. `["f3d","fprj"]`), `nameContains` (optional substring filter), `includeFiles` (default true)-  - Returns: `{project, rootFolder, folders[], files[], stats: {foldersVisited, foldersSkipped, filesFound, maxDepthReached, truncated}}`-  - **Per-folder timeout (30s):** If a single folder's cloud API calls take >30s (common for large projects), the folder is skipped and counted in `foldersSkipped`. This prevents indefinite hangs.-  - **Progress tracking:** While running, `fusion_addin_status` returns `walkProgress` with `{foldersVisited, filesFound, currentFolder, queueSize}` — poll this every 1–10s to monitor progress (see "Live folder progress streaming" below).-  - **Non-blocking alternative:** Use `fusion_search_cloud_files` for targeted searches, or `fusion_list_cloud_files` for single-folder listings (these are faster but don't recurse).-  - Example: `adom-desktop fusion_walk_cloud_tree '{"projectName":"Main","folderPath":"Molecules","nameContains":"DRV","extensions":["fprj"]}'`--#### Live folder progress streaming with `watch`--For long walks, use the built-in `watch` wrapper. It spawns the inner search command on a worker thread, polls `fusion_addin_status` internally, and emits one JSON event per line to stdout as the walker visits each folder. **No manual polling loop needed.**--```bash-adom-desktop watch '{"command":"fusion_walk_cloud_tree","args":{"projectName":"Main","folderPath":"Molecules","nameContains":"BQ25792"}}'-```--Output is one JSON object per line, three event types:--```jsonl-{"event":"started","command":"fusion_walk_cloud_tree","args":{...},"interval":2,"_hint":"streaming progress events follow, one per line, ending with 'complete' or 'error'"}-{"event":"progress","busyCommand":"walk_cloud_tree","elapsedSeconds":15.0,"foldersVisited":14,"queueSize":50,"filesFound":0,"currentFolder":"Molecules/Sensing"}-{"event":"progress","busyCommand":"walk_cloud_tree","elapsedSeconds":23.4,"foldersVisited":22,"queueSize":108,"filesFound":0,"currentFolder":"Molecules/Examples"}-{"event":"progress","busyCommand":"walk_cloud_tree","elapsedSeconds":31.8,"foldersVisited":30,"queueSize":100,"filesFound":0,"currentFolder":"Molecules/RAPID NAME TAGS/Connor Wood"}-... (one per real change in walkProgress, deduped) ...-{"event":"complete","result":{"folders":[...],"files":[...],"stats":{"foldersVisited":169,"filesFound":12,"truncated":false}}}-```--Read stdout line-by-line. Stop when you see `event:complete` or `event:error`. The full final result is in the `complete` event's `result` field.--**Optional `interval` arg** (default 2s, clamped to [1, 30]):--```bash-adom-desktop watch '{"command":"fusion_walk_cloud_tree","args":{...},"interval":1}'-```--**For Claude Code consumers**: pipe `watch` directly into the `Monitor` tool. Each JSON line becomes a real-time event notification in the chat — you (and the user) see folder names appear one-by-one as the walker visits them. No bash loop, no `disown`, no subprocess gymnastics. Verified live in 1.3.16 on a 169-folder walk that returned all 12 BQ25792 cloud files cleanly.--**Watchable commands** (whitelist): `fusion_walk_cloud_tree`, `fusion_search_cloud_files`. Other commands return `success:false` with a `_hint` listing the watchable set.--**When NOT to use watch**: short single-folder operations (`fusion_list_cloud_files`, `fusion_open_cloud_file`) — those return in <2s and don't need streaming. The `watch` wrapper is purely for the long BFS commands.-- `fusion_search_cloud_files` -- **Long-running.** Recursive search for files by name/extension across cloud folders. Similar to `fusion_walk_cloud_tree` but optimized for search (stops early when enough matches found). Also blocks the main thread while running.-  - Args: `projectName` (optional), `folderPath` (optional), `nameContains` (required), `extensions` (optional), `maxResults` (default 50), `maxDepth` (default 10)-  - Returns: `{files[], stats}`--**Export formats for `fusion_export_cloud_file`:**--| Format | Extension | Use case |-|--------|-----------|----------|-| `step` | .step | Industry-standard CAD interchange (default) |-| `stl` | .stl | 3D printing, mesh-based |-| `f3d` | .f3d | Fusion 360 native archive (preserves all features) |-| `iges` | .iges | Legacy CAD interchange |-| `sat` | .sat | ACIS solid modeling kernel format |-| `smt` | .smt | Parasolid format |--Also supported for import via `fusion_import_step`: STEP (.step/.stp), STL (.stl), IGES (.iges/.igs), SAT (.sat), SMT (.smt), OBJ (.obj), F3D (.f3d).--**Round-trip: Docker → Fusion Cloud → Docker**--```bash-# === Docker to Fusion Cloud ===-# 1. Send file from Docker to Windows desktop-adom-desktop send_files '{"files": [{"path": "/home/user/component.step"}]}'-# 2. Import into Fusion-adom-desktop fusion_import_step '{"filePath": "C:/Users/john/Downloads/component.step"}'-# 3. Save to cloud (gets a wip_urn for 3D library linking)-adom-desktop fusion_save_to_cloud '{"name": "my-component", "projectName": "Personal"}'--# === Fusion Cloud to Docker ===-# 1. Find and open the cloud file-adom-desktop fusion_walk_cloud_tree '{"projectName": "Personal", "nameContains": "my-component"}'-adom-desktop fusion_open_cloud_file '{"fileName": "my-component", "projectName": "Personal"}'-# 2. Export to local filesystem-adom-desktop fusion_export_cloud_file '{"outputPath": "C:/tmp/export/my-component.step", "format": "step"}'-# 3. Pull back to Docker-adom-desktop pull_file '{"path": "C:/tmp/export/my-component.step"}'--# === Cloud management ===-adom-desktop fusion_list_cloud_projects '{}'-adom-desktop fusion_create_cloud_folder '{"folderName": "Electronics", "projectName": "Main"}'-adom-desktop fusion_list_cloud_files '{"projectName": "Main", "folderPath": "Electronics"}'-adom-desktop fusion_delete_cloud_file '{"fileName": "old-file", "projectName": "Main"}'-```--#### Manufacturing Exports (Gerbers, BOM, CPL)--Export manufacturing files from an open PCB board — everything needed to fabricate boards and assemble components via Adom's PCBA service. **All manufacturing commands require a .brd board open in PCB Editor** (use `fusion_open_board` or `fusion_show_2d_board`).--**Recommended workflow order:** `detect_layers` → `set_design_rules` → DRC → `export_gerbers` → `export_bom` → `export_cpl` → `pull_file` all back to Docker.--Every manufacturing command returns structured JSON with:-- `message` — AI-oriented summary explaining what was produced and why it matters-- `nextSteps[]` — ordered list of what to run next in the manufacturing pipeline-- `hint` — tips for interpreting results or recovering from issues-- `data` — structured metadata (paths, counts, layer info) for programmatic use--**Decision guide — which command to run:**-- **Don't know the layer count?** → Run `fusion_detect_layers` first-- **Need to check if the board meets fab specs?** → Run `fusion_set_design_rules` then DRC-- **Ready to generate fab files?** → Run `fusion_export_gerbers`, then `fusion_export_bom`, then `fusion_export_cpl`-- **Need visual review before export?** → Run `fusion_export_board_image` with preset `assembly_top` or `fabrication`-- **Something failed?** → Check `data.hint` and `data.recoverySteps` in the error response--**Commands:**--- `fusion_detect_layers` -- Detect if the open board is 2-layer or 4-layer. Uses ULP script (primary) and CAM comparison (fallback). Returns `layerCount`, `copperLayers[]`, `method` (how it was detected), and `nextSteps[]`. **Run this first** — all other manufacturing commands auto-detect layers too, but running this explicitly gives you the data before committing to exports.--- `fusion_set_design_rules` -- Apply Adom's JLCPCB-derived design rules (.edru XML files) to the open board. Auto-detects 2-layer vs 4-layer and loads the appropriate rule set. Returns `description` (human-readable rule summary), `edruFile`, and `nextSteps[]`.-  - `action`: `"apply"` (default) loads rules into the board; `"export"` saves current board rules to a file; `"show"` displays rule capabilities without modifying anything-  - `layers`: `"auto"` (default), `"2"`, or `"4"` — force a specific rule set-  - After applying: run `fusion_electron_run '{"command": "DRC"}'` to check for violations. DRC markers appear in the board editor.--- `fusion_apply_instapcb_rules` -- Convenience wrapper that applies the bundled Adom InstaPCB design rule sets without needing a local `.edru` file. Args: `layers` (`"2"` or `"4"`). Sends the bundled `.edru` from Docker to Windows automatically and loads it via the EAGLE `drc load` command. Use this instead of `fusion_set_design_rules` when you just want Adom's standard 2-layer or 4-layer InstaPCB rules.--- `fusion_load_design_rules` -- Generic loader for ANY custom `.edru` file by path. Args: `filePath`. Use this for third-party fab vendor rules (JLCPCB, PCBWay, OSHPark, etc.) that the user has on disk.--- `fusion_export_gerbers` -- Export Gerber (RS-274X) + Excellon drill files as a ZIP. Auto-detects 2-layer vs 4-layer and selects the correct JLCPCB-compatible CAM job. Returns `zipPath`, `zipSizeKB`, `files[]` (list of gerber files in the ZIP with sizes), `layerCount`, and `nextSteps[]`.-  - `outputDir`: directory for the output ZIP (default: `C:/tmp/adom-gerbers/`)-  - `boardName`: prefix for the ZIP filename (default: from active document name)-  - `layers`: `"auto"` (default), `"2"`, or `"4"` — force CAM job selection-  - Output ZIP contains: GTL, GBL (copper), GTS, GBS (solder mask), GTP, GBP (paste), GTO, GBO (silkscreen), GKO (outline), XLN (drill). 4-layer boards also get G1, G2 (inner copper).--- `fusion_export_bom` -- Export Bill of Materials as CSV. Groups identical parts by value+package with quantity counts. Returns `componentCount`, `uniquePartCount`, and `nextSteps[]`.-  - `outputPath`: (default: `C:/tmp/adom-bom.csv`)-  - `grouped`: `true` (default) groups by value+package; `false` lists every component individually-  - Output columns: Comment, Designator, Footprint, Quantity, Library — compatible with JLCPCB, PCBWay, Mouser, Digi-Key.--- `fusion_export_cpl` -- Export Component Placement List (pick-and-place) as CSV. Returns `totalPlacements`, `topCount`, `bottomCount`, and `nextSteps[]`.-  - `outputPath`: (default: `C:/tmp/adom-cpl.csv`)-  - `side`: `"all"` (default), `"top"`, or `"bottom"` — filter for single-sided assembly-  - Output columns: Designator, Mid X, Mid Y, Layer, Rotation — coordinates in mm.--- `fusion_export_board_image` -- Export PNG image of the board with layer presets. Returns `fileSize`, `preset`, and `nextSteps[]`.-  - `outputPath`: (default: `C:/tmp/adom-board.png`)-  - `dpi`: resolution (default: 300, max: 600)-  - `preset`: layer preset name (see table below)-  - `layers`: custom layer numbers as int array — overrides preset-  - `monochrome`: `true` for black & white-  - `listPresets`: set `true` to get available presets instead of exporting--**Layer presets for `fusion_export_board_image`:**--| Preset | Layers | Description |-|--------|--------|-------------|-| `all` | All | Every layer visible |-| `top_copper` | 1, 17, 18 | Top copper + pads + vias |-| `bottom_copper` | 16, 17, 18 | Bottom copper + pads + vias |-| `top_silkscreen` | 21, 25 | Top silkscreen + component names |-| `bottom_silkscreen` | 22, 26 | Bottom silkscreen + component names |-| `top_soldermask` | 29 | Top solder mask openings |-| `bottom_soldermask` | 30 | Bottom solder mask openings |-| `top_paste` | 31 | Top paste/stencil openings |-| `bottom_paste` | 32 | Bottom paste/stencil openings |-| `board_outline` | 20 | Board outline (dimension layer) |-| `drill` | 44, 45, 17, 18 | Drill holes + vias |-| `assembly_top` | 1, 17, 18, 20, 21, 25, 51 | Top assembly — copper + silk + outline |-| `assembly_bottom` | 16, 17, 18, 20, 22, 26, 52 | Bottom assembly — copper + silk + outline |-| `fabrication` | 1, 16, 17–20, 21, 22, 25, 26, 29, 30, 51 | Full fabrication view |--**Adom InstapcbPCB manufacturing capabilities (used by `fusion_set_design_rules`):**--| Parameter | Metric | Imperial |-|-----------|--------|----------|-| Layers | 1, 2, 4, 6 | — |-| Min trace width | 0.08mm | 3 mil |-| Min trace spacing | 0.08mm | 3 mil |-| Min via drill | 0.2mm | 8 mil |-| Min silkscreen text | 0.1mm | 4 mil |-| Board edge clearance | 0.05mm | 2 mil |-| Board thickness | 1.6mm | 63 mil |-| Copper weight | 0.5 oz (17.5 μm) | — |-| Solder mask | Green | — |-| Surface finish | HASL / ENIG | — |-| Turnaround | 4 hours (fab + assembly) | — |--**Manufacturing workflow — Fusion to Adom PCBA:**--```bash-# 1. Open the board-adom-desktop fusion_open_board '{"filePath": "C:/projects/myboard.brd"}'--# 2. Detect layer count (auto-selects 2-layer or 4-layer rules/CAM)-adom-desktop fusion_detect_layers--# 3. Apply Adom design rules for the detected layer count + run DRC-adom-desktop fusion_set_design_rules '{"action": "apply"}'-adom-desktop fusion_electron_run '{"command": "DRC"}'--# 4. Export manufacturing files (gerbers auto-select correct CAM job)-adom-desktop fusion_export_gerbers '{"outputDir": "C:/tmp/mfg"}'-adom-desktop fusion_export_bom '{"outputPath": "C:/tmp/mfg/bom.csv"}'-adom-desktop fusion_export_cpl '{"outputPath": "C:/tmp/mfg/cpl.csv"}'--# 5. Export board images for review-adom-desktop fusion_export_board_image '{"outputPath": "C:/tmp/mfg/top-copper.png", "preset": "top_copper"}'-adom-desktop fusion_export_board_image '{"outputPath": "C:/tmp/mfg/assembly-top.png", "preset": "assembly_top"}'-adom-desktop fusion_export_board_image '{"outputPath": "C:/tmp/mfg/board-outline.png", "preset": "board_outline"}'--# 6. Pull files back to Docker for Adom PCBA ordering-adom-desktop pull_file '{"path": "C:/tmp/mfg/bom.csv"}'-adom-desktop pull_file '{"path": "C:/tmp/mfg/cpl.csv"}'-```--### Desktop Tools--- `desktop_open_folder` -- Open a file or folder in Windows Explorer. If given a file path, Explorer opens with the file highlighted (selected). Args: `path` (file or folder path).-- `desktop_open_url` -- Open a URL in the user's **NATIVE OS BROWSER** — i.e. the real Edge / Chrome / Firefox / Brave they use every day, with their saved logins, history, bookmarks, and extensions. This is NOT pup, NOT Chrome for Testing — it's the browser the human actually uses. Hand it off when the user needs to interact with a logged-in account themselves.-  - Args: `url` (required string), `browser` (optional: `"default"` (Windows-registered default — could be Edge, Chrome, Firefox, Brave; whatever they set), `"chrome"`, `"edge"`, `"firefox"`, `"brave"`).-  - Examples:-    - `adom-desktop desktop_open_url '{"url":"https://claude.ai/"}'` -- opens in user's default native browser, already signed in-    - `adom-desktop desktop_open_url '{"url":"https://docs.example.com","browser":"edge"}'` -- force native Edge-  - Returns `{ok, browser, url, exePath?}`. Allowed schemes: http, https, mailto, ftp, ftps. Other schemes refused.--#### `desktop_open_url` vs `browser_open_window` — two completely different browsers--| | `desktop_open_url` | `browser_open_window` |-|---|---|---|-| **What browser** | Native OS browser the user uses daily — Microsoft Edge / Google Chrome / Mozilla Firefox / Brave (whichever they've set as Windows default, or the one you name explicitly) | Puppeteer-controlled **Chrome for Testing** — a separate Chromium build that pup launches |-| **Profile / data** | The user's real profile: saved logins, history, bookmarks, extensions, autofill | Isolated profile under `plugins/puppeteer/profiles/<sessionId>/` — empty, no saved logins |-| **Who interacts with the page** | The HUMAN. Claude hands the URL off and is done. | CLAUDE drives it programmatically — screenshots, clicks, eval, recording. The human just watches. |-| **Use when** | A login is required (claude.ai, GitHub, internal tools, banking) — the human's existing session must be reused | Automation, scraping, screenshot capture, video recording, headful UI testing |-| **Can Claude control it after launch?** | No — once handed off, Claude can't see or drive it | Yes — every `browser_*` verb (screenshot, eval, navigate, record, etc.) |--**Decision rule:** human-in-the-loop login flow → `desktop_open_url`. AI-driven automation → `browser_open_window`.-- `desktop_bring_to_front` -- Bring a window to the foreground by HWND or title substring. Uses keybd_event + AttachThreadInput to bypass Windows foreground lock. Preserves maximized state.-  - Args: `hwnd` (int) OR `titleContains` (string, case-insensitive). One required.-  - Example: `adom-desktop desktop_bring_to_front '{"titleContains": "Fusion"}'`-- `desktop_set_window_state` -- Change a window's show state without bringing it to the foreground (or both, if combined with `desktop_bring_to_front`). Args: `hwnd` (int) OR `titleContains` (string), plus `state` (one of `"maximize"`, `"minimize"`, `"restore"`, `"show"`, `"hide"`). Use to ensure Fusion or KiCad windows are maximized for screenshots, or to hide noisy background windows during demos.-  - Example: `adom-desktop desktop_set_window_state '{"hwnd":133560,"state":"maximize"}'`-- `desktop_revoke_approvals` -- Revoke all shell auto-approve permissions and deny pending approvals. The next shell command will show the approval dialog.-  - Example: `adom-desktop desktop_revoke_approvals`-- `desktop_caption` -- Show a global caption overlay that stays visible above ALL windows (KiCad, Fusion, Chrome). Native Win32 overlay — not a browser DOM element. Click-through: mouse events pass to windows below. Captured by screen recordings (browser `getDisplayMedia`, OBS, Game Bar).-  - Show: `adom-desktop desktop_caption '{"text":"Step 1: Opening the board","position":"top","size":"large","duration":3000}'`-  - Custom position: `adom-desktop desktop_caption '{"text":"Look here","x":0.3,"y":0.2,"size":"large","duration":0}'`-  - Hide: `adom-desktop desktop_caption '{"action":"hide"}'`-  - Multiple simultaneous captions (use `id` to keep them independent):-    ```-    adom-desktop desktop_caption '{"text":"Step 1: Opening board","id":"step","position":"center","size":"large","duration":0}'-    adom-desktop desktop_caption '{"text":"REC ●","id":"status","position":"bottom-left","size":"medium","duration":0}'-    ```-  - Replace only the step caption (status stays):-    ```-    adom-desktop desktop_caption '{"text":"Step 2: Routing","id":"step","position":"center","size":"large","duration":0}'-    ```-  - Hide just the step caption: `adom-desktop desktop_caption '{"action":"hide","id":"step"}'`-  - Args:-    - `text` (string) — caption text-    - `id` (string, optional) — identifies this caption. Captions with the same id replace each other; different ids coexist simultaneously. Default `"_default"` — so bare calls without id still replace each other for backward compat.-    - `position` — preset: `"top"`, `"bottom"` (default), `"center"`, `"top-left"`, `"top-right"`, `"bottom-left"`, `"bottom-right"`-    - `x` (float 0.0–1.0) — normalized screen X, overrides position horizontal. 0.0 = left edge, 1.0 = right edge. Centers the caption box on this point.-    - `y` (float 0.0–1.0) — normalized screen Y, overrides position vertical. 0.0 = top edge, 1.0 = bottom edge.-    - `size` — preset: `"large"` (72px), `"medium"` (32px, default), `"small"` (20px)-    - `fontSize` (int) — custom font size in px (8–400). Overrides `size` preset when provided.-    - `duration` (ms) — auto-dismiss timer. Default 4000. 0 = stay until explicitly hidden.-    - `action` — `"hide"` to dismiss a caption (with `id`: hides only that caption; without `id`: hides all captions), `"force-clear"` to EnumWindows and destroy ALL caption windows regardless of id (nuclear option — use at top of demo scripts for guaranteed clean slate)-  - Captions with the same `id` replace each other instantly (previous destroyed, no fade overlap). Captions with different ids coexist — multiple captions can be visible simultaneously.  ### Desktop Screenshots @@ -926,422 +245,25 @@ Take screenshots of the user's desktop or individual windows. All screenshots us  Multi-session Puppeteer -- each session gets its own Chrome window. Auto-starts on first command. **Always pass `sessionId` on every command.** -Two levels of granularity: **window** (one Chromium window per session, own taskbar icon) and **tab** (many tabs per session, one taskbar icon). Use windows when isolation matters, tabs when showing many related views (10 alignment previews, etc.) to avoid flooding the taskbar.--**Window-level commands** (one session = one Chrome window):-- `browser_rescan` -- (v1.5.1+) Recover orphaned Chrome windows whose CDP socket dropped. 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 injected into each page's title at launch. Idempotent. Args: `adoptOrphans?` (default false; when true, pages without a tag get attached under a generated sessionId — useful when Chrome has tabs the bridge has never seen).-  - **When you need this**: any time `browser_navigate` / `browser_eval` / `browser_screenshot` errors with `errorCode: "session_disconnected"` (the bridge knows about your sessionId but its CDP socket dropped) or `errorCode: "session_not_found"` (after a bridge restart that rebuilt from disk but couldn't reach the live Chrome). The 30s health check now auto-attempts reconnect, so most transient blips recover before you notice — but `browser_rescan` is the explicit recovery primitive.-  - Returns: `{ok, rescanned, profilesReconnected, profilesUnreachable, reattached, orphansAdopted, liveSessions, disconnectedSessions, _hint}`.-- `browser_open_window` -- Open a Chrome window. Args: `sessionId` (required), `profile` (required), `url` (required), `freshProfile` (optional), `strictPermissions` (optional, default `false`), `downloadPath` (optional, default `%USERPROFILE%\Downloads`).-  - **Full-capability default (v1.6.3+):** every pup session opens with the capabilities chip-fetcher-style flows actually need:-    - **Scripted downloads enabled.** `Page.setDownloadBehavior` is called with `behavior: 'allow'` and `downloadPath: %USERPROFILE%\Downloads` (override via the `downloadPath` arg). Re-applied on every main-frame navigation as a safety belt. This unblocks the silent-download-failure scenario: before v1.6.3, JS-triggered downloads (clicks on `<a download>`, `fetch().then(blob → save)`, vendor "Download Symbol" buttons that do `window.location = url`) could be silently dropped by Chrome — no error, no UI, no file. chip-fetcher debugged this for hours before the fix.-    - **Clipboard read/write granted.** Paste-into-vendor-form flows work without permission prompts.-    - **Notifications + geolocation + MIDI denied.** Chrome doesn't pop up permission dialogs under automation.-    - The response includes `defaultPermissionsApplied: true`, `defaultPermissions: ["clipboard-read=granted", "clipboard-write=granted", "notifications=denied", "geolocation=denied", "midi=denied"]`, `downloadsEnabled: true`, `downloadPath: <resolved abs path>`. The path is what `desktop_watch_files` polls by default — the two ends meet without configuration.-  - **Opt-out:** pass `strictPermissions: true` to keep Chromium's defaults in place. Rarely needed — only when driving an untrusted site you're auditing. With strict mode, scripted downloads may be silently dropped, clipboard prompts fire, etc.-  - **History note:** v1.4.12 used `Browser.grantPermissions(['automaticDownloads', 'notifications', 'clipboardReadWrite'])` — but Chrome 146 rejects `automaticDownloads` as an unknown permission name, taking the whole grant down. v1.6.3 fixes by using `Browser.setPermission` per-permission (per-permission failure tolerance) AND by realizing `Page.setDownloadBehavior` is the actual mechanism for unblocking scripted downloads, not a permission grant.-- `browser_close_window` -- Close a session's Chrome window (closes ALL tabs in that session). Args: `sessionId`-- `browser_list_windows` -- List all open sessions with URL, title, tabCount, active status--**Tab-level commands** (manage tabs within a session's window):-- `browser_open_tab` -- Add a tab to an existing session. Returns `{tabId, url, title, active}`. Args: `sessionId`, `url`-- `browser_switch_tab` -- Make a specific tab the active one (calls `bringToFront`). Args: `sessionId`, `tabId`-- `browser_close_tab` -- Close a specific tab. Leaves other tabs + session intact. Args: `sessionId`, `tabId`-- `browser_list_tabs` -- List all tabs in a session: `{tabs:[{tabId,url,title,active,errorCount,opener,openerTabId}], activeTabId, count}`. Args: `sessionId`. **Auto-tracks popup tabs since v1.4.7** — tabs the page spawned via `window.open()` / `target="_blank"` / form-submit-with-target=_blank appear here automatically with `opener:"popup"` and `openerTabId` pointing at the tab that triggered them. Tabs Claude opened via `browser_open_window`/`browser_open_tab` have `opener:"user"`.--**Tab-aware operations** (all accept optional `tabId`; omit = use active tab):-- `browser_navigate` -- Navigate to a new URL. Args: `sessionId`, `url`, `tabId?`-- `browser_screenshot` -- Capture screenshot, auto-resized to <=1568px. Args: `sessionId`, `maxWidth`, `fullPage`, `tabId?`-- `browser_eval` -- Evaluate JS in the page context. Args: `sessionId`, `expr`, `tabId?`-- `browser_input_dispatch` -- **Trusted** click / type / key / move via Chromium's CDP-backed `Input.dispatchMouseEvent` / `Input.dispatchKeyEvent`. Events have `isTrusted: true`, so they pass framework click-handler gates (React/Vue/Svelte) and most vendor anti-automation checks that `browser_eval`-side `dispatchEvent(new MouseEvent(...))` silently fails on. Args: `sessionId?`, `tabId?`, `type` (`click`|`move`|`type`|`key`), plus per-type fields:-  - `click`: either `selector` (uses bounding rect — preferred when DOM is stable) OR `{x, y}` viewport coords. Optional `button` (`left`/`right`/`middle`), `clickCount` (1 or 2 for double-click), `delay`, `firstMatch` (default false; see below).-  - `move`: `{x, y}` plus optional `steps` for human-like trajectory.-  - `type`: `text` to type; pass optional `selector` to focus that element first; optional `delay` between keystrokes.-  - `key`: a Puppeteer KeyInput name (`Enter`, `Escape`, `Tab`, `ArrowUp`, `F1`, etc).-  - **Smart selector pick (v1.4.8+, modal-scoped in v1.4.9+, plausibility-filtered in v1.4.10+):** when a `selector` matches multiple elements (a class like `button.wg-button--primary` often does — cookie X buttons + duplicate hidden copies + the actual submit), the bridge picks intelligently:-    1. **If a plausible modal/dialog is open** — `<dialog open>`, `[aria-modal="true"]`, `[role="dialog"]`, OR a fixed/absolute element with z-index ≥100 covering >25% of the viewport — AND it contains at least one visible interactive element with text — restrict the candidate set to elements **inside** that modal first. Tiebreak by largest visual area inside it. The page behind a modal is visually inert; clicking elements behind it is almost never what the caller intended. The "plausibility filter" added in v1.4.10 prevents empty overlay containers (Vue-Toastification toast wrappers, notification mount points, full-screen ad scrims) from being mistaken for the real modal — a common false-positive on Vue/React apps that mount toast portals at body level.-    2. Otherwise filter to **visible elements with non-empty text** and tiebreak by **largest visual area**.-    3. Fall back to visible-only, then document-order first as last resort.-  - Response includes `matchedCount`, `chosenIndex`, `clickedRect:{x,y,w,h}`, `clickedText`, `insideModal`, `modalDetected`, `modalRoot:{tag,id,cls,z,role,ariaModal}|null` (v1.4.10+ — which DOM node won the modal-detection heuristic, for diagnosing false-positives), and `pickStrategy` (`only-match` | `first-match` | `modal-scoped-largest` | `visible-text-largest` | `visible-text-largest-no-modal-match` | `visible-largest` | `fallback-first-document-order`). **Always sanity-check the response** — if `clickedText` doesn't look right, inspect `modalRoot` to see whether modal detection latched onto the intended overlay or got fooled by a sibling, then refine the selector OR pass `firstMatch:true` to skip the smart pick OR fall back to `{x, y}` coords.-  - **Use this whenever a click "lands but does nothing"** — most vendor "Generate Datasheet" / "Download" / "Submit" buttons gate on `event.isTrusted`. After clicking, follow up with `browser_list_tabs` (popups auto-track) to grab any new tab the click spawned.--- `browser_fetch_url` -- Fetch an arbitrary URL with the session's cookies and return 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 (multi-MB). Also handles ZIPs, CAD bundles, anything served as a binary content-type.-  - Args: `sessionId?`, `tabId?` (picks cookie context — defaults to active tab), `url` (required), `method?` (default GET), `headers?` (object; Cookie auto-included from session), `body?` (raw string).-  - **`saveTo` writes on the DOCKER container's filesystem** (the CLI handles the write after receiving bytes from the bridge). Pass an absolute path. Parent dirs are created. Returns `savedTo` with the canonical absolute path (verified to exist on disk) plus `savedToFilesystem:"container"`.-  - **`desktopSaveTo` writes on the WINDOWS desktop's filesystem** (the bridge writes directly via `fs.writeFileSync`). Pass an absolute Windows path (e.g. `C:\\Users\\you\\Downloads\\foo.pdf`). Returns `desktopSavedTo` with the resolved absolute path. Use this when the user wants the file local on the desktop (e.g. dropping a CAD bundle into Downloads).-  - **Without saveTo or desktopSaveTo**, returns `bodyBase64` in the response — caller decides what to do.-  - Returns: `{ok, bytes, contentType, status, sessionId, tabId, bodyBase64?, savedTo?, savedToFilesystem?, desktopSavedTo?, desktopSaveError?}`.-  - **CRITICAL READ-THE-RESPONSE rule:** when you pass `saveTo`, the response's `savedTo` is the actual filesystem path the file was written to. v1.4.8 had a bug where the bridge would respond `savedTo:"/tmp/foo.pdf"` but the file existed nowhere; v1.4.9 fixes this — `savedTo` always reflects a real on-disk path.-  - **Recipe — popup PDF capture (the right pattern):**-    ```bash-    # After browser_input_dispatch click that spawns a PDF popup, READ THE URL-    # FROM browser_list_tabs (don't eval against the popup's tabId — popups-    # auto-close fast and eval-after-close errors with a structured hint).-    LIST=$(adom-desktop browser_list_tabs '{"sessionId":"chip-fetcher"}')-    POPUP_URL=$(printf '%s' "$LIST" | jq -r '.tabs[] | select(.opener=="popup") | .url' | head -1)-    POPUP_TAB=$(printf '%s' "$LIST" | jq -r '.tabs[] | select(.opener=="popup") | .tabId' | head -1)--    adom-desktop browser_fetch_url "{-      \"sessionId\":\"chip-fetcher\",-      \"tabId\":\"$POPUP_TAB\",-      \"url\":\"$POPUP_URL\",-      \"saveTo\":\"/tmp/datasheet.pdf\"-    }"-    # → {ok:true, savedTo:"/tmp/datasheet.pdf" (exists), savedToFilesystem:"container",-    #    bytes:3798336, contentType:"application/pdf", status:200}-    ```-- **Per-call auto-recovery (v1.6.1+):** every tab-aware verb (`browser_navigate / eval / screenshot / input_dispatch / errors / reload / fetch_url`) now **silently reconnects or relaunches** before surfacing any session error. The Docker container should NEVER see "Session closed" / "detached Frame" / "session_disconnected" again unless recovery is genuinely impossible. The recovery ladder:-  1. Live session → return immediately.-  2. Session is `_lostBrowser` (CDP socket dropped after sleep/wake / network blip / puppeteer hiccup): try `attemptReconnectProfile()` which checks (a) the on-disk session file's recorded `cdpPort`, (b) the profile dir's `DevToolsActivePort`, and (c) **scans running Chrome processes for a `--remote-debugging-port=N` matching this profile dir** (this catches the canonical sleep/wake symptom — Chrome alive, CDP serving, but the bridge has no on-disk record). If reconnect succeeds, walk pages and re-attach by parsing the `(session: X)` tag from titles.-  3. Reconnect failed: kill any orphan Chrome processes for this profile, clean the profile lock files (`lockfile` on Windows, `SingletonLock` / `SingletonSocket` / `SingletonCookie` on POSIX, plus stale `DevToolsActivePort`), then relaunch via `launchSession` with the last-known URL from the session file. The sessionId is preserved — the caller gets a transparent recovery.-  4. Total failure (no profile name, no last-known URL, can't launch) → only THEN does the structured `session_not_found` / `session_disconnected` error fire.--  When recovery happens, the response includes `_recovered: true` and `_recoveryReason: "reconnect-no-page" | "relaunch"` so callers can log / metric the recovery rate.--- **Stale sessionId detection (v1.5.1+):** when auto-recovery (above) genuinely cannot fix the problem (no profile known, no last-known URL, Chrome won't relaunch — rare), a structured error fires. Two error codes you might see:-  - **`session_not_found`** — the bridge has no record of this sessionId. Recipe: check `liveSessions` for the right name, or `browser_open_window` if you want to start fresh.-  - **`session_disconnected`** — the bridge has the session in memory (and on disk) but auto-recovery couldn't reach its Chrome. Recipe: call **`browser_rescan`** to force a manual recovery pass, or `browser_open_window` with explicit URL to relaunch.--- **`browser_rescan`** (v1.5.1+, enhanced v1.6.1+) — explicit recovery primitive. v1.6.1 adds a running-process scan: walks all Chrome processes whose `--user-data-dir` is under the puppeteer profiles dir, even if the bridge has no in-memory or on-disk record of them. This is what fixed the chipsmith-after-sleep scenario where the bridge had been restarted while Chrome was alive — neither in-memory state nor session files referenced the live Chrome, but the process list did.-- **Stale tabId detection (v1.4.10):** every tab-aware verb (`browser_navigate`, `browser_screenshot`, `browser_eval`, `browser_input_dispatch`, `browser_errors`, `browser_reload`, `browser_close_tab`, `browser_fetch_url`) returns a structured error when you pass a `tabId` that no longer exists:-  ```json-  {-    "ok": false,-    "errorCode": "tab_not_found",-    "error": "Tab \"tab-3\" not found in session \"chip-fetcher\".",-    "currentTabs": ["tab-1", "tab-2"],-    "lastKnownUrl": "https://wago.priintcloud.com/datasheets/2601-3105/en/...",-    "opener": "popup",-    "openerTabId": "tab-1",-    "closedMsAgo": 1842,-    "_hint": "This tab closed 2s ago at URL https://... Call browser_fetch_url with that URL (and the opener tabId \"tab-1\" for cookies) to get its bytes — do NOT try to eval against the closed tabId."-  }-  ```-  - `lastKnownUrl` + `opener` + `openerTabId` + `closedMsAgo` are populated when the bogus tabId matches a tab that closed in the last few seconds (kept in a 20-entry per-session ring buffer). Critical for popup PDF workflows where the viewer auto-closes after rendering — you can `browser_fetch_url` directly against `lastKnownUrl` instead of having to re-trigger the spawning click.-  - **Important regression note:** v1.4.9 documented this error but the CLI was silently stripping `tabId` from `browser_navigate` / `browser_screenshot` / `browser_eval` / `browser_errors` / `browser_reload` calls before forwarding to the bridge — so the bridge always saw "no tabId" and fell back to the active tab. v1.4.10 fixes the CLI to forward `tabId` so the structured error actually fires. If you're still seeing silent fallback, your CLI is < v1.4.10 (`adom-desktop --version` to check).-- `browser_errors` -- Collected console/page errors. Args: `sessionId`, `clear` (default true), `tabId?`-- `browser_reload` -- Reload the page. Args: `sessionId`, `tabId?`--**Other**:-- `browser_status` -- All sessions with URLs, tab counts, and error counts+- `browser_open_window` -- Open a Chrome window. Args: `sessionId` (required), `url` (required), `freshProfile` (optional)+- `browser_close_window` -- Close a session's Chrome window. Args: `sessionId`+- `browser_list_windows` -- List all open sessions with URL, title, active status+- `browser_navigate` -- Navigate to a new URL. Args: `sessionId`, `url`+- `browser_screenshot` -- Capture screenshot, auto-resized to <=1568px. Args: `sessionId`, `maxWidth`, `fullPage`+- `browser_eval` -- Evaluate JS in the page context. Args: `sessionId`, `expr`+- `browser_errors` -- Collected console/page errors. Args: `sessionId`, `clear` (default true)+- `browser_reload` -- Reload the page. Args: `sessionId`+- `browser_status` -- All sessions with URLs and error counts - `browser_close` -- Close ALL sessions - `browser_wait` -- Wait for content to settle. Args: `ms`--#### Credential vault — silent HTTP Basic Auth (v1.4.4+)--When pup navigates to a host that issues an HTTP Basic Auth challenge (every `*.componentsearchengine.com` subdomain that wraps NXP / Mouser / TI / etc CAD bundles, several vendor design + doc portals), Chrome shows a NATIVE auth dialog that lives outside the page DOM. `browser_eval` can't dismiss it. Without the credential vault, the user has to type the same email + password every navigation — chip-fetcher hits this 5–10 times per chip.--The vault stores credentials in the OS keychain (Windows DPAPI / macOS Keychain / Linux libsecret) and applies them via `page.authenticate()` BEFORE the navigation, so the dialog never appears. **Passwords never leave the keychain** — `credential_list` returns host + username only.--- `credential_set` -- Store creds for a host pattern. Encrypted at rest by the user's session key.-  - Args: `host` (glob: `"*.componentsearchengine.com"` or exact `"nxp.componentsearchengine.com"`), `username`, `password`-  - Returns: `{ok, host, username}` (no password)-- `credential_list` -- List stored entries.-  - Returns: `{credentials: [{host, username, addedAt, updatedAt}]}`-  - Passwords are **never** returned by this verb.-- `credential_delete` -- Remove a credential entry. Drops the password from the keychain and the entry from the index.-  - Args: `host` (must match the pattern used at `credential_set` time, exactly)--**Host matching:** simple glob. `*.example.com` matches any subdomain; exact strings match exactly. Best-match wins (longest non-glob suffix beats shorter glob), so a `nxp.componentsearchengine.com` entry would override a `*.componentsearchengine.com` entry for that exact host.--**Recipe — chip-fetcher / CSE flow:**--```bash-# 1. One-time setup (ask user for their CSE creds, then store)-adom-desktop credential_set '{-  "host":"*.componentsearchengine.com",-  "username":"[email protected]",-  "password":"<paste>"-}'-# → {ok:true, host:"*.componentsearchengine.com", username:"[email protected]"}--# 2. Navigate freely — no popup-adom-desktop browser_open_window '{-  "sessionId":"chip-fetcher",-  "profile":"chip-fetcher",-  "url":"https://nxp.componentsearchengine.com/preview_newDesign.php?..."-}'-adom-desktop browser_eval '{"sessionId":"chip-fetcher","expr":"document.title"}'-# → "SamacSys Part Preview" (NOT "Sign in")--# 3. Audit — passwords are never returned-adom-desktop credential_list-# → {credentials:[{host:"*.componentsearchengine.com", username:"[email protected]", ...}]}--# 4. Remove-adom-desktop credential_delete '{"host":"*.componentsearchengine.com"}'-```--Hooks fire BEFORE every `page.goto` in three places: `browser_open_window`'s first nav, `browser_open_tab`, `browser_navigate`. The credential lookup is `O(N)` over stored entries which is fine for typical N (single digits).--If a host has no stored creds, navigation proceeds normally — the user will see Chrome's native dialog as before. There's no fallback prompt; the next step is for you to ask the user for the credential, then `credential_set` it.--Example — show 10 alignment previews in ONE Chrome window:-```bash-adom-desktop browser_open_window '{"sessionId":"align","profile":"align","url":"http://127.0.0.1:8901/"}'-for port in 8902 8903 8904 8905 8906 8907 8908 8909 8910; do-  adom-desktop browser_open_tab "{\"sessionId\":\"align\",\"url\":\"http://127.0.0.1:$port/\"}"-done-adom-desktop browser_list_tabs '{"sessionId":"align"}'     # see all 10-adom-desktop browser_screenshot '{"sessionId":"align","tabId":"tab-3"}'-adom-desktop browser_eval '{"sessionId":"align","tabId":"tab-5","expr":"document.getElementById(\"meta-seat-z\").textContent"}'-``` - `browser_alert_window` -- Flash the Windows taskbar orange. **Always call after updating a pup window.** Args: `sessionId`-- `browser_focus_window` -- **Tab-only.** Calls Puppeteer's `page.bringToFront()` — activates the session's tab WITHIN its Chrome window, but does NOT raise the OS window above other apps on the desktop. Args: `sessionId`. **For OS-level foreground use `browser_raise_os_window` (below).**-- `browser_raise_os_window` -- **Real OS foreground raise.** Brings the Chrome window hosting this pup session above all other desktop apps. Use this BEFORE recording, screenshots, animations, or any flow where Chrome being occluded would matter — when Chrome is in the background, `document.hidden=true` and Chrome throttles `requestAnimationFrame` to ~1 Hz, breaking cinematic camera orbits, CSS animations, video element auto-play, fps counters, etc. Internally: focuses the tab inside Chrome, then EnumWindows-finds the OS window by title containing `(session: <sessionId>)`, restores it from minimized if needed, and runs the foreground-lock bypass (`AttachThreadInput` + `keybd_event` phantom-key trick). Args: `sessionId`. Returns: `{sessionId, hwnd, title, raised, error?}`.-  ```bash-  # Typical recording prep-  adom-desktop browser_open_window '{"sessionId":"demo","profile":"demo","url":"…"}'-  adom-desktop browser_raise_os_window '{"sessionId":"demo"}'   # ← page now actually visible-  adom-desktop desktop_record_start '{"reason":"…"}'-  ```-- `browser_lower_os_window` -- **OS-level minimize.** Sends the Chrome window for this session to the back / minimized so the user gets their desktop back after Claude finishes a long pup-driven flow. Args: `sessionId`. Returns: `{sessionId, hwnd, lowered, error?}`.--### Screen Recording+- `browser_focus_window` -- Bring a session's window to foreground. Args: `sessionId` -> **CRITICAL — pick the right verb.** Claude frequently mistakes "record a pup" / "record this Chrome window" / "record the page" for desktop recording. They are NOT the same. Picking wrong gives a black/wrong-content `.webm` because the pup window may not be in the OS foreground when the desktop recorder snaps frames.--#### Decision tree (read this BEFORE you reach for any record command)--| User said... | Use |-|---|---|-| "record a pup" / "record this pup window" / "record the tab" / "record the page" / "record this Chrome window I just opened" | **`browser_record_start`** — tab-scoped via CDP, captures the pup tab regardless of foreground state, ~50 fps actual at 30 fps target, single `.webm` |-| "record my screen" / "record what I'm doing" (and Hydrogen is available, which is most of the time) | **Hydrogen's built-in:** `recording start --share screen` — same `getDisplayMedia` pipeline but already wired into Hydrogen with native UI. Prefer this for casual "record my screen" asks. |-| "record everything on screen including KiCad / Fusion / OS dialogs / multiple apps switching" | **`desktop_record_start`** with `confirmDesktopNotTabRecording: true` (required arg — see below) |-| "record a pup tab AND give me a parallel desktop video at the same time" (e.g. ralph-loop screenshot tests with simultaneous narration) | **Both at once:** `browser_record_start` on the tab AND `desktop_record_start` on the desktop. Adom-desktop's desktop recorder exists for this case — Hydrogen's recorder can't run while it's busy doing tab capture, so adom-desktop fills that gap. |--#### Why this matters--- **Wrong verb → wrong content.** desktop_record_* captures whatever is on the user's monitor at frame time. If the pup window is occluded behind your IDE / Slack / their email client, the resulting clip shows YOUR app, not the pup tab. Even with `browser_raise_os_window` first, the user's natural alt-tab activity covers the pup window within seconds.-- **Hydrogen already has desktop recording.** Most "record my screen" asks should just be Hydrogen's recorder. Adom-desktop's `desktop_record_*` is mostly redundant — it shines exactly when Hydrogen is busy capturing a tab and you need a parallel desktop angle.--#### The guardrail--`desktop_record_start` REQUIRES `confirmDesktopNotTabRecording: true`. Without it, the call returns `errorCode: desktop_record_needs_confirmation` with a long error string explaining the alternatives (browser_record_start, Hydrogen, or pass the confirm). This is intentional friction — re-read the error and confirm you really want full-desktop, not pup-tab.--#### Output summary--| Mode | Commands | HUD on screen | Concurrent? | Output |-|---|---|---|---|---|-| **Tab** (one pup tab's viewport) | `browser_record_*` | NO (the user already sees the tab) | YES (one per tab) | Single finished `.webm` (real-time playback, ~50fps actual) |-| **Desktop** (full screen) | `desktop_record_*` | YES — always-on-top control panel with reason + manual stop | No (one at a time) | Single finished `.webm` |--#### Desktop recording (HUD)--The recorder is a **visible Chrome window** in the bottom-right corner showing recording status, the stated reason, and a manual Stop button. The HUD persists across multiple short clips (a "session"); only `desktop_recorder_close` (or 5 min idle) dismisses it.--**Two REQUIRED args on `desktop_record_start`:**-- `reason` — string, why you're recording (renders in HUD title + sidecar `.json`)-- `confirmDesktopNotTabRecording: true` — boolean acknowledgement that you understand this captures the WHOLE desktop, NOT a pup tab. The bridge rejects calls without this with `errorCode: desktop_record_needs_confirmation` and a hint pointing at `browser_record_start` (for pup tab) or Hydrogen (for casual screen recording).--```bash-# Start a desktop clip (HUD opens with reason; recording begins).-# Note: confirmDesktopNotTabRecording: true is REQUIRED — without it the-# bridge rejects with errorCode: desktop_record_needs_confirmation.-adom-desktop desktop_record_start '{-  "reason":"Parallel desktop angle alongside Hydrogen ralph-loop tab capture",-  "confirmDesktopNotTabRecording": true,-  "monitor":"primary",-  "fps":30,-  "audio":false-}'-# → { "ok":true, "recordingId":"rec-1", "filePath":"…\\rec-desktop-….webm", … }--# … drive KiCad/Fusion/etc …--# Stop this clip — HUD stays open, ready for next clip-adom-desktop desktop_record_stop '{"recordingId":"rec-1"}'-# → { "ok":true, "filePath":"…\\rec-desktop-….webm", "sizeKB":4231, "durationMs":18432 }--# (Optional) Record more clips in the same session — reason persists, but-# confirmDesktopNotTabRecording must be passed every time.-adom-desktop desktop_record_start '{-  "reason":"Parallel desktop angle alongside Hydrogen ralph-loop tab capture",-  "confirmDesktopNotTabRecording": true-}'-# … etc …--# When done: close the HUD-adom-desktop desktop_recorder_close-# → { "ok":true, "sessionSummary":{"clipCount":3, "totalSizeKB":12390, "reason":"…", "clips":[…]} }--# Pull each WebM to Docker (already a finished video — no muxing needed)-adom-desktop pull_file '{"filePaths":["C:\\…\\rec-desktop-….webm"], "saveTo":"/tmp"}'-ffprobe -v error -show_entries stream=codec_name,duration -of default=nw=1 /tmp/rec-desktop-….webm-# → codec_name=vp9, duration=18.432-```--Other commands:-- `desktop_recorder_open '{"reason":"…"}'` — open the HUD without starting a clip (so the user knows ahead of time you're about to record)-- `desktop_record_status` — `{hudOpen, reason, currentRecording, clipsThisSession}`-- `desktop_record_list` — completed `.webm` files with sidecar metadata (incl. reason)-- `desktop_list_monitors` — primary monitor info (v1.1: full multi-monitor enumeration)--The user can click Stop in the HUD at any time — that converges through the same code path as your `desktop_record_stop` call. Don't fight it; if the user stops manually, treat the clip as complete and react accordingly.--#### Tab recording (real-time video, no HUD, concurrent)--**For smooth video — narrated demos, motion-heavy scenes, animations, anything the user will watch end-to-end — use `browser_record_start/stop` NOT a `browser_screenshot` loop.** The screenshot loop maxes out around 3 fps and reads as choppy.--**Mechanism:** CDP `Page.startScreencast` (Chrome's native compositor-driven JPEG-frame stream) with `Page.screencastFrameAck` per-frame backpressure. Each frame is written to disk with a wall-clock timestamp, then ffmpeg muxes into a single VP9 `.webm` using the concat demuxer with **per-frame durations** so playback matches real wall time. Tab-scoped at the protocol level (the CDP session is bound to a specific Page target — no cross-tab/cross-Chrome leakage).--**FPS expectations.** Chrome's compositor pushes a frame each time the page paints. `everyNthFrame` is computed from the requested fps (e.g. 30 fps → every 2nd paint frame). On rAF-animated pages with the tab foregrounded, expect actual fps to **meet or exceed the target** — measured 49.7 fps actual at 30 fps target on a 2.5K viewport. On occluded windows Chrome paint-throttles, so always raise the OS window first.--**Why not MediaRecorder + getDisplayMedia?** Tested — Chrome 146 crashes when `getDisplayMedia({preferCurrentTab:true})` is called via Puppeteer (known upstream bug, [puppeteer #13478](https://github.com/puppeteer/puppeteer/issues/13478)). `Page.startScreencast` is Chrome's other native video pipeline; it's stable and produces equivalent output (compositor frames as JPEGs).--Output is a single finished **`.webm`** file — no tar, no concat staging exposed to callers, no Docker-side mux. Just `pull_file`.--**Raise the OS window first.** Chrome paint-throttles occluded windows, so calling `browser_record_start` on a backgrounded tab produces stutters. Always:--```bash-adom-desktop browser_raise_os_window '{"sessionId":"align"}'   # surface pup tab to OS foreground-adom-desktop browser_record_start '{"sessionId":"align","fps":30}'-```--**Anti-throttle launch flags (already baked into pup).** Pup spawns Chrome with four flags that suppress most of Chrome's background throttling so recordings don't collapse to ~1 fps when the user alt-tabs to another app:--- `--disable-renderer-backgrounding`-- `--disable-backgrounding-occluded-windows`-- `--disable-background-timer-throttling`-- `--disable-features=CalculateNativeWinOcclusion`--Verified: with all four flags, `browser_record_start` on a window completely covered by Notepad still produces **29.65 fps actual at 30 fps target**.--> **Caveat: pup sessions launched in a bridge version older than v1.3.32 don't have these flags.** Chrome launch flags apply only at process spawn — they cannot be added retroactively. If `fpsActual` reports ≤1 even with `browser_raise_os_window` first, the session's Chrome was launched with an old flag set. Fix: close + reopen the session (`browser_close '{"sessionId":"<id>"}'` then `browser_open_window`) so it spawns a fresh Chrome with the current flags. The `kicadVersionUsed`-style proof: there's no per-session "flags" reporter yet, so the reliable test is the recording fps itself.--Multiple recordings on different tabs run in parallel — each tab has its own capture loop.--```bash-# Record three tabs in parallel-for TAB in tab-1 tab-2 tab-3; do-  adom-desktop browser_record_start "{\"sessionId\":\"align\",\"tabId\":\"$TAB\",\"fps\":30}"-done--# … drive each tab through its scenario …--# Stop and pull each (already a finished webm — no muxing step on Docker)-for REC_ID in rec-tab-1 rec-tab-2 rec-tab-3; do-  STOP=$(adom-desktop browser_record_stop "{\"sessionId\":\"align\",\"recordingId\":\"$REC_ID\"}")-  WEBM=$(printf '%s' "$STOP" | jq -r '.output | fromjson | .filePath')-  adom-desktop pull_file "{\"filePaths\":[\"$WEBM\"],\"saveTo\":\"/tmp\"}"-done-```--Args: `sessionId` (required), `tabId` (optional — defaults to active tab), `fps` (default 30; note the actual ceiling is ~10-15 fps), `quality` (default 85, 1-100 JPEG quality), `maxDurationMs` (default 600000).--Returns from `browser_record_stop`: `{recordingId, sessionId, tabId, filePath, sizeKB, durationMs, frameCount, fpsTarget, fpsActual, stopReason}`.--Other commands:-- `browser_record_status '{"sessionId":"align"}'` — list active tab recordings with live `frameCount`, `fpsTarget`, `fpsActual`, `durationMs`, `sizeBytesApprox`-- `browser_record_list` — completed `.webm` files on disk with sidecar metadata-- Tab close auto-stops any recording for that tab (mux fires, file finalizes, you don't have to call `browser_record_stop` first)--**Notes & gotchas:**-- The desktop HUD itself is captured by the desktop recording (it's a visible window). Same as Hydrogen — accepted cost for the visible-status UX. The window is small + bottom-right.-- ffmpeg is **required** on Windows for tab recording (the mux step). Install via `winget install Gyan.FFmpeg`. Bridge logs the detection result at startup.-- `maxDurationMs` defaults to 600000 (10 min) for safety; raise it for longer recordings.-- On bridge shutdown (graceful), active recordings are drained — the mux runs and you get a valid `.webm`.-- The mux file (`<rec>.webm`) is produced by ffmpeg and has a valid `format=duration`. The frames staging dir (`<rec>.frames/`) is deleted after a successful mux.--### Filesystem primitives — `desktop_list_files` / `desktop_watch_files` / `desktop_pull_glob` (v1.5.0+)--**These are the canonical "wait for a download" primitives.** They replace the `shell_execute` + PowerShell-poll-Downloads pattern that earlier callers (chip-fetcher, similar tools) had to use. No shell, no user approval prompt, no parsing of `dir` output. They run pure Rust on the desktop side, so they're fast, predictable, and never trip over PowerShell quoting.--The pattern they replace looks like this:--```powershell-# OLD — DON'T do this for new code:-$before = (Get-Date).ToFileTime()-# … click download …-while (-not (Get-ChildItem ~/Downloads -Filter ul_*.zip | Where { $_.LastWriteTime.ToFileTime() -gt $before })) {-  Start-Sleep -Seconds 1-}-$file = Get-ChildItem ~/Downloads -Filter ul_*.zip | Sort LastWriteTime -Desc | Select -First 1-```--becomes one call:--```bash-# NEW — single primitive, no shell:-adom-desktop desktop_watch_files '{-  "path": "%USERPROFILE%\\Downloads",-  "glob": "ul_*.zip",-  "timeoutMs": 60000-}'-# → {ok:true, file:{path,name,size,mtime}, elapsedMs} on success-# → {ok:false, error:"timeout", elapsedMs, _hint, ...} on timeout-```--#### `desktop_list_files` — one-shot directory listing--Lists files in a directory (non-recursive) matching a shell-glob, optionally filtered to those modified after a given timestamp.--- Args: `path` (Windows abs path; `~` / `%USERPROFILE%` / `%APPDATA%` / `%LOCALAPPDATA%` / `%TEMP%` are expanded), `glob` (default `*`; shell-style with `*` and `?`; case-insensitive on Windows), `modifiedSince` (optional unix-seconds OR ISO-8601 string).-- Returns: `{ok, path, glob, files: [{path, name, size, mtime}, ...] sorted newest-first, count}`.-- Does NOT recurse. Lists one directory only.--#### `desktop_watch_files` — block until a match arrives--Polls the directory every `pollMs` (default 1000) until at least one file matches the glob with `mtime > since`, or `timeoutMs` (default 60000, max 600000 = 10 min) elapses.--- Args: same as `desktop_list_files` plus `since` (defaults to **now** — without an explicit value, only NEW files report), `timeoutMs`, `pollMs`.-- Returns on match: `{ok:true, file:{path,name,size,mtime}, elapsedMs}`.-- Returns on timeout: `{ok:false, error:"timeout", elapsedMs, _hint, path, glob}`.-- The `since`-defaults-to-now behavior matches the canonical "click then watch" flow. If you might miss the file by starting the watch slightly late (e.g. fast small downloads), record `$(date +%s)` BEFORE the click and pass it as `since`.--#### `desktop_pull_glob` — one-shot orchestrator (recommended for download flows)--Composes `desktop_list_files` (or `desktop_watch_files` if `wait:true`) with the existing streaming `pull_file` mechanism. The whole "wait for a download then pull it" flow in one call:--```bash-BEFORE=$(date +%s)-# … trigger the download click via browser_input_dispatch …-adom-desktop desktop_pull_glob "{-  \"path\": \"%USERPROFILE%\\\\Downloads\",-  \"glob\": \"ul_*.zip\",-  \"since\": $BEFORE,-  \"wait\": true,-  \"timeoutMs\": 60000,-  \"saveTo\": \"/tmp/cse-out\"-}"-# → {ok:true, files:[{name,path,size,sha256,chunks}, ...], errors:[], matchedCount}-```--- When `wait:true`: blocks via `desktop_watch_files` until at least one match appears, then re-lists the directory and pulls **everything matching** (so a `.crdownload` + final `.zip` written in the same poll tick both come along).-- When `wait:false` (default): just lists what's there now and pulls those.-- Pulls use the existing `pull_file` streaming pipeline — sha256-verified, ~1MB chunks, resumes the same way `pull_file` does.--#### Glob semantics--- `*` matches any run of non-separator chars (within a single filename — these primitives don't recurse).-- `?` matches a single non-separator char.-- Case-insensitive on Windows (matches the filesystem); case-sensitive on macOS/Linux.-- Examples: `ul_*.zip`, `LIB_*.zip`, `*.pdf`, `datasheet_*.pdf`, `*.step`, `*.STEP` (same on Windows).--#### Path expansion--- `~` and `~/foo` → home dir (Linux/macOS convention; works on Windows too).-- `%USERPROFILE%` → home dir (Windows; also works cross-platform).-- `%APPDATA%` → Windows roaming app data (`dirs::config_dir()` on others).-- `%LOCALAPPDATA%` → Windows local app data (`dirs::data_local_dir()` on others).-- `%TEMP%` → Windows TEMP env var; `/tmp` on Linux/macOS.-- Expansion is case-insensitive: `%userprofile%` works the same as `%USERPROFILE%`.-- Forward slashes (`/`) and back slashes (`\`) both work in the path string.--#### Time format for `since` / `modifiedSince`--- Number: unix seconds (e.g. `1746483600`). Floats accepted.-- String of digits: same (e.g. `"1746483600"`).-- ISO-8601 / RFC-3339 string: e.g. `"2026-05-04T22:30:00Z"` or with offset `"2026-05-04T15:30:00-07:00"`.-- Files with `mtime <= since` are filtered out (strict `>`).--### Shell — `shell_execute` (escape hatch only)+### Shell  - `shell_execute` -- Run a shell command on the desktop. Returns `{pending: true, requestId}` when the approval dialog shows. Poll with `get_deferred_result` for the output after the user approves. - `shell_kill_all` -- Kill all running shell commands and deny pending approvals. -**Deprecated for download polling.** As of v1.5.0, use `desktop_watch_files` / `desktop_pull_glob` for waiting on download arrivals — those don't require user approval, don't go through PowerShell quoting, and are O(directory entries) rather than spawning a process every second. `shell_execute` itself stays as an escape hatch for genuinely shell-only operations (multi-step ad-hoc admin tasks, chained pipelines, etc.).- Use `where python` / `C:\Python3xx\python.exe` for Python -- avoid `python` which may not be on PATH.  ## Detecting App Installation@@ -1355,113 +277,15 @@ adom-desktop status  ### Handling "not installed" errors -When a command returns `errorCode: "node_not_found"`, `errorCode: "kicad_not_installed"`, or `errorCode: "fusion_not_installed"`, guide the user through installation:--**Picking the right native browser + profile (v1.7.1+):**--When you need to open a URL in the user's NATIVE browser AND it matters which account is signed in (work Google Workspace vs personal Gmail vs media YouTube channel etc.), `desktop_open_url` alone isn't enough — you need to target a specific profile. The flow:--```bash-# 1. Discover what's installed + which profiles are configured.-adom-desktop desktop_list_browsers '{}'-# → {-#     "default": "chrome",-#     "browsers": [-#       { "name": "chrome", "displayName": "Google Chrome", "version": "146...",-#         "exePath": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",-#         "profiles": [-#           { "id": "Default",   "name": "John (work)",  "gaia": "[email protected]",      "isDefault": true },-#           { "id": "Profile 1", "name": "Personal",     "gaia": "[email protected]" },-#           { "id": "Profile 2", "name": "Adom Media",   "gaia": "[email protected]" }-#         ] },-#       { "name": "edge", "profiles": [...] },-#       { "name": "firefox", "profiles": [{"id":"default-release","name":"default","isDefault":true}] }-#     ]-#   }--# 2. Match the URL's context to the right profile, then open it there.-# E.g. opening a Google Doc shared by your work team → use the work profile:-adom-desktop desktop_open_url '{-  "url":"https://docs.google.com/document/d/...",-  "browser":"chrome",-  "profile":"Default"-}'-# Or YouTube channel management for the media account:-adom-desktop desktop_open_url '{-  "url":"https://studio.youtube.com",-  "browser":"chrome",-  "profile":"Profile 2"-}'-```--Profile-picking heuristics for Docker Claude:-- **Workspace / @adom.inc URLs** → match `profile.gaia.endsWith("@adom.inc")` and not the `media@` one → typically `Default`.-- **Personal Gmail / Drive / etc.** → match `profile.gaia === "[email protected]"` (or whatever the user's personal address resolves to).-- **YouTube Studio / channel-specific work** → match the media account profile.-- **Random scratch / experiments** → use the user's Edge profile, where they keep miscellaneous accounts (per the user's stated preference).-- **No clear match** → fall back to `browser:"default"` (no profile flag) and let the user pick.--`profile` is optional. Omit it to open in whichever profile the browser was last using (legacy v1.6.x behavior). Profile flag is silently ignored when `browser:"default"` (no clean way to inject through the OS URL handler — name the browser explicitly to use it).--**Node.js not installed (the puppeteer bridge can't auto-spawn):**--v1.7.0+ — any `browser_*` command returning `errorCode:"node_not_found"` means Node isn't on the desktop machine. Trigger the unattended winget install:--```bash-adom-desktop desktop_install_node '{}'-# → runs `winget install --id OpenJS.NodeJS.LTS --silent ...` (Windows only).-# → ~1-2 min; downloads + installs the Node.js LTS line (well-tested with puppeteer).-# → returns {ok:true, exitCode:0, _hint:"Retry the original browser_* command"} on success.-# → on failure: {ok:false, errorCode:"winget_unavailable" | "winget_install_failed", _hint}.-```--After install, **just retry the original `browser_*` command** — the bridge picks up the new node.exe via the registry App Paths lookup (which winget writes), no Adom Desktop restart needed. v1.7.0+ also pre-fetches Node + Chrome for Testing on first launch after install (background task on AUTOSTART_VERSION 11), so most users will never see `node_not_found` to begin with.--If `desktop_install_node` returns `errorCode:"winget_unavailable"` (older Windows or managed corporate device), fall back to manual:--> Node.js LTS is required for browser automation. winget isn't available on this machine — download Node.js LTS from https://nodejs.org/ and run the installer. Let me know when it's done and I'll retry.--**v1.7.0+ Node detection.** The bridge looks for `node.exe` in:-- System PATH (via `where node` on Windows, `which node` elsewhere)-- Windows registry App Paths (catches winget, MSIX, custom-dir installs)-- Standard install dirs: `C:\Program Files\nodejs\`, `C:\Program Files (x86)\nodejs\`, `%LOCALAPPDATA%\Programs\nodejs\`-- Per-user version managers: nvm-windows (`%APPDATA%\nvm\`), volta (`%LOCALAPPDATA%\Volta\bin\`), fnm (`%FNM_DIR%`)-- macOS Homebrew (`/opt/homebrew/bin`, `/usr/local/bin`)--If detection still misses an install on a user's machine, get the actual `node.exe` path from the user — that's a real bug to file.--**KiCad not installed:**+When a command returns `errorCode: "kicad_not_installed"` or `errorCode: "fusion_not_installed"`, guide the user through installation:  **KiCad not installed:**--v1.6.0+ — try the unattended winget install FIRST before asking the user to download manually:--```bash-adom-desktop desktop_install_kicad '{}'-# → runs `winget install --id KiCad.KiCad --silent ...` (Windows only).-# → 2-5 minutes; downloads ~700 MB then installs.-# → returns {ok:true, exitCode:0, _hint:"Run kicad_list_versions to verify"} on success.-# → on failure (no winget / network failure / corporate-managed device):-#   {ok:false, errorCode:"winget_unavailable" | "winget_install_failed", _hint, ...}-```--If `desktop_install_kicad` fails with `errorCode:"winget_unavailable"`, fall back to manual:--> KiCad isn't installed and winget isn't available. Download from https://www.kicad.org/download/ — it's free and open source, install the latest stable (9.x or 10.x).+> KiCad isn't installed on your desktop. Would you like to install it?+> Download from: https://www.kicad.org/download/+> It's free and open source. Install the latest stable version (9.x recommended). > Let me know when the install is done and I'll verify the connection. -After EITHER path, have them restart Adom Desktop OR call `kicad_list_versions` again — the kicad bridge caches detection results, so a fresh scan may be needed to pick up freshly-installed binaries.--**v1.6.0+ — improved KiCad detection.** The bridge now scans for KiCad in:-- `C:\Program Files\KiCad\<version>\` (the standard location)-- `C:\Program Files (x86)\KiCad\<version>\` (32-bit edge cases)-- `%LOCALAPPDATA%\Programs\KiCad\<version>\` (winget per-user installs)-- `%LOCALAPPDATA%\KiCad\<version>\` (rare manual installs)-- Windows registry: `HKLM\SOFTWARE\KiCad\<version>\InstallationPath` and the `WOW6432Node` mirror-- `HKCU` mirrors of the above for per-user installs-- macOS: `/Applications/KiCad/` and `/Applications/` directly--If detection still misses an install on a user's machine, that's a real bug to file — get the actual install path from the user and we'll add it to the scan list.+After install, have them restart Adom Desktop (so the KiCad bridge re-detects).  **Fusion 360 not installed:** > Fusion 360 isn't installed on your desktop. Would you like to install it?@@ -1473,13 +297,11 @@ After install, have them restart Adom Desktop, then run `adom-desktop status` to  ### Handling "not running" errors -When `errorCode: "fusion_not_running"`, launch it programmatically with the first-class startup command:+When `errorCode: "fusion_not_running"`, launch it programmatically: ```bash-adom-desktop fusion_start-# On Windows: discovers exe via %LOCALAPPDATA% env var webdeploy glob.-# On Docker: delegates to the bridge on the connected desktop via relay.-# ~15-30s typical. Auto-dismisses startup dialogs.-# Returns {"addinReady": true, "pickerDismissed": true|false, ...}+adom-desktop fusion_launch+# Waits ~15-30s for Fusion to start + add-in to connect+# Returns {"success": true, "addinConnected": true} ```  KiCad doesn't need to be running for most commands (the bridge launches it on demand).@@ -1491,56 +313,6 @@ When Fusion is running but `addinInstalled: false` or `addinConnected: false`:  The add-in auto-installs when the Fusion bridge starts and Fusion is detected. -### Handling `fusion_addin_not_responding` errors--When `errorCode: "fusion_addin_not_responding"`, Fusion is running but the AdomBridge add-in isn't answering. Call `fusion_dismiss_blocking_dialogs` FIRST — a modal dialog is the most common cause. If that doesn't help, try `fusion_start` to restart Fusion cleanly. Last resort: user enables add-in manually via UTILITIES > ADD-INS > AdomBridge > Run on Startup + Run.--### Handling `main_thread_busy` errors--When `errorCode: "main_thread_busy"`, the Fusion add-in's main thread is occupied by a long-running command (typically `walk_cloud_tree` or `search_cloud_files`). This applies **across all bridges/sessions** — even if you didn't start the walk, another session might have.--**Do NOT:**-- Retry the failed command — it will block behind the same lock-- Call `fusion_dismiss_blocking_dialogs` — there's no dialog to dismiss, and sending Escape will interrupt the active walk-- Force-kill Fusion — the walk will complete on its own--**Do:**-- Wait for the walk/search to finish. If you started it, you should be using `adom-desktop watch` (see "Live folder progress streaming with `watch`" above) which streams live progress automatically. If another session started it, poll `fusion_addin_status` every 2–5s to check progress.-- Use commands that **don't need the main thread** while waiting:-  - `fusion_addin_status` — check busy state and walk progress-  - `fusion_window_info` — get window HWND, title, dialogs-  - `fusion_screenshot_fusion` — capture what Fusion looks like-  - `fusion_click_fusion` — click in the Fusion window-  - `fusion_send_key` — send keyboard input-  - `fusion_close_window` — close a specific dialog by HWND--The response includes progress info:-```json-{-  "errorCode": "main_thread_busy",-  "busyCommand": "walk_cloud_tree",-  "elapsedSeconds": 42.3,-  "walkProgress": {-    "foldersVisited": 15,-    "filesFound": 87,-    "currentFolder": "Molecules/XRP",-    "queueSize": 8-  },-  "_hint": "Add-in is busy with a long-running command. Do NOT retry..."-}-```--**Stalled walk detection**: If `fusion_addin_status` returns `busy: true` but `walkProgress` is missing and `mainThreadStalled: true`, a modal dialog is blocking the event loop — the walk was dispatched but never started. Call `fusion_dismiss_blocking_dialogs`, then the walk auto-resumes.--### Auto-recovery (`_autoRecovery` field)--When a fusion_* command fails with "not responding" / "not connected", the CLI automatically:-1. Calls `fusion_dismiss_blocking_dialogs` to clear any modal-2. If successful, retries the original command-3. Attaches `_autoRecovery: {action, dismissed[]}` to the retry result--If you see `_autoRecovery` in a response, the command already succeeded after auto-dismissal — no manual intervention needed. The field is informational.- ## Troubleshooting  ### Check connection status
install.shadded+15
@@ -0,0 +1,15 @@+#!/bin/bash+set -e+PKG_DIR="$(cd "$(dirname "$0")" && pwd)"+SKILLS_DIR="$HOME/.claude/skills"+mkdir -p "$SKILLS_DIR/adom-desktop"+if [ -f "$PKG_DIR/SKILL.md" ]; then+  cp "$PKG_DIR/SKILL.md" "$SKILLS_DIR/adom-desktop/SKILL.md"+fi+for skill_dir in "$PKG_DIR/skills"/*/; do+  [ -d "$skill_dir" ] || continue+  skill_name=$(basename "$skill_dir")+  mkdir -p "$SKILLS_DIR/$skill_name"+  cp -r "$skill_dir"* "$SKILLS_DIR/$skill_name/"+done+echo "Installed adom-desktop"
package.jsonadded+11
@@ -0,0 +1,11 @@+{+  "slug": "adom-desktop",+  "version": "1.0.0",+  "type": "app",+  "description": "Laptop bridge: screenshots, file transfer, notifications, KiCad + Fusion 360 control, real-Chrome (pup) automation. One install gives Claude the main + pup + kicad + fusion skills.",+  "dependencies": {},+  "scripts": {+    "install": "./install.sh",+    "uninstall": "./uninstall.sh"+  }+}
uninstall.shadded+4
@@ -0,0 +1,4 @@+#!/bin/bash+set -e+rm -rf "$HOME/.claude/skills/adom-desktop"+echo "Uninstalled adom-desktop"