Adom Desktop
UnreviewedLaptop bridge: screenshots, file transfer, notifications, KiCad + Fusion 360 control, real-Chrome (pup) automation. One install gives Claude the main + pup + kicad + fusion skills.
name: adom-desktop
description: Use when the user wants to send files to their desktop, control KiCad or Fusion 360, send desktop notifications, or troubleshoot the desktop connection. Provides CLI tools for bridging the Docker container to the user's local machine.
Adom Desktop
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:
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
Claude Code -> adom-desktop <command> -> Relay Server (HTTP :8766) -> WebSocket :8765 -> Adom Desktop App -> KiCad / Fusion 360 / Browser / Shell
The adom-desktop binary is a single Rust CLI that does everything:
adom-desktop serve-- Start the relay server (WebSocket :8765 + HTTP API :8766)adom-desktop <command> '<json>'-- Send commands to the desktop app via the running relay
The relay server runs in the Docker container. The Adom Desktop app runs on the user's PC and connects via WebSocket.
Starting the Relay Server
The relay must be running before the desktop app can connect:
adom-desktop serve &
This starts:
- WebSocket server on
0.0.0.0:8765(desktop app connects here) - HTTP API on
127.0.0.1:8766(CLI commands go here)
Check if it's running:
curl -sf http://127.0.0.1:8766/health
First-Time Setup (Install & Connect)
You are running on a Docker container. You have NO access to the user's desktop. Guide them step-by-step, ask questions, wait for answers, and verify each step.
Step 1: Check if already connected
adom-desktop ping
If this returns { "status": "connected" }, the desktop is already set up -- skip to "Verify the connection" below. If it errors, continue.
Step 2: Ensure the relay is running
curl -sf http://127.0.0.1:8766/health
If not running: adom-desktop serve &
Step 3: Ask what OS they use
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
.dmgfrom the wiki or GitHub Releases. Mount, drag to Applications, then runxattr -cr "/Applications/Adom Desktop.app"(unsigned build). Launch and connect to the relay. - Linux (Ubuntu/Debian) -- proceed. Download from the wiki: either the
.debpackage (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.
Step 4: Install & connect
Run adom-desktop setup_desktop to get the installer_url and server_config fields. Then present both options to the user — the Claude Code prompt (fastest) and the manual steps (if they don't have Claude Code/Desktop):
Option A: Automatic setup via Claude Code (recommended)
If you have Claude Code or Claude Desktop on your laptop, paste this prompt:
Install Adom Desktop and connect it to my cloud container. Download the installer from
<installer_url>, run it silently, then write this JSON to%USERPROFILE%\.adom\config.json:{"servers":[<server_config JSON>]}Then launch "Adom Desktop" from the Start Menu.
(Replace <installer_url> and <server_config JSON> with the actual values from setup_desktop.)
Option B: Manual setup
- Download the installer:
- Run it and follow the prompts — the app installs to the Start Menu as "Adom Desktop"
- Open Adom Desktop from the Start Menu
- Paste this JSON into the text field that says "Paste server JSON to add ..." and press Enter:
<server_config JSON> - The server will appear and auto-connect
Wait for the user to confirm they see a green dot next to the server name before proceeding.
Important: Each container has its own relay server. New containers need a new entry -- old entries from previous containers won't work.
Step 6: Verify the connection
adom-desktop ping
# Expected: { "echo": "pong", "roundTripMs": ..., "status": "connected" }
adom-desktop status
# Expected: one client with the user's hostname and capabilities
adom-desktop notify_user '{"title":"Hello from Docker!","body":"Your desktop is connected."}'
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
Ask: "Would you like to use the visual browser debugging feature? This opens Chrome windows on your desktop that I can control."
- Yes + Node.js installed -- "Great, the browser bridge auto-installs deps on first use."
- Yes + no Node.js -- "Download Node.js from https://nodejs.org (LTS), install it, then restart Adom Desktop."
- No -- skip, they can enable it later
Common connection issues
| Symptom | Fix |
|---|---|
ping returns "No desktop client connected" |
User hasn't added this container in the desktop app yet |
| Desktop app shows "disconnected" | Check the URL uses wss:// (not ws:// or https://), port is 8765 |
| Relay server not running | adom-desktop serve & |
| Multiple stale connections | adom-desktop kick_all -- app auto-reconnects within seconds |
| Desktop app not installed | Download from github.com/adom-inc/adom-desktop/releases |
CLI Tool
adom-desktop <command> '<json-args>'
Examples:
adom-desktop ping
adom-desktop status
adom-desktop browser_open_window '{"sessionId":"dart2","url":"https://example.com"}'
adom-desktop browser_eval '{"sessionId":"dart2","expr":"document.title"}'
adom-desktop browser_screenshot '{"sessionId":"dart2"}'
adom-desktop browser_list_windows
adom-desktop browser_close_window '{"sessionId":"dart2"}'
adom-desktop notify_user '{"title":"Hello","body":"From Docker"}'
adom-desktop shell_execute '{"command":"echo hello"}'
adom-desktop pull_file '{"filePaths":["C:\\Users\\john\\Downloads\\image.png"],"saveTo":"/tmp"}'
Output: JSON to stdout. Screenshots save to /tmp/conduit-screenshots/ and return the file path.
Config location: The desktop app stores server config at ~/.adom/config.json (%USERPROFILE%\.adom\config.json). This is separate from the binary — config survives updates and reinstalls.
Available Commands
Get full structured command list with descriptions, args, and prerequisites:
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.status-- Check who's connected, their capabilities, desktop paths, and app installation status. Thedesktop.appsobject shows:kicad.installed/kicad.version/kicad.bridgeRunningfusion360.installed/fusion360.running/fusion360.bridgeRunning/fusion360.addinInstalled/fusion360.addinConnectedbrowser.bridgeRunning
kick_all-- Force-disconnect all WebSocket clients. Active Adom Desktop apps auto-reconnect within seconds.
File Transfer
send_files-- Send files from the Docker container to the desktop. Files are base64-encoded in transit.filePaths: array of absolute paths on the servertargetApp: "kicad", "fusion360", or "general"destinationFolder: relative subfolder only (e.g."kicad/symbols","fusion"). The desktop app controls the base directory. Absolute paths are rejected.- Returns
destinationPaths[]with the absolute path of every saved file.
pull_file-- Pull files from the desktop to the container.filePaths: array of absolute Windows paths on the desktopsaveTo: 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}]. Usesha256to 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).chunksis the count of 1 MiB binary frames received. - When at least one but not all files succeed:
successistrue,errors[]lists the failures alongsidefiles[]. When ALL fail:successisfalse.
Desktop Notifications
notify_user-- Send a desktop notification with optional action buttons.title,body,level(info/warning/error/emergency),actions(array of button labels)
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}
- Example:
kicad_install_symbol-- Send a .kicad_sym file and install it as a library. Args include optionalkicadVersion.kicad_install_library-- Install a symbol, footprint, or 3D model library. Each KiCad version has its own sym-lib-table / fp-lib-table — passkicadVersionto target a specific install (otherwise installs into the newest version's tables).kicad_open_board-- Open a .kicad_pcb file. Args:filePath, optionalkicadVersion. Example:adom-desktop kicad_open_board '{"filePath":"C:/foo.kicad_pcb","kicadVersion":"9.0"}'kicad_open_schematic-- Open a .kicad_sch file. Args:filePath, optionalkicadVersion.kicad_open_symbol_editor-- Open the Symbol Editor. OptionalkicadVersion.kicad_open_footprint_editor-- Open the Footprint Editor. OptionalkicadVersion.kicad_open_3d_viewer-- Open the 3D Viewer (works from Footprint Editor or PCB Editor). OptionalkicadVersion.kicad_run_drc-- Run Design Rule Check on a board. OptionalkicadVersion.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 asprojectManager,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. CheckhasModalDialogsto detect blocking save/error prompts.- Example:
adom-desktop kicad_window_info - Returns:
{projectManager: {hwnd, title}, editors: [{hwnd, title}], modalDialogs: [{hwnd, title, ownerHwnd}], hasModalDialogs: bool}
- Example:
kicad_screenshot_all-- Screenshots every KiCad window in one call (editors first, then project manager, then dialogs). Uses same process-based enumeration askicad_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}]}
- Example:
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)
- Args:
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), usex=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}'
- Args:
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:
kicad_window_info→ checkhasModalDialogs- If true:
kicad_screenshot_all→ READ each dialog screenshot - Dismiss with
kicad_send_key {"key":"enter"}(OK) or{"key":"escape"}(cancel) - For specific button clicks: identify position in screenshot →
kicad_click {"hwnd":<dialog_hwnd>, "x":0.5, "y":0.8}
Fusion 360 Tools
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. DiscoversFusion360.exevia 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 callsfusion_dismiss_blocking_dialogsto 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 returnserrorCode: fusion_addin_not_responding(or you see_hintmentioning 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 withfusion_send_key escapetargeted 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 inremaining[].screenshotPath, plus an explicit per-hwnd recovery script in_hintthat you follow mechanically: Read the screenshot with the Read tool, understand what the dialog says, thenfusion_send_key '{"key":"escape","hwnd":<hwnd>}'(or"enter"for OK-only dialogs). Re-callfusion_dismiss_blocking_dialogsto verifymainThreadResponsive=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 toBLOCKING_DIALOG_PATTERNSincli/src/commands.rsso 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/statusHTTP 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 returnsmain_thread_busyimmediately 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}}
- Example:
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.exereturns newest), bare name (searches PATH + Start Menu.lnktargets). Returns{path, source}. Does NOT launch.adom-desktop launch '{"path":"...", "args":[...], "cwd":"...", "detached":true}'-- Same resolution rules asfind_exe, then spawns. Fails in terminal (exit 1) with a clear error if the path doesn't exist. Always prefer this over rawstart.
For Fusion specifically, use fusion_start — it wraps launch plus the full startup-picker / add-in-readiness dance.
fusion_import_step-- Import a STEP/STL/IGES file into Fusion 360fusion_open_lbr-- Open an EAGLE .lbr library filefusion_open_electronics-- Check if the Electronics workspace is activefusion_electron_run-- Execute any EAGLE command via Electron.run. Returns rich state:activeWorkspace,activeDocument,commandType,_hint, andfusionOperations(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— 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:
# 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 documentcommandType— Detected type:view_control,layer_control,edit,design_rule, etc.editorType— For EDIT commands:package,symbol,devicefusionOperations— Array of Fusion operations that fired (diff of internal log)hint— Human-readable description of what happenedrawResult— 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 FITafter opening a file or switching views - Use
DISPLAY NONEthenDISPLAY <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_infoinstead 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_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
package3dhas awip_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
Package3DCreateCmdin Electronics Library Editor, which opens a new Design workspace where you model/import the 3D shape, then save to link it. EXPORT SCRIPTstrips 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) includepackage3dreferences. - Example libraries location:
%LOCALAPPDATA%/Autodesk/webdeploy/production/<hash>/Applications/Electron/LibEagle/examples/libraries/examples/
To open a built-in example library for reference:
# 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 viafusion_electron_runonly 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. ReturnsopenDocumentsarray (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 offusion_walk_cloud_treewhen 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}]}
- Example:
fusion_screenshot_fusion-- Captures the Fusion main window or a specific dialog by HWND. Uses PrintWindow (works without bringing to foreground). Saves WebP toC:/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)
- Args:
fusion_screenshot_all-- Screenshots the main Fusion window and lists all dialog windows with their HWNDs. Usefusion_screenshot_fusion {"hwnd": ...}to capture each dialog.- Example:
adom-desktop fusion_screenshot_all
- Example:
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": falsefor pixel offsets. Uses SendInput for CEF dialog compatibility. Preferfusion_send_keyfor 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)
- Args:
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)
- Args:
Fusion 360 Workflow Guide
Opening Electronics Projects
- Open the
.fprjfile:adom-desktop fusion_open_cloud_file '{"projectName":"Main","fileName":"DRV8411A","fileExtension":"fprj","folderPath":"Molecules/XRP/DRV8411A"}' - Do NOT try to open
.fbrdor.fschdirectly — they fail or show a "Select Electronics Design File" dialog - Enter the board editor:
adom-desktop fusion_show_2d_board - Enter the schematic editor: After step 3, use
adom-desktop fusion_electron_run '{"command":"EDIT .sch"}' - 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 dialogsdialogBlocking—trueif a modal dialog was detected
After receiving the response, you MUST:
- READ each screenshot file using the Read tool to visually inspect what Fusion shows
- 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
- "What to design?" wizard → dismiss with
- 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 inpostOpenScreenshot.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"}'orfusion_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_recoverybefore launching Fusion. This moves recovery files to~/.adom/recovery/fusion/<timestamp>/— preserving them for the user while preventing modal dialogs. - The
fusion_startcommand also auto-relocates recovery files before starting Fusion. - The
fusion_dismiss_recoverycommand 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_runnow checks if the output file was created - Use
fusion_export_bomandfusion_export_cplfor manufacturing data (these use the add-in's XML parser, not EAGLE export) - Use
fusion_export_gerbersfor 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_boardor 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')"
- Digital twin pipeline:
- FBX — Not available in current Fusion builds via the API. The command exists but fails clearly. Use
fusion_export_usdzinstead. - 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:
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
- Args:
Electronics Source File Export (fusion_export_source)
fusion_export_source— Export the active electronics document as.fsch,.fbrd, or.flbrsource 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
- Args:
Full workflow to export both board and schematic from a cloud project:
# 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
.fprjfirst — opening.fbrd/.fschdirectly fails or triggers blocking dialogs - You MUST call
fusion_show_2d_boardbefore exporting.fbrd(the Electronics Design overview won't work) - Always export
.fbrdFIRST (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 WRITEcommand is blocked — it opens a blocking save dialog. Usefusion_export_sourceinstead
- You MUST open the
Note:
WRITEis blocked — it opens a "Version Description" dialog on cloud docs even with a path argument (tested 2026-04-10). Usefusion_save_to_cloudto save to cloud,fusion_export_sourcefor Fusion-format, orfusion_export_eagle_sourcefor plain EAGLE format.
Plain EAGLE Source Export (fusion_export_eagle_source)
fusion_export_eagle_source— Export the active electronics document as plain EAGLE XML.schor.brd.- Args:
outputPath(required — full path ending in.schor.brd) - Internally: exports
.fsch/.fbrdviaDocument.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/.brdextensions instead of.fsch/.fbrd.
# 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"}'- Args:
Dialog Dismissal (fusion_close_window)
fusion_close_window— Close a specific Fusion dialog by sending WM_CLOSE (equivalent to clicking X).- Args:
hwnd(required — fromfusion_window_infoorfusion_dismiss_blocking_dialogsremaining[]) - Works on dialogs that Escape doesn't close — e.g. "Recovered Documents"
- Does NOT force-kill — the dialog can still intercept WM_CLOSE
- Args:
Electronics Import (Round-Trip)
fusion_import_electronics— Import Fusion-native.fsch,.fbrd, or.flbrfiles as new local documents- Uses
Document.newDesignFromLocalunder 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_cloudto persist to Fusion cloud .fschimport works standalone — creates a new schematic project.fbrdimport may fail — boards have external references to schematics/libraries. Error: "New design cannot be created from a local file containing external references".flbrimport works standalone — creates a new library project- For legacy EAGLE files: use
fusion_open_schematic(.sch),fusion_open_board(.brd),fusion_open_lbr(.lbr)
- Uses
Library Round-Trip Workflow
Export and re-import Fusion electronics libraries:
# 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:filesarray with{name, id, versionNumber, fileExtension, dateModified}andsubfoldersarray.fusion_create_cloud_folder-- Create a folder in a cloud project. Args:folderName(required),projectName(optional),parentPath(optional). ReturnsfolderId. 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). ReturnshasRecovery: true/false. Use this BEFORE opening files to avoid the blocking "Open recovery document instead?" dialog. If recovery exists, callfusion_open_cloud_filewithrecovery: "open"(restore unsaved work) orrecovery: "discard"(delete recovery, open cloud version).fusion_open_cloud_file-- Open a cloud file in Fusion by name. If a recovery document exists and norecoveryarg 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 viapull_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 withfusion_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 withfusion_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_statusreturnswalkProgresswith{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_filesfor targeted searches, orfusion_list_cloud_filesfor 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"]}'
- Args:
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.
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:
{"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]):
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 tofusion_walk_cloud_treebut 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}
- Args:
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
# === 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 mattersnextSteps[]— ordered list of what to run next in the manufacturing pipelinehint— tips for interpreting results or recovering from issuesdata— structured metadata (paths, counts, layer info) for programmatic use
Decision guide — which command to run:
- Don't know the layer count? → Run
fusion_detect_layersfirst - Need to check if the board meets fab specs? → Run
fusion_set_design_rulesthen DRC - Ready to generate fab files? → Run
fusion_export_gerbers, thenfusion_export_bom, thenfusion_export_cpl - Need visual review before export? → Run
fusion_export_board_imagewith presetassembly_toporfabrication - Something failed? → Check
data.hintanddata.recoveryStepsin 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). ReturnslayerCount,copperLayers[],method(how it was detected), andnextSteps[]. 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. Returnsdescription(human-readable rule summary),edruFile, andnextSteps[].action:"apply"(default) loads rules into the board;"export"saves current board rules to a file;"show"displays rule capabilities without modifying anythinglayers:"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.edrufile. Args:layers("2"or"4"). Sends the bundled.edrufrom Docker to Windows automatically and loads it via the EAGLEdrc loadcommand. Use this instead offusion_set_design_ruleswhen you just want Adom's standard 2-layer or 4-layer InstaPCB rules.fusion_load_design_rules-- Generic loader for ANY custom.edrufile 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. ReturnszipPath,zipSizeKB,files[](list of gerber files in the ZIP with sizes),layerCount, andnextSteps[].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. ReturnscomponentCount,uniquePartCount, andnextSteps[].outputPath: (default:C:/tmp/adom-bom.csv)grouped:true(default) groups by value+package;falselists 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. ReturnstotalPlacements,topCount,bottomCount, andnextSteps[].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. ReturnsfileSize,preset, andnextSteps[].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 presetmonochrome:truefor black & whitelistPresets: settrueto 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:
# 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 inadom-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.
- Args:
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) ORtitleContains(string, case-insensitive). One required. - Example:
adom-desktop desktop_bring_to_front '{"titleContains": "Fusion"}'
- Args:
desktop_set_window_state-- Change a window's show state without bringing it to the foreground (or both, if combined withdesktop_bring_to_front). Args:hwnd(int) ORtitleContains(string), plusstate(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"}'
- Example:
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
- Example:
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 (browsergetDisplayMedia, 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
idto 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 textid(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). Overridessizepreset when provided.duration(ms) — auto-dismiss timer. Default 4000. 0 = stay until explicitly hidden.action—"hide"to dismiss a caption (withid: hides only that caption; withoutid: 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
idreplace each other instantly (previous destroyed, no fade overlap). Captions with different ids coexist — multiple captions can be visible simultaneously.
- Show:
Desktop Screenshots
Take screenshots of the user's desktop or individual windows. All screenshots use lossless PNG.
Always save screenshots to project-content/screenshots/.
Naming convention: desktop-<descriptive-name>-YYYY-MM-DD-HhMMam/pm.png
Workflow: Call desktop_list_windows first to get HWNDs, then desktop_screenshot_window with the HWND.
desktop_list_windows-- List all visible windows (returns HWND, title, class name, position/size)desktop_screenshot_window-- Capture a specific window by HWNDdesktop_screenshot_screen-- Capture the entire desktop (all monitors)
Browser Automation (Puppeteer)
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_screenshoterrors witherrorCode: "session_disconnected"(the bridge knows about your sessionId but its CDP socket dropped) orerrorCode: "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 — butbrowser_rescanis the explicit recovery primitive. - Returns:
{ok, rescanned, profilesReconnected, profilesUnreachable, reattached, orphansAdopted, liveSessions, disconnectedSessions, _hint}.
- When you need this: any time
browser_open_window-- Open a Chrome window. Args:sessionId(required),profile(required),url(required),freshProfile(optional),strictPermissions(optional, defaultfalse),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.setDownloadBehavioris called withbehavior: 'allow'anddownloadPath: %USERPROFILE%\Downloads(override via thedownloadPatharg). 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 dowindow.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 whatdesktop_watch_filespolls by default — the two ends meet without configuration.
- Scripted downloads enabled.
- Opt-out: pass
strictPermissions: trueto 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 rejectsautomaticDownloadsas an unknown permission name, taking the whole grant down. v1.6.3 fixes by usingBrowser.setPermissionper-permission (per-permission failure tolerance) AND by realizingPage.setDownloadBehavioris the actual mechanism for unblocking scripted downloads, not a permission grant.
- Full-capability default (v1.6.3+): every pup session opens with the capabilities chip-fetcher-style flows actually need:
browser_close_window-- Close a session's Chrome window (closes ALL tabs in that session). Args:sessionIdbrowser_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,urlbrowser_switch_tab-- Make a specific tab the active one (callsbringToFront). Args:sessionId,tabIdbrowser_close_tab-- Close a specific tab. Leaves other tabs + session intact. Args:sessionId,tabIdbrowser_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 viawindow.open()/target="_blank"/ form-submit-with-target=_blank appear here automatically withopener:"popup"andopenerTabIdpointing at the tab that triggered them. Tabs Claude opened viabrowser_open_window/browser_open_tabhaveopener:"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-backedInput.dispatchMouseEvent/Input.dispatchKeyEvent. Events haveisTrusted: true, so they pass framework click-handler gates (React/Vue/Svelte) and most vendor anti-automation checks thatbrowser_eval-sidedispatchEvent(new MouseEvent(...))silently fails on. Args:sessionId?,tabId?,type(click|move|type|key), plus per-type fields:click: eitherselector(uses bounding rect — preferred when DOM is stable) OR{x, y}viewport coords. Optionalbutton(left/right/middle),clickCount(1 or 2 for double-click),delay,firstMatch(default false; see below).move:{x, y}plus optionalstepsfor human-like trajectory.type:textto type; pass optionalselectorto focus that element first; optionaldelaybetween 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
selectormatches multiple elements (a class likebutton.wg-button--primaryoften does — cookie X buttons + duplicate hidden copies + the actual submit), the bridge picks intelligently:- 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. - Otherwise filter to visible elements with non-empty text and tiebreak by largest visual area.
- Fall back to visible-only, then document-order first as last resort.
- If a plausible modal/dialog is open —
- 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), andpickStrategy(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 — ifclickedTextdoesn't look right, inspectmodalRootto see whether modal detection latched onto the intended overlay or got fooled by a sibling, then refine the selector OR passfirstMatch:trueto 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 withbrowser_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-pagefetch(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). saveTowrites 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. ReturnssavedTowith the canonical absolute path (verified to exist on disk) plussavedToFilesystem:"container".desktopSaveTowrites on the WINDOWS desktop's filesystem (the bridge writes directly viafs.writeFileSync). Pass an absolute Windows path (e.g.C:\\Users\\you\\Downloads\\foo.pdf). ReturnsdesktopSavedTowith 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
bodyBase64in 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'ssavedTois the actual filesystem path the file was written to. v1.4.8 had a bug where the bridge would respondsavedTo:"/tmp/foo.pdf"but the file existed nowhere; v1.4.9 fixes this —savedToalways reflects a real on-disk path. - Recipe — popup PDF capture (the right pattern):
# 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}
- Args:
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:- Live session → return immediately.
- Session is
_lostBrowser(CDP socket dropped after sleep/wake / network blip / puppeteer hiccup): tryattemptReconnectProfile()which checks (a) the on-disk session file's recordedcdpPort, (b) the profile dir'sDevToolsActivePort, and (c) scans running Chrome processes for a--remote-debugging-port=Nmatching 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. - Reconnect failed: kill any orphan Chrome processes for this profile, clean the profile lock files (
lockfileon Windows,SingletonLock/SingletonSocket/SingletonCookieon POSIX, plus staleDevToolsActivePort), then relaunch vialaunchSessionwith the last-known URL from the session file. The sessionId is preserved — the caller gets a transparent recovery. - Total failure (no profile name, no last-known URL, can't launch) → only THEN does the structured
session_not_found/session_disconnectederror fire.
When recovery happens, the response includes
_recovered: trueand_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: checkliveSessionsfor the right name, orbrowser_open_windowif 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: callbrowser_rescanto force a manual recovery pass, orbrowser_open_windowwith 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-diris 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 atabIdthat no longer exists:{ "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+closedMsAgoare 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 canbrowser_fetch_urldirectly againstlastKnownUrlinstead of having to re-trigger the spawning click.- Important regression note: v1.4.9 documented this error but the CLI was silently stripping
tabIdfrombrowser_navigate/browser_screenshot/browser_eval/browser_errors/browser_reloadcalls 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 forwardtabIdso the structured error actually fires. If you're still seeing silent fallback, your CLI is < v1.4.10 (adom-desktop --versionto 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 countsbrowser_close-- Close ALL sessionsbrowser_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)
- Args:
credential_list-- List stored entries.- Returns:
{credentials: [{host, username, addedAt, updatedAt}]} - Passwords are never returned by this verb.
- Returns:
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 atcredential_settime, exactly)
- Args:
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:
# 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:
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:sessionIdbrowser_focus_window-- Tab-only. Calls Puppeteer'spage.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 usebrowser_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=trueand Chrome throttlesrequestAnimationFrameto ~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_eventphantom-key trick). Args:sessionId. Returns:{sessionId, hwnd, title, raised, error?}.# 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
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
.webmbecause 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_windowfirst, 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 witherrorCode: desktop_record_needs_confirmationand a hint pointing atbrowser_record_start(for pup tab) or Hydrogen (for casual screen recording).
# 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.webmfiles 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). 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:
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
fpsActualreports ≤1 even withbrowser_raise_os_windowfirst, the session's Chrome was launched with an old flag set. Fix: close + reopen the session (browser_close '{"sessionId":"<id>"}'thenbrowser_open_window) so it spawns a fresh Chrome with the current flags. ThekicadVersionUsed-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.
# 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 liveframeCount,fpsTarget,fpsActual,durationMs,sizeBytesApproxbrowser_record_list— completed.webmfiles 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_stopfirst)
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. maxDurationMsdefaults 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 validformat=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:
# 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:
# 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_filesplussince(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 assince.
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:
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 viadesktop_watch_filesuntil at least one match appears, then re-lists the directory and pulls everything matching (so a.crdownload+ final.zipwritten 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_filestreaming pipeline — sha256-verified, ~1MB chunks, resumes the same waypull_filedoes.
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;/tmpon 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 <= sinceare filtered out (strict>).
Shell — shell_execute (escape hatch only)
shell_execute-- Run a shell command on the desktop. Returns{pending: true, requestId}when the approval dialog shows. Poll withget_deferred_resultfor 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
After connecting, always run adom-desktop status to check what's installed. The desktop.apps object tells you exactly what the user has:
adom-desktop status
# Look at the desktop.apps field in the response
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:
# 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 themedia@one → typicallyDefault. - 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:
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 nodeon Windows,which nodeelsewhere) - 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:
KiCad not installed:
v1.6.0+ — try the unattended winget install FIRST before asking the user to download manually:
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).
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>\InstallationPathand theWOW6432Nodemirror HKCUmirrors 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.
Fusion 360 not installed:
Fusion 360 isn't installed on your desktop. Would you like to install it?
Download from: https://www.autodesk.com/products/fusion-360
It's free for personal/hobby use (requires an Autodesk account).
Let me know when the install is done and I'll verify the connection.
After install, have them restart Adom Desktop, then run adom-desktop status to verify.
Handling "not running" errors
When errorCode: "fusion_not_running", launch it programmatically with the first-class startup command:
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, ...}
KiCad doesn't need to be running for most commands (the bridge launches it on demand).
Handling "add-in not installed" errors
When Fusion is running but addinInstalled: false or addinConnected: false:
The AdomBridge add-in needs to be installed in Fusion 360. I can install it for you — this lets me control Fusion remotely.
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 withwatch" above) which streams live progress automatically. If another session started it, pollfusion_addin_statusevery 2–5s to check progress. - Use commands that don't need the main thread while waiting:
fusion_addin_status— check busy state and walk progressfusion_window_info— get window HWND, title, dialogsfusion_screenshot_fusion— capture what Fusion looks likefusion_click_fusion— click in the Fusion windowfusion_send_key— send keyboard inputfusion_close_window— close a specific dialog by HWND
The response includes progress info:
{
"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:
- Calls
fusion_dismiss_blocking_dialogsto clear any modal - If successful, retries the original command
- 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
Use status to see connected clients. A healthy connection shows one client from the user's hostname with a recent lastPong timestamp.
Stale connections causing timeouts
Use kick_all to reset -- active Adom Desktop apps reconnect within seconds.
No desktop client connected
- Confirm the Adom Desktop app is running on the user's PC
- Confirm it's pointed at the correct WebSocket URL
- Check if port 8765 is exposed and reachable
Relay not running
curl -sf http://127.0.0.1:8766/health
# If fails: adom-desktop serve &
Shell commands on Windows
shell_execute runs in cmd.exe. Prefer writing scripts via send_files then executing them. Use the full Python path.
Building from Source
cd cli && cargo build --release
# Binary at: cli/target/release/adom-desktop