app
Adom Bridge (macOS)
Public Made by Adomby adom
Adom Bridge is a small operating system for AI on your Mac: it gives an AI real hands on your native desktop, including your signed-in browser, KiCad, Fusion 360, the shell, files, windows and screenshots. It provisions its own Node and Python so bridges run without touching your setup, and it talks straight to your machine. It ships with Adom Hydrogen and installs standalone too, so if you drive your Mac with Claude Desktop or another AI, this is what gives that AI hands.
← Commit history
Publish 1.9.207
19 files changed
+3410−18
SKILL.md+604package.json+2−2page.json+1−1skills/adom-bridge-bridge-ownership/SKILL.md+2−2skills/adom-bridge-bridge-sdk/SKILL.md+10−3skills/adom-bridge-bridges-guide/SKILL.md+30skills/adom-bridge-cli-guide/SKILL.md+1−1skills/adom-bridge-discovery/SKILL.md+5−6skills/adom-bridge-installer/._SKILL.mdskills/adom-bridge-installer/SKILL.md+452skills/adom-bridge-macos/._SKILL.mdskills/adom-bridge-macos/SKILL.md+2031skills/adom-bridge-recording/._SKILL.mdskills/adom-bridge-recording/SKILL.md+2−2skills/adom-bridge-window-capture/SKILL.md+1−1skills/adom-bridge-window-identity/._SKILL.mdskills/adom-bridge-window-identity/SKILL.md+106skills/adom-bridge-windows-host/._SKILL.mdskills/adom-bridge-windows-host/SKILL.md+163SKILL.md+604@@ -534,6 +534,599 @@ Two generic CLI commands exist for launching any executable or app safely - they - `adom-bridge launch '{"path":"...", "args":[...], "cwd":"...", "detached":true}'` -- Same resolution rules as `find_exe`, then spawns. Fails in terminal (exit 1) with a clear error if the path doesn't exist. **Always prefer this over a raw shell `open`.** 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 360+- `fusion_open_lbr` -- Open an EAGLE .lbr library file+- `fusion_open_electronics` -- Check if the Electronics workspace is active+- `fusion_electron_run` -- Execute any EAGLE command via Electron.run. Returns rich state: `activeWorkspace`, `activeDocument`, `commandType`, `_hint`, and `fusionOperations` (diff of Fusion's internal operation log showing what actually fired). **Avoid blocking commands** - see list below. **Full EAGLE command reference:** [`skills/eagle-commands.md`](./eagle-commands.md) - popular/safe commands split from blocking/modal ones, with layer reference and chaining syntax.+- `fusion_execute_text_command` -- Low-level app.executeTextCommand() access. Returns the command result plus workspace context.+- `fusion_board_info` -- Get structured board data from the open PCB layout. Returns: component placements (name, package, x, y, rotation), net names, copper traces, layer setup, board thickness, DRC violations. Much richer than a screenshot - gives exact coordinates and connectivity. Requires a .brd board open in PCB Editor.++#### `fusion_electron_run` - EAGLE Command Execution++Executes EAGLE commands inside Fusion 360's Electronics workspace. Works in **Schematic Editor**, **PCB Editor (Board Layout)**, and **Electronics Library** contexts.++**How it works:** Sends the command string via Fusion's `Electron.run` text command. EAGLE's `Electron.run` is fire-and-forget - it never returns output or throws on invalid commands. To compensate, the handler snapshots Fusion's internal operation log (`Diagnostics.RecentOperations`) before and after execution, returning a diff showing what actually fired.++**Usage:**+```bash+# Basic command+adom-bridge fusion_electron_run '{"command": "WINDOW FIT"}'++# Multiple commands in sequence (use semicolons)+adom-bridge fusion_electron_run '{"command": "DISPLAY NONE; DISPLAY 1 16 17 18 20 21"}'++# Navigate in library editor+adom-bridge fusion_electron_run '{"command": "EDIT SOIC8.pac"}'+adom-bridge fusion_electron_run '{"command": "EDIT RESISTOR.sym"}'+adom-bridge fusion_electron_run '{"command": "EDIT MYDEVICE.dev"}'+```++**Response fields:**+- `activeWorkspace` - Current workspace (Schematic Editor, PCB Editor, Electronics Library)+- `activeDocument` - Name of the open document+- `commandType` - Detected type: `view_control`, `layer_control`, `edit`, `design_rule`, etc.+- `editorType` - For EDIT commands: `package`, `symbol`, `device`+- `fusionOperations` - Array of Fusion operations that fired (diff of internal log)+- `hint` - Human-readable description of what happened+- `rawResult` - Raw return from Electron.run (usually empty)++**EAGLE Command Reference - Safe for automation:**++| Command | Context | Description |+|---------|---------|-------------|+| **View / Navigation** | | |+| `WINDOW FIT` | Any | Zoom to fit all content |+| `WINDOW (x1 y1 x2 y2)` | Any | Zoom to specific area (coordinates in current units) |+| `DISPLAY ALL` | Any | Show all layers |+| `DISPLAY NONE` | Any | Hide all layers |+| `DISPLAY 1 16 17 18 20 21` | Board | Show specific layers by number |+| **Grid** | | |+| `GRID MM 0.1` | Any | Set grid to 0.1mm |+| `GRID MIL 25` | Any | Set grid to 25mil |+| `GRID INCH 0.05` | Any | Set grid to 0.05 inch |+| **Board Layout** | | |+| `RATSNEST` | Board | Recalculate airwires (unrouted connections) |+| `RIPUP` | Board | Remove all routed traces |+| `RIPUP *` | Board | Remove all traces (same as RIPUP with no selection) |+| `ROUTE` | Board | Start auto-router |+| `DRC` | Board | Run design rule check |+| `BOARD` | Schematic | Switch to paired board layout |+| `SCHEMATIC` | Board | Switch to paired schematic |+| **Schematic** | | |+| `VALUE value` | Schematic | Set component value |+| `NAME name` | Any | Rename selected element |+| `SMASH` | Any | Detach name/value labels from components |+| **Library Editor** | | |+| `EDIT name.pac` | Library | Open a package (footprint) for editing |+| `EDIT name.sym` | Library | Open a symbol for editing |+| `EDIT name.dev` | Library | Open a deviceset for editing |+| `EXPORT SCRIPT 'path.scr'` | Library | Export entire library as EAGLE script |+| **Scripting / Settings** | | |+| `SET CONFIRM YES` | Any | Suppress confirmation dialogs |+| `SET CONFIRM OFF` | Any | Re-enable confirmation dialogs |+| `SCRIPT 'path.scr'` | Any | Run batch commands from a .scr script file |++**EAGLE Layer Numbers (commonly used):**++| Layer | Name | What it shows |+|-------|------|--------------|+| 1 | Top | Top copper |+| 16 | Bottom | Bottom copper |+| 17 | Pads | Through-hole pads |+| 18 | Vias | Via holes |+| 19 | Unrouted | Airwires (ratsnest) |+| 20 | Dimension | Board outline (required for 3D) |+| 21 | tPlace | Top silkscreen |+| 22 | bPlace | Bottom silkscreen |+| 25 | tNames | Top component names |+| 27 | tValues | Top component values |+| 29 | tStop | Top solder mask |+| 31 | tCream | Top stencil/paste |+| 51 | tDocu | Top documentation |++**Blocking commands - AVOID from automation:**++| Command | Why it blocks |+|---------|---------------|+| `WRITE` | Opens Save As dialog - use `fusion_save_lbr` or `fusion_close_document` instead |+| `ADD` | Opens component picker dialog |+| `SHOW name` | Opens interactive highlight mode |+| `SET` (no params) | Opens settings dialog |+| `GRID` (no params) | Opens grid settings dialog |+| `EDIT new.sym` | Opens "Create new?" confirmation if symbol doesn't exist |+| `CHANGE` | Opens interactive change mode |+| `MOVE` | Opens interactive move mode |++**Tips:**+- Always run `WINDOW FIT` after opening a file or switching views+- Use `DISPLAY NONE` then `DISPLAY <layers>` to show only specific layers+- Combine commands with `;` - e.g., `SET CONFIRM YES; RIPUP *; RATSNEST`+- For board screenshots: `DISPLAY NONE; DISPLAY 1 16 17 18 20 21; WINDOW FIT`+- Use `fusion_board_info` instead of EAGLE commands when you need structured data+- `fusion_export_lbr` -- Export the open Electronics library as an EAGLE .scr script (note: does NOT include 3D package references - those are cloud-linked only)+- `fusion_save_lbr` -- Save the open Electronics library as a .flbr file++#### EAGLE Libraries with 3D Packages++Fusion 360's .lbr format supports `package3d` elements that link footprints to 3D models. Key facts:++- **3D models are cloud-hosted** - each `package3d` has a `wip_urn` (e.g., `urn:adsk.wipprod:fs.file:vf.xxxxx`) pointing to a Fusion cloud document. You cannot embed STEP files directly in .lbr XML.+- **Creating 3D packages requires the Fusion UI** - use `Package3DCreateCmd` in Electronics Library Editor, which opens a new Design workspace where you model/import the 3D shape, then save to link it.+- **`EXPORT SCRIPT` strips 3D references** - the .scr export only contains 2D data (symbols, footprints, devicesets). To preserve 3D links, keep the .lbr XML format.+- **Fusion's built-in examples** have 3D packages - 34 of 37 libraries in the EAGLE examples directory (e.g., `Connector_USB.lbr`, `Resistor.lbr`, `Capacitor.lbr`) include `package3d` references.+- **Example libraries location** (macOS): under the Fusion install's `.../Applications/Electron/LibEagle/examples/libraries/examples/` (find the install with `find_exe` or ask the bridge; the webdeploy hash changes per build)++To open a built-in example library for reference:+```bash+# Find the examples directory first+adom-bridge fusion_execute_text_command '{"command": "Python.RunScript /tmp/find_eagle_libs.py"}'+# Then open one+adom-bridge fusion_open_lbr '{"filePath": "<path>/Connector_USB.lbr"}'+```+- `fusion_open_schematic` -- Open a .sch schematic in Fusion's Schematic Editor. Args: `filePath`.+- `fusion_open_board` -- Open a .brd board layout in Fusion's Board Layout editor. Args: `filePath`.+- `fusion_show_3d_board` -- Switch to 3D PCB board view (must have a .brd open). Board MUST have an outline on layer 20 (Dimension) or 3D generation fails. Auto-zooms to fit after switching.+- `fusion_show_2d_board` -- Switch back to 2D board layout from 3D PCB view. EAGLE commands via `fusion_electron_run` only work in 2D.+- `fusion_close_document` -- Close a document without save dialog. Args: `name` (optional, defaults to active doc), `save` (optional, default false). Essential for automation - avoids modal save dialog that blocks Fusion.+- `fusion_document_info` -- List ALL open documents/tabs with name, type, and active status, plus detailed cloud info for the active document. Returns `openDocuments` array (every tab) and active doc details (cloud project, folder, file ID, version, save status). Lightweight - uses only in-memory data, no cloud API calls. Use this instead of `fusion_walk_cloud_tree` when you just need to know what's open.+- `fusion_activate_document` -- Switch to a specific open document tab. Args: `name` (substring match, case-insensitive), `documentType` ("Electronics", "PCB", "FusionDesign", "Drawing"). Essential for automation - switch between schematic, board, and library tabs without user interaction. If no match found, returns the list of open documents so you can refine.+- `fusion_close` -- Close Fusion 360. **Always call this when done with Fusion 360 commands to clean up.**+- `fusion_dismiss_recovery` -- Dismiss recovery document dialogs (both "Recovered Documents" list and "Open recovery document instead?" prompts). Also relocates recovery files to `~/.adom/recovery/fusion/` for safekeeping.+- `fusion_relocate_recovery` -- Proactively move Fusion crash recovery files to `~/.adom/recovery/fusion/<timestamp>/` without dismissing any dialogs. Call this **before** launching Fusion to prevent recovery dialogs from appearing. Files are preserved (not deleted) so the user can manually restore them if needed.+- `fusion_close_all_documents` -- Close all open documents. Args: `saveChanges` (default: false). Use before force-killing Fusion to prevent recovery files.++#### Fusion 360 UI Interaction++Commands for interacting with Fusion 360's UI - detecting dialogs, taking screenshots, clicking, and sending keyboard input. Essential for handling blocking dialogs and automating CEF-based UI elements.++- `fusion_window_info` -- Returns the Fusion main window HWND, title, rect, and a list of all Qt dialog windows (recovery dialogs, wizards, file pickers). Essential for detecting blocking dialogs before/after operations.+ - Example: `adom-bridge fusion_window_info`+ - Returns: `{hwnd, title, rect, dialogs: [{hwnd, title, className, rect}]}`++- `fusion_screenshot_fusion` -- Captures the Fusion main window or a specific dialog by window id. Background capture (works without bringing to foreground). Saves WebP to `/tmp/adom-bridge-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-bridge fusion_screenshot_fusion` (main window)+ - Example: `adom-bridge fusion_screenshot_fusion '{"hwnd": 12345}'` (specific dialog)++- `fusion_screenshot_all` -- Screenshots the main Fusion window and lists all dialog windows with their HWNDs. Use `fusion_screenshot_fusion {"hwnd": ...}` to capture each dialog.+ - Example: `adom-bridge fusion_screenshot_all`++- `fusion_click_fusion` -- Click at coordinates within the Fusion window or a specific dialog. x/y are relative (0.0-1.0) by default. Set `"relative": false` for pixel offsets. Uses synthetic OS input for CEF dialog compatibility. **Prefer `fusion_send_key` for dialogs** - enter/escape/tab covers most cases.+ - Args: `x` (required), `y` (required), `relative` (optional, default true), `hwnd` (optional - target a specific dialog HWND instead of main window)+ - Example: `adom-bridge fusion_click_fusion '{"x": 0.5, "y": 0.7}'` (main window)+ - Example: `adom-bridge fusion_click_fusion '{"hwnd": 12345, "x": 0.75, "y": 0.85}'` (dialog button)++- `fusion_send_key` -- Send keyboard input to Fusion or a specific dialog via synthetic OS input. **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-bridge fusion_send_key '{"key": "escape"}'` (dismiss CEF dialog)+ - Example: `adom-bridge fusion_send_key '{"key": "enter", "hwnd": 12345}'` (confirm dialog)++#### Fusion 360 Workflow Guide++##### Opening Electronics Projects+1. Open the `.fprj` file: `adom-bridge fusion_open_cloud_file '{"projectName":"Main","fileName":"DRV8411A","fileExtension":"fprj","folderPath":"Molecules/XRP/DRV8411A"}'`+2. Do NOT try to open `.fbrd` or `.fsch` directly - they fail or show a "Select Electronics Design File" dialog+3. Enter the board editor: `adom-bridge fusion_show_2d_board`+4. Enter the schematic editor: After step 3, use `adom-bridge fusion_electron_run '{"command":"EDIT .sch"}'`+5. Switch back to board: `adom-bridge fusion_electron_run '{"command":"EDIT .brd"}'`++##### Auto-Screenshot on Open Commands++**Open commands automatically screenshot Fusion and return the images in the response.** The following commands include `postOpenScreenshot` in their `data` field:+- `fusion_open_cloud_file`, `fusion_open_schematic`, `fusion_open_board`, `fusion_show_3d_board`, `fusion_show_2d_board`++The response `data.postOpenScreenshot` contains:+- `screenshots[]` - array of `{type, savedTo, sizeKB, title?, hwnd?}`. Type is `"main_window"` or `"dialog"`.+- `message` - human-readable instruction to READ each screenshot and check for blocking dialogs+- `dialogBlocking` - `true` if a modal dialog was detected++**After receiving the response, you MUST:**+1. **READ each screenshot file** using the Read tool to visually inspect what Fusion shows+2. Look for blocking dialogs in the screenshots:+ - "What to design?" wizard → dismiss with `fusion_send_key {"key": "escape"}`+ - "PCB out of date" banner → click Update or X+ - "Recovered Documents" → `fusion_dismiss_recovery`+ - "Save changes?" → `fusion_send_key {"key": "escape"}`+ - Any other modal → identify and dismiss+3. **Screenshot again** after dismissing to confirm it's clear++Screenshots are saved as WebP (lossless, ~20-40KB each, downscaled to ≤1568px) for token efficiency. Both the main Fusion window AND all Qt dialog windows are captured separately.++**Why this matters**: Fusion shows Qt dialogs and CEF overlays that are invisible to the API. The `success: true` response from an open command does NOT mean the UI is ready - a dialog may be blocking all further operations. The API cannot detect these. Only a screenshot can.++##### Detecting and Handling Blocking Dialogs+- The auto-screenshot captures dialog windows separately (look for `type: "dialog"` entries in `postOpenScreenshot.screenshots[]`)+- Common blocking dialogs: "What do you want to design?", "Recovered Documents", "Open recovery document instead?", "Select Electronics Design File"+- Dismiss recovery dialogs: `adom-bridge fusion_dismiss_recovery`+- Dismiss CEF dialogs (inside main window): `adom-bridge fusion_send_key '{"key": "escape"}'` or `fusion_click_fusion`+- Dismiss "What do you want to design?" wizard: `adom-bridge fusion_send_key '{"key": "escape"}'`+- For manual screenshot of specific dialogs: `adom-bridge desktop_screenshot_window '{"hwnd": <DIALOG_HWND>}'`++##### Preventing Recovery Documents+- Recovery files are at: `~/Library/Application Support/Autodesk/Autodesk Fusion 360/<USER_ID>/CrashRecovery/`+- Before force-killing Fusion, close all documents: `adom-bridge fusion_close_all_documents '{"saveChanges": false}'`+- **Best practice**: Call `adom-bridge fusion_relocate_recovery` before launching Fusion. This moves recovery files to `~/.adom/recovery/fusion/<timestamp>/` - preserving them for the user while preventing modal dialogs.+- The `fusion_start` command also auto-relocates recovery files before starting Fusion.+- The `fusion_dismiss_recovery` command handles both "Recovered Documents" list and "Open recovery document instead?" prompts, and also relocates files.++##### EAGLE Export Limitations+- **Supported**: `EXPORT IMAGE`, `EXPORT NETLIST`, `EXPORT PARTLIST`+- **NOT supported** (fail silently): `EXPORT DXF`, `EXPORT SVG`, `EXPORT DRILL`+- Always verify export output: the updated `fusion_electron_run` now checks if the output file was created+- Use `fusion_export_bom` and `fusion_export_cpl` for manufacturing data (these use the add-in's XML parser, not EAGLE export)+- Use `fusion_export_gerbers` for Gerber files - produces a ZIP with all gerber layers (GTL, GBL, GTS, GBS, GTP, GBP, GTO, GBO, GKO, XLN). Auto-detects 2-layer vs 4-layer boards.++##### 3D Model Exports+- From the 3D view (activate the .f3d document): `fusion_export_step`, `fusion_export_stl`, `fusion_export_3mf`, `fusion_export_f3d`, `fusion_export_usdz`, `fusion_export_iges`, `fusion_export_sat`+- Switch to 3D view: `fusion_show_3d_board` or activate the .f3d document+- **STEP** - Industry-standard CAD interchange (SolidWorks, CATIA, Creo). Highest geometric fidelity.+- **IGES** - Legacy CAD interchange. Use STEP for modern workflows.+- **SAT** - ACIS solid model format. Used by SolidWorks, SpaceClaim.+- **STL** - Mesh format for 3D printing and visualization. Options: refinement "low"/"medium"/"high".+- **3MF** - Modern 3D printing with color/material support and multi-body.+- **F3D** - Native Fusion 360 archive. Preserves parametric features, sketches, timeline, component refs. Best for archival.+- **USDZ** - **Best for digital twins and GLB conversion.** Preserves full component hierarchy (Board, copper layers, soldermask, Packages), PBR materials, and named nodes. Each component becomes a toggleable node in GLB viewers. Also viewable directly on iOS/macOS (Apple Quick Look / AR).+ - Digital twin pipeline: `fusion_export_usdz` → pull_file → `blender --background --python-expr "import bpy; bpy.ops.wm.usd_import(filepath='board.usdz'); bpy.ops.export_scene.gltf(filepath='board.glb')"`+- **FBX** - Not available in current Fusion builds via the API. The command exists but fails clearly. Use `fusion_export_usdz` instead.+- **DXF/DWG/OBJ/SKP** - Not available via the Fusion API (dialog-only). Commands exist but return clear errors with alternatives.++##### 3D Viewport Captures+- `fusion_take_screenshot` - Capture the Fusion viewport at any resolution without opening a dialog. Uses Fusion's render API (`saveAsImageFile`).+ - Args: `outputPath` (required), `width` (default 1920), `height` (default 1080), `orientation` (optional)+ - Orientations: `home`, `front`, `back`, `top`, `bottom`, `left`, `right`+ - Example: Capture all 6 standard views for use as product images/icons:+ ```bash+ for orient in home front back top bottom left right; do+ adom-bridge fusion_take_screenshot "{\"outputPath\": \"/tmp/3d-${orient}.png\", \"width\": 1920, \"height\": 1080, \"orientation\": \"${orient}\"}"+ done+ ```++##### Electronics Source File Export (`fusion_export_source`)+- `fusion_export_source` - Export the active electronics document as `.fsch`, `.fbrd`, or `.flbr` source file.+ - Args: `outputPath` (required - full path with extension)+ - The extension determines the format: `.fsch` (schematic), `.fbrd` (board), `.flbr` (library)+ - Validates extension, creates output directory, verifies file was created, returns file size++- **Full workflow to export both board and schematic from a cloud project:**+ ```bash+ # Step 1: Open the .fprj (NOT .fbrd/.fsch directly - those fail)+ adom-bridge 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-bridge fusion_screenshot_fusion++ # Step 3: Enter board view (MUST be Board Layout workspace, not 3D)+ adom-bridge fusion_show_2d_board++ # Step 4: Export .fbrd (board first - it's already in board view)+ adom-bridge fusion_export_source '{"outputPath": "/tmp/exports/MyDesign.fbrd"}'++ # Step 5: Switch to schematic (EDIT .s1 = first schematic sheet)+ adom-bridge fusion_electron_run '{"command": "EDIT .s1"}'++ # Step 6: Export .fsch+ adom-bridge fusion_export_source '{"outputPath": "/tmp/exports/MyDesign.fsch"}'++ # Step 7: Pull files to Docker+ adom-bridge pull_file '{"filePaths":["/tmp/exports/MyDesign.fbrd","/tmp/exports/MyDesign.fsch"], "saveTo":"/tmp/exports"}'+ ```++- **Critical gotchas:**+ - You MUST open the `.fprj` first - opening `.fbrd`/`.fsch` directly fails or triggers blocking dialogs+ - You MUST call `fusion_show_2d_board` before exporting `.fbrd` (the Electronics Design overview won't work)+ - Always export `.fbrd` FIRST (from board view), then switch to schematic for `.fsch`+ - If export fails with "file not found", the wrong workspace is active - screenshot to verify+ - After `fusion_open_cloud_file`, always screenshot to check for "Select Electronics Design File" dialog+ - `WRITE` command is blocked - it opens a blocking save dialog. Use `fusion_export_source` instead++- Note: `WRITE` is blocked - it opens a "Version Description" dialog on cloud docs even with a path argument (tested 2026-04-10). Use `fusion_save_to_cloud` to save to cloud, `fusion_export_source` for Fusion-format, or `fusion_export_eagle_source` for plain EAGLE format.++##### Plain EAGLE Source Export (`fusion_export_eagle_source`)+- `fusion_export_eagle_source` - Export the active electronics document as plain EAGLE XML `.sch` or `.brd`.+ - Args: `outputPath` (required - full path ending in `.sch` or `.brd`)+ - Internally: exports `.fsch`/`.fbrd` via `Document.CopyToDesktop`, then extracts the EAGLE XML from the ZIP container, cleans up the temp file.+ - The output is valid EAGLE XML (`<?xml><eagle version="9.7.0">...`) parseable by standalone EAGLE, KiCad import, or any XML tool.+ - Same workflow/prerequisites as `fusion_export_source` - just use `.sch`/`.brd` extensions instead of `.fsch`/`.fbrd`.++ ```bash+ # Board (.brd) - must be in PCB Editor / Board Layout+ adom-bridge fusion_show_2d_board+ adom-bridge fusion_export_eagle_source '{"outputPath": "/tmp/exports/MyDesign.brd"}'++ # Schematic (.sch) - must be in Schematic Editor+ adom-bridge fusion_electron_run '{"command": "EDIT .s1"}'+ adom-bridge fusion_export_eagle_source '{"outputPath": "/tmp/exports/MyDesign.sch"}'+ ```++##### Dialog Dismissal (`fusion_close_window`)+- `fusion_close_window` - Close a specific Fusion dialog by sending WM_CLOSE (equivalent to clicking X).+ - Args: `hwnd` (required - from `fusion_window_info` or `fusion_dismiss_blocking_dialogs` remaining[])+ - Works on dialogs that Escape doesn't close - e.g. "Recovered Documents"+ - Does NOT force-kill - the dialog can still intercept WM_CLOSE++##### Electronics Import (Round-Trip)+- `fusion_import_electronics` - Import Fusion-native `.fsch`, `.fbrd`, or `.flbr` files as new local documents+ - Uses `Document.newDesignFromLocal` under the hood+ - Auto-screenshots after import (catches blocking dialogs)+ - Args: `filePath` (required path on the Mac to .fsch, .fbrd, or .flbr)+ - After import, use `fusion_save_to_cloud` to persist to Fusion cloud+ - **`.fsch` import works standalone** - creates a new schematic project+ - **`.fbrd` import may fail** - boards have external references to schematics/libraries. Error: "New design cannot be created from a local file containing external references"+ - **`.flbr` import works standalone** - creates a new library project+ - For legacy EAGLE files: use `fusion_open_schematic` (.sch), `fusion_open_board` (.brd), `fusion_open_lbr` (.lbr)++##### Library Round-Trip Workflow+Export and re-import Fusion electronics libraries:+```bash+# 1. Open library from cloud+adom-bridge 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-bridge fusion_save_lbr '{"outputPath":"/tmp/library.flbr"}'+adom-bridge fusion_export_lbr '{"outputPath":"/tmp/library.scr"}'+adom-bridge fusion_close_document++# 3. Re-import the .flbr+adom-bridge fusion_import_electronics '{"filePath":"/tmp/library.flbr"}'++# 4. Verify symbols survived round-trip+adom-bridge fusion_export_lbr '{"outputPath":"/tmp/library-verify.scr"}'++# 5. Save to cloud with new name+adom-bridge fusion_save_to_cloud '{"name":"Library-copy"}'+```++##### Demo Projects+171+ electronics molecules are exported in the `adom-bridge-demo` repo (separate from adom-bridge).+Three reference boards with full export formats:++| Board | Cloud Path | Layers | Components | Files |+|-------|-----------|--------|------------|-------|+| DRV8411A | Main / Molecules / XRP / DRV8411A | 4 | 29 | 27 |+| DRV8323SR | Main / Molecules / Experiments / MotorControl / DRV8323SR | 2 | 43 | 27 |+| VL53L8BreakoutMolecule | Main / Molecules / XRP / TimeOfFlightVL53L8 / VL53L8BreakoutMolecule | 2 | 30 | 27 |++Each folder contains: .fsch, .fbrd, bom.csv, cpl.csv, gerbers.zip, 6 board images, 7 3D renders (home/front/back/top/bottom/left/right), STEP, IGES, SAT, STL, 3MF, F3D, USDZ, board screenshot, 3D board screenshot.++#### Cloud Document Management++Manage Fusion 360 cloud documents (hub projects, files, versions). Required for 3D package workflows since EAGLE library 3D models are stored as cloud documents.++- `fusion_save_to_cloud` -- Save the active Fusion document to the cloud. Args: `name` (required), `projectName` (optional, defaults to active project), `folderPath` (optional), `description` (optional). Returns: `fileId`, `versionNumber`, `wipUrn` (if cloud-hosted).+- `fusion_list_cloud_projects` -- List all cloud projects in the user's hub. Returns array of `{name, id}` per hub.+- `fusion_list_cloud_files` -- List files in a cloud project/folder. Args: `projectName` (optional), `folderPath` (optional). Returns: `files` array with `{name, id, versionNumber, fileExtension, dateModified}` and `subfolders` array.+- `fusion_create_cloud_folder` -- Create a folder in a cloud project. Args: `folderName` (required), `projectName` (optional), `parentPath` (optional). Returns `folderId`. Idempotent - returns existing folder if it already exists.+- `fusion_check_recovery` -- Check if a cloud file has a recovery document from a previous crash. Args: `fileName` (required), `projectName` (optional), `folderPath` (optional). Returns `hasRecovery: true/false`. Use this BEFORE opening files to avoid the blocking "Open recovery document instead?" dialog. If recovery exists, call `fusion_open_cloud_file` with `recovery: "open"` (restore unsaved work) or `recovery: "discard"` (delete recovery, open cloud version).+- `fusion_open_cloud_file` -- Open a cloud file in Fusion by name. **If a recovery document exists and no `recovery` arg is given, the command STOPS and reports the recovery instead of opening** - you must decide whether to preserve or discard unsaved work. Args: `fileName` (required), `projectName` (optional), `folderPath` (optional), `recovery` ("open" = restore unsaved work, "discard" = delete recovery and open cloud version).+- `fusion_export_cloud_file` -- Export the active Fusion document to a local file for transfer back to Docker. Args: `outputPath` (required), `format` (optional, default "step"). The exported file can then be pulled back to Docker via `pull_file`.+- `fusion_delete_cloud_file` -- Delete a cloud file by name. Args: `fileName` (required), `projectName` (optional), `folderPath` (optional). File must not be open in Fusion - close it first with `fusion_close_document`.+- `fusion_walk_cloud_tree` -- **Long-running.** BFS walk of a cloud folder tree. Returns a flat list of all files and folders. Runs entirely on the Fusion main thread - **blocks all other add-in commands** until done (check progress with `fusion_addin_status`).+ - Args: `projectName` (optional), `folderPath` (optional, starting folder), `maxDepth` (default 10), `maxFolders` (default 500), `extensions` (optional list, e.g. `["f3d","fprj"]`), `nameContains` (optional substring filter), `includeFiles` (default true)+ - Returns: `{project, rootFolder, folders[], files[], stats: {foldersVisited, foldersSkipped, filesFound, maxDepthReached, truncated}}`+ - **Per-folder timeout (30s):** If a single folder's cloud API calls take >30s (common for large projects), the folder is skipped and counted in `foldersSkipped`. This prevents indefinite hangs.+ - **Progress tracking:** While running, `fusion_addin_status` returns `walkProgress` with `{foldersVisited, filesFound, currentFolder, queueSize}` - poll this every 1–10s to monitor progress (see "Live folder progress streaming" below).+ - **Non-blocking alternative:** Use `fusion_search_cloud_files` for targeted searches, or `fusion_list_cloud_files` for single-folder listings (these are faster but don't recurse).+ - Example: `adom-bridge fusion_walk_cloud_tree '{"projectName":"Main","folderPath":"Molecules","nameContains":"DRV","extensions":["fprj"]}'`++#### Live folder progress streaming with `watch`++For long walks, use the built-in `watch` wrapper. It spawns the inner search command on a worker thread, polls `fusion_addin_status` internally, and emits one JSON event per line to stdout as the walker visits each folder. **No manual polling loop needed.**++```bash+adom-bridge watch '{"command":"fusion_walk_cloud_tree","args":{"projectName":"Main","folderPath":"Molecules","nameContains":"BQ25792"}}'+```++Output is one JSON object per line, three event types:++```jsonl+{"event":"started","command":"fusion_walk_cloud_tree","args":{...},"interval":2,"_hint":"streaming progress events follow, one per line, ending with 'complete' or 'error'"}+{"event":"progress","busyCommand":"walk_cloud_tree","elapsedSeconds":15.0,"foldersVisited":14,"queueSize":50,"filesFound":0,"currentFolder":"Molecules/Sensing"}+{"event":"progress","busyCommand":"walk_cloud_tree","elapsedSeconds":23.4,"foldersVisited":22,"queueSize":108,"filesFound":0,"currentFolder":"Molecules/Examples"}+{"event":"progress","busyCommand":"walk_cloud_tree","elapsedSeconds":31.8,"foldersVisited":30,"queueSize":100,"filesFound":0,"currentFolder":"Molecules/RAPID NAME TAGS/Connor Wood"}+... (one per real change in walkProgress, deduped) ...+{"event":"complete","result":{"folders":[...],"files":[...],"stats":{"foldersVisited":169,"filesFound":12,"truncated":false}}}+```++Read stdout line-by-line. Stop when you see `event:complete` or `event:error`. The full final result is in the `complete` event's `result` field.++**Optional `interval` arg** (default 2s, clamped to [1, 30]):++```bash+adom-bridge 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 substring search on file names across cloud folders. v1.0.2+ of the add-in adds: per-folder timeout, doEvents() every 50 files, per-file try/except, iterative BFS, and a stale-lock watchdog - Fusion stays responsive throughout (no "Not Responding" freeze).+ - **Args (no hard upper caps in v1.0.2+):** `query` (required, substring, case-insensitive), `projectName` (optional, defaults to active), `folderPath` (optional starting subfolder - **narrow with this**), `recursive` (default false), `maxDepth` (default 2), `maxFolders` (default 10), `maxResults` (default 20), `folderTimeout` (default 30s), `searchTimeout` (default 120s), `timeout` (HTTP envelope, default 620s).+ - **Before calling:** if the user knows roughly where the file is (e.g. customer-named folder), **ask them**. Naming a subfolder cuts 5-40 min searches down to ~10s.+ - **Why slow:** Autodesk's free Fusion 360 Python API has no indexed file-search endpoint. We walk the Data API one folder at a time - each = one HTTP round-trip to Autodesk. **This is an Autodesk API limitation, not adom-bridge's.** When telling the user the search is taking a while, attribute it to Autodesk / Fusion 360, not to adom-bridge's bridge. **There is now a FAST native alternative - `aps_search` (see "APS - fast indexed cloud search" below).** It hits Autodesk Platform Services' indexed Data Management API and returns ~instantly, even when Fusion is CLOSED. Call `aps_status` first; if `configured:true`, prefer `aps_search` and only fall back to this folder-walking verb when APS isn't set up.+ - **Interpreting results (critical for no-false-negatives):** the response contains `searchComplete` (bool), `foldersSkipped`, `filesSkipped`, `truncated`, `folderLimitReached`, `searchTimedOut`. `searchComplete:true` ONLY when **all five** are clean - that's the confidence flag. If `searchComplete:false`, the search hit a cap before exhausting the scope; **don't report "file not found"** - re-run with broader caps OR narrower `folderPath`.+ - **Case sensitivity:** fully case-insensitive both directions (query and file names lowercased). Substring match, not whole-word - `"cosm"` matches `COSMIIC`, `COSMOCOIL`, etc.+ - **Cost feedback in response:** `costAnalysis: {elapsedSeconds, foldersPerSecond, estimatedSecondsPer100Folders}` so the AI can budget the next search realistically.+ - **Real numbers from a live test:** searching `Main/Molecules` recursively (173 folders deep, max 8 depth) for "cosmiic" returned in 168 s with `searchComplete:true, totalFound:3`. Fusion stayed `main_thread:responsive` throughout.+ - **Pair with `watch`** for streaming progress updates: see below.++#### APS - fast indexed cloud search (v1.8.182+)++`aps_search` is a **native, ~instant** alternative to the folder-walking `fusion_search_cloud_files`. It queries **Autodesk Platform Services** (APS, formerly Forge) **Data Management API**, which has a real server-side index - so a search that takes minutes by folder-walking returns in well under a second, and it works **even when Fusion 360 is closed** (it's pure HTTPS, no bridge required). adom-bridge runs its own 3-legged **PKCE** OAuth, so the user's APS **client secret never leaves their machine**.++**One-time setup (user side):**+1. Register a free app at <https://aps.autodesk.com> → enable the **Data Management API**.+2. Set the app's **Callback URL** to exactly `http://localhost:8910/aps/callback`.+3. Put the **Client ID** in Bridge config key `aps_client_id` (in `~/.adom/config.json`) or via Settings.+4. Run `aps_auth` once - it opens the system browser for Autodesk sign-in. Tokens cache in `~/.adom/aps_tokens.json` and auto-refresh; you won't be prompted again.++**Verbs:**+- `aps_status` - **call this first.** Non-prompting. Returns `{configured, signedIn}`. `configured:false` → no client id; use `fusion_search_cloud_files`. `configured:true, signedIn:false` → run `aps_auth`. Both true → `aps_search` is ready.+- `aps_auth` - interactive sign-in (opens browser). One-time per device.+- `aps_hubs` - `{hubs[]{id,name}}` (your Autodesk teams).+- `aps_projects {hubId?}` - `{hubId, projects[]{id,name}}` (default: first hub).+- `aps_search {query, projectName?, projectId?, hubId?}` - `{query, hubId, count, results[]{name,id,type,lastModified,project}}`. `query` is a case-insensitive substring of the file's displayName. Without `projectName`/`projectId` it searches up to 8 projects in the hub. Open a hit with `fusion_open_cloud_file {fileName, projectName}`.++**Decision flow for an AI doing a cloud file search:** `aps_status` → if `configured` (sign in with `aps_auth` if needed) use `aps_search`; else fall back to `fusion_search_cloud_files` and tell the user that registering a free APS app (above) would make future searches instant.++**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: workspace → Fusion Cloud → workspace**++```bash+# === workspace to Fusion Cloud ===+# 1. Send file from Docker to the Mac+adom-bridge send_files '{"files": [{"path": "/home/user/component.step"}]}'+# 2. Import into Fusion+adom-bridge fusion_import_step '{"filePath": "/Users/<you>/Downloads/component.step"}'+# 3. Save to cloud (gets a wip_urn for 3D library linking)+adom-bridge fusion_save_to_cloud '{"name": "my-component", "projectName": "Personal"}'++# === Fusion Cloud to Docker ===+# 1. Find and open the cloud file+adom-bridge fusion_walk_cloud_tree '{"projectName": "Personal", "nameContains": "my-component"}'+adom-bridge fusion_open_cloud_file '{"fileName": "my-component", "projectName": "Personal"}'+# 2. Export to local filesystem+adom-bridge fusion_export_cloud_file '{"outputPath": "/tmp/export/my-component.step", "format": "step"}'+# 3. Pull back to Docker+adom-bridge pull_file '{"path": "/tmp/export/my-component.step"}'++# === Cloud management ===+adom-bridge fusion_list_cloud_projects '{}'+adom-bridge fusion_create_cloud_folder '{"folderName": "Electronics", "projectName": "Main"}'+adom-bridge fusion_list_cloud_files '{"projectName": "Main", "folderPath": "Electronics"}'+adom-bridge fusion_delete_cloud_file '{"fileName": "old-file", "projectName": "Main"}'+```++#### Manufacturing Exports (Gerbers, BOM, CPL)++Export manufacturing files from an open PCB board - everything needed to fabricate boards and assemble components via Adom's PCBA service. **All manufacturing commands require a .brd board open in PCB Editor** (use `fusion_open_board` or `fusion_show_2d_board`).++**Recommended workflow order:** `detect_layers` → `set_design_rules` → DRC → `export_gerbers` → `export_bom` → `export_cpl` → `pull_file` all back to Docker.++Every manufacturing command returns structured JSON with:+- `message` - AI-oriented summary explaining what was produced and why it matters+- `nextSteps[]` - ordered list of what to run next in the manufacturing pipeline+- `hint` - tips for interpreting results or recovering from issues+- `data` - structured metadata (paths, counts, layer info) for programmatic use++**Decision guide - which command to run:**+- **Don't know the layer count?** → Run `fusion_detect_layers` first+- **Need to check if the board meets fab specs?** → Run `fusion_set_design_rules` then DRC+- **Ready to generate fab files?** → Run `fusion_export_gerbers`, then `fusion_export_bom`, then `fusion_export_cpl`+- **Need visual review before export?** → Run `fusion_export_board_image` with preset `assembly_top` or `fabrication`+- **Something failed?** → Check `data.hint` and `data.recoverySteps` in the error response++**Commands:**++- `fusion_detect_layers` -- Detect if the open board is 2-layer or 4-layer. Uses ULP script (primary) and CAM comparison (fallback). Returns `layerCount`, `copperLayers[]`, `method` (how it was detected), and `nextSteps[]`. **Run this first** - all other manufacturing commands auto-detect layers too, but running this explicitly gives you the data before committing to exports.++- `fusion_set_design_rules` -- Apply Adom's JLCPCB-derived design rules (.edru XML files) to the open board. Auto-detects 2-layer vs 4-layer and loads the appropriate rule set. Returns `description` (human-readable rule summary), `edruFile`, and `nextSteps[]`.+ - `action`: `"apply"` (default) loads rules into the board; `"export"` saves current board rules to a file; `"show"` displays rule capabilities without modifying anything+ - `layers`: `"auto"` (default), `"2"`, or `"4"` - force a specific rule set+ - After applying: run `fusion_electron_run '{"command": "DRC"}'` to check for violations. DRC markers appear in the board editor.++- `fusion_apply_instapcb_rules` -- Convenience wrapper that applies the bundled Adom InstaPCB design rule sets without needing a local `.edru` file. Args: `layers` (`"2"` or `"4"`). Sends the bundled `.edru` from Docker to the Mac automatically and loads it via the EAGLE `drc load` command. Use this instead of `fusion_set_design_rules` when you just want Adom's standard 2-layer or 4-layer InstaPCB rules.++- `fusion_load_design_rules` -- Generic loader for ANY custom `.edru` file by path. Args: `filePath`. Use this for third-party fab vendor rules (JLCPCB, PCBWay, OSHPark, etc.) that the user has on disk.++- `fusion_export_gerbers` -- Export Gerber (RS-274X) + Excellon drill files as a ZIP. Auto-detects 2-layer vs 4-layer and selects the correct JLCPCB-compatible CAM job. Returns `zipPath`, `zipSizeKB`, `files[]` (list of gerber files in the ZIP with sizes), `layerCount`, and `nextSteps[]`.+ - `outputDir`: directory for the output ZIP (default: `/tmp/adom-gerbers/`)+ - `boardName`: prefix for the ZIP filename (default: from active document name)+ - `layers`: `"auto"` (default), `"2"`, or `"4"` - force CAM job selection+ - Output ZIP contains: GTL, GBL (copper), GTS, GBS (solder mask), GTP, GBP (paste), GTO, GBO (silkscreen), GKO (outline), XLN (drill). 4-layer boards also get G1, G2 (inner copper).++- `fusion_export_bom` -- Export Bill of Materials as CSV. Groups identical parts by value+package with quantity counts. Returns `componentCount`, `uniquePartCount`, and `nextSteps[]`.+ - `outputPath`: (default: `/tmp/adom-bom.csv`)+ - `grouped`: `true` (default) groups by value+package; `false` lists every component individually+ - Output columns: Comment, Designator, Footprint, Quantity, Library - compatible with JLCPCB, PCBWay, Mouser, Digi-Key.++- `fusion_export_cpl` -- Export Component Placement List (pick-and-place) as CSV. Returns `totalPlacements`, `topCount`, `bottomCount`, and `nextSteps[]`.+ - `outputPath`: (default: `/tmp/adom-cpl.csv`)+ - `side`: `"all"` (default), `"top"`, or `"bottom"` - filter for single-sided assembly+ - Output columns: Designator, Mid X, Mid Y, Layer, Rotation - coordinates in mm.++- `fusion_export_board_image` -- Export PNG image of the board with layer presets. Returns `fileSize`, `preset`, and `nextSteps[]`.+ - `outputPath`: (default: `/tmp/adom-board.png`)+ - `dpi`: resolution (default: 300, max: 600)+ - `preset`: layer preset name (see table below)+ - `layers`: custom layer numbers as int array - overrides preset+ - `monochrome`: `true` for black & white+ - `listPresets`: set `true` to get available presets instead of exporting++**Layer presets for `fusion_export_board_image`:**++| Preset | Layers | Description |+|--------|--------|-------------|+| `all` | All | Every layer visible |+| `top_copper` | 1, 17, 18 | Top copper + pads + vias |+| `bottom_copper` | 16, 17, 18 | Bottom copper + pads + vias |+| `top_silkscreen` | 21, 25 | Top silkscreen + component names |+| `bottom_silkscreen` | 22, 26 | Bottom silkscreen + component names |+| `top_soldermask` | 29 | Top solder mask openings |+| `bottom_soldermask` | 30 | Bottom solder mask openings |+| `top_paste` | 31 | Top paste/stencil openings |+| `bottom_paste` | 32 | Bottom paste/stencil openings |+| `board_outline` | 20 | Board outline (dimension layer) |+| `drill` | 44, 45, 17, 18 | Drill holes + vias |+| `assembly_top` | 1, 17, 18, 20, 21, 25, 51 | Top assembly - copper + silk + outline |+| `assembly_bottom` | 16, 17, 18, 20, 22, 26, 52 | Bottom assembly - copper + silk + outline |+| `fabrication` | 1, 16, 17–20, 21, 22, 25, 26, 29, 30, 51 | Full fabrication view |++**Adom InstapcbPCB manufacturing capabilities (used by `fusion_set_design_rules`):**++| Parameter | Metric | Imperial |+|-----------|--------|----------|+| Layers | 1, 2, 4, 6 | - |+| Min trace width | 0.08mm | 3 mil |+| Min trace spacing | 0.08mm | 3 mil |+| Min via drill | 0.2mm | 8 mil |+| Min silkscreen text | 0.1mm | 4 mil |+| Board edge clearance | 0.05mm | 2 mil |+| Board thickness | 1.6mm | 63 mil |+| Copper weight | 0.5 oz (17.5 μm) | - |+| Solder mask | Green | - |+| Surface finish | HASL / ENIG | - |+| Turnaround | 4 hours (fab + assembly) | - |++**Manufacturing workflow - Fusion to Adom PCBA:**++```bash+# 1. Open the board+adom-bridge fusion_open_board '{"filePath": "C:/projects/myboard.brd"}'++# 2. Detect layer count (auto-selects 2-layer or 4-layer rules/CAM)+adom-bridge fusion_detect_layers++# 3. Apply Adom design rules for the detected layer count + run DRC+adom-bridge fusion_set_design_rules '{"action": "apply"}'+adom-bridge fusion_electron_run '{"command": "DRC"}'++# 4. Export manufacturing files (gerbers auto-select correct CAM job)+adom-bridge fusion_export_gerbers '{"outputDir": "/tmp/mfg"}'+adom-bridge fusion_export_bom '{"outputPath": "/tmp/mfg/bom.csv"}'+adom-bridge fusion_export_cpl '{"outputPath": "/tmp/mfg/cpl.csv"}'++# 5. Export board images for review+adom-bridge fusion_export_board_image '{"outputPath": "/tmp/mfg/top-copper.png", "preset": "top_copper"}'+adom-bridge fusion_export_board_image '{"outputPath": "/tmp/mfg/assembly-top.png", "preset": "assembly_top"}'+adom-bridge fusion_export_board_image '{"outputPath": "/tmp/mfg/board-outline.png", "preset": "board_outline"}'++# 6. Pull files back to Docker for Adom PCBA ordering+adom-bridge pull_file '{"path": "/tmp/mfg/bom.csv"}'+adom-bridge pull_file '{"path": "/tmp/mfg/cpl.csv"}'+``` ### Desktop Tools @@ -1410,6 +2003,17 @@ curl -sf http://127.0.0.1:8766/health `shell_execute` runs via `sh -c` on the Mac. For multi-line work prefer `run_script` (base64, no quoting hell). Use `python3`, not `python`. +## Building from Source++```bash+cd cli && cargo build --release+# Binary at: cli/target/release/adom-bridge+```++## Repo++[github.com/adom-inc/adom-bridge](https://github.com/adom-inc/adom-bridge)+ ## Run wiki ops on the user's Mac - `wiki_exec` (v1.9.116+) Bridge bundles the `adom-wiki` CLI inside the app (Contents/Resources), and `wiki_exec` runs it ON THE MAC - so version-controlling a local project (KiCad board, docs folder) on wiki.adom.inc needs NO per-file `pull_file` round trip to the container and there is no container/Mac drift.
package.json+2−2@@ -1,6 +1,6 @@ { "slug": "adom-bridge-macos",- "version": "1.9.204",+ "version": "1.9.207", "type": "app", "brief": "Adom Bridge is a small operating system for AI on your Mac: it gives an AI real hands on your native desktop, including your signed-in browser, KiCad, Fusion 360, the shell, files, windows and screenshots. It provisions its own Node and Python so bridges run without touching your setup, and it talks straight to your machine. It ships with Adom Hydrogen and installs standalone too, so if you drive your Mac with Claude Desktop or another AI, this is what gives that AI hands.", "description": "Install this package in a cloud or Adom Hydrogen container, not on the Mac. It delivers the Claude skill set that teaches an AI every desktop verb (shell, files, screenshots, windows, notifications, KiCad, Fusion 360, browser automation), plus the Linux adom-bridge CLI when one is staged (publishes from the mac release flow are skills-only; the install degrades gracefully and keeps any CLI already on PATH). The CLI drives the Adom Bridge app running on your Mac over the relay; the macOS app itself is the separate DMG in the Download box, not part of this package. Run adom-wiki pkg update to keep the skills current.",@@ -79,4 +79,4 @@ "dist/**", "version.json" ]-}+}
page.json+1−1@@ -1,6 +1,6 @@ { "name": "adom-desktop-macos",- "version": "1.9.204",+ "version": "1.9.207", "type": "app", "description": "Install this package in a cloud or Adom Hydrogen container, not on the Mac. It delivers the Claude skill set that teaches an AI every desktop verb (shell, files, screenshots, windows, notifications, KiCad, Fusion 360, browser automation), plus the Linux adom-bridge CLI when one is staged (publishes from the mac release flow are skills-only; the install degrades gracefully and keeps any CLI already on PATH). The CLI drives the Adom Bridge app running on your Mac over the relay; the macOS app itself is the separate DMG in the Download box, not part of this package. Run adom-wiki pkg update to keep the skills current.", "dependencies": {},
skills/adom-bridge-bridge-ownership/SKILL.md+2−2@@ -34,14 +34,14 @@ one was **bridge code**. The platform never stated the boundary, so the AI guess manage it. Divergent results from the bridge's own status verb = a **bridge** bug. - **The bridge's add-in / plugin code AND getting it deployed into the host app.** Editing the streamed cache source is not enough if the host app loads the add-in- from elsewhere (e.g. Fusion loads its add-in from its own AddIns dir, not the+ from elsewhere (e.g. Fusion loads from `%APPDATA%\…\API\AddIns\`, not the bridges-cache) — wiring cache→host-app is the **bridge's** concern. - The bridge's `SKILL.md`, `describe` verb, hero/wiki page, and version. - The **edit-source → publish → bump version → reinstall** loop. ## The cardinal rules -1. **NEVER hand-edit the streamed cache** (`~/.adom/bridges-cache/<bridge>/` on macOS).+1. **NEVER hand-edit the streamed cache** (`%LOCALAPPDATA%\Adom Bridge\bridges-cache\<bridge>\`). It's a download. Edit the **source** (the bridge's own repo/wiki), `publish`, bump the version, then `bridge_install`/`refresh_bridges` to pull it. A hand-edit is overwritten on the next sync and may not even be what the host app
skills/adom-bridge-bridge-sdk/SKILL.md+10−3@@ -101,9 +101,16 @@ The response always carries `_capabilities` / `_couldHaveDoneBetter` when a call --- -## Branding a borrowed window (Windows-only verb family)+## Branding + driving a window you borrowed (window identity + taskbar, v1.9.106–1.9.153) -On Windows, Adom Bridge has a generic AUMID verb family (`desktop_register_app_identity`, `desktop_set_window_identity`, `desktop_set_window_jumplist`, taskbar overlays) to make a window your bridge drives present as YOUR app. Those shell surfaces don't exist on macOS — `desktop_taskbar` maps what it can onto the Dock and ignores the rest. The full guide ships with the Windows Adom Bridge package.+If your bridge drives a window another process owns — the classic case is a **Chrome for Testing** window that otherwise reads *"Test"* with Chrome's icon — Adom Bridge gives you a generic verb family to make it present as YOUR app across every Windows shell surface, and to show live status on its taskbar button. Adom Bridge stays generic: it applies whatever name/icon/command you pass; it knows nothing about your app. **Full guide: the `adom-bridge-window-identity` skill** (`https://wiki.adom.inc/api/v1/pages/adom-bridge/files/docs/WINDOW_IDENTITY.md` or the repo `skills/WINDOW_IDENTITY.md`). The essentials:++- **Two halves, you need both:** `desktop_register_app_identity {appId, displayName, iconPath, shortcut}` teaches Windows what an AUMID means (HKCU only, no UAC; `shortcut:false` for a per-session id with no Start Menu spam), and `desktop_set_window_identity {hwnd, appId, iconPath, displayName, relaunchCommand}` stamps it on the live window. Stamp without register → Alt-Tab / taskbar fall back to the owning exe's icon.+- **Per-session identity:** register `shortcut:false` per window (e.g. `Adom.Pup.<sessionId>`) so each keeps its own taskbar button; `desktop_unregister_app_identity {appId}` on close.+- **Jump list + header:** `desktop_set_window_jumplist {appId, tasks, hwnd?, headerIcon?, headerCommand?}` adds the right-click menu AND (with `hwnd`+`headerIcon`) brands the header row (the app tile above Pin/Close) with no Start Menu shortcut.+- **Taskbar status:** `desktop_taskbar {hwnd, progress?, overlay?, flash?, thumbnailTooltip?, thumbnailClip?}` paints glanceable state on any window's button. **New in v1.9.153:** `thumbnailTooltip` is the ONLY uncapped hover-text surface (the window title truncates ~25-30 chars; this does not, and takes newlines; `""` clears it), and `thumbnailClip:{x,y,w,h}` crops the live hover preview to a region (`null` resets). Overlay compositing (`overlay.avatar`/`profileDir`) preserves a browser profile face while adding your mark.+- **Icon format — read the `iconCheck` in every response.** The identity verbs inspect any icon you pass and return `iconCheck {kind,sizes,issues,ok,hint}`. The traps it catches: a **256px `.ico` size that is not PNG-compressed** renders as the generic white-document icon (Vista+ rule); **SVG can't be loaded** by the Win32 shell; the AUMID registry icon wants a `.ico`/`.png`, not an `.exe`. If `iconCheck.ok` is false, fix the file and re-stamp — don't ship the broken icon. Ship a multi-size `.ico` (16/24/32/48/256, the 256 PNG-compressed).+- **The live-tile trap:** re-stamping the SAME appId with a new icon updates the title bar + Alt-Tab but NOT the taskbar tile (Win11 bakes it at button creation). To change a live tile, register + stamp a DIFFERENT appId; the button is recreated in ~1s. --- @@ -302,7 +309,7 @@ This is the code Adom Bridge streams onto the user's machine. Two files on your "updateManifestUrl":"https://wiki.adom.inc/api/v1/pages/adom-bridge-blender-bridge/files/blender-manifest.json" } ``` -**Users install/update** with Adom Bridge's `bridge_install {manifestUrl}` → Adom Bridge downloads the zip, sha256-verifies, unzips into the bridges cache (`~/.adom/bridges-cache/<name>/` on macOS), rescans, and remembers the URL for auto-update.+**Users install/update** with Adom Bridge's `bridge_install {manifestUrl}` → Adom Bridge downloads the zip, sha256-verifies, unzips into `%LOCALAPPDATA%\Adom Bridge\bridges-cache\<name>\`, rescans, and remembers the URL for auto-update. **To ship a new version:** bump `version` in BOTH `bridge.json` (in the zip) and the manifest, re-`release upload` the zip, re-POST the manifest. Users auto-get it on the next 4h poll, or immediately via `refresh_bridges {name}`. **A bundled SEED** (a `plugins/<name>/` folder shipped in Adom Bridge's NSIS) is the offline/first-run fallback; your published cache copy with a higher `version` supersedes it (cache-over-bundled, version-aware). ⚠ **A `release upload` REPLACES a same-platform asset ONLY when the FILENAME is IDENTICAL** — a different name (a version bump `-v1.2.0` → `-v1.3.0`) ADDS a second asset, leaving a stale zip on the release. Keep ONE asset per release: overwrite the same filename, or `adom-wiki release delete-asset <owner/slug> <ver> "<old-filename>"` after a rename. (A stale extra won't break the manifest-URL install, but it clutters the release.)
skills/adom-bridge-bridges-guide/SKILL.md+30@@ -107,6 +107,36 @@ bundled (numeric semver). After an update, the running server process still hold code until it respawns; the version-aware liveness check forces a respawn on the next verb call, so you normally do not need to kill anything. +## Where bridges live on disk (never `find ~`)++Every bridge's directory is already in the inventory — ask, don't search:++```bash+# The on-disk map: roots {bundled, cache, thirdParty, logs, adomHome} plus, per bridge,+# installPath (its directory), entrypoint (main file) and logPath. Aliases: bridge_where.+adom-bridge bridge_paths+# bridge_list carries the same fields (installPath / entrypoint / logPath / roots) alongside+# the runtime info. Then read with read_file / list_dir / bridge_log_read.+```++- **bundled** — read-only seeds inside the app bundle+ (macOS `Adom Bridge.app/Contents/Resources/plugins/<name>/`).+- **cache** — writable, wiki-streamed + third-party installs+ (macOS `~/Library/Application Support/Adom Bridge/bridges-cache/<name>/`,+ Windows `%LOCALAPPDATA%\Adom Bridge\bridges-cache\<name>\`). Cache shadows bundled.+ A Hydrogen-embedded Bridge keeps its **own** cache (`…/adom-hydrogen/bridges-cache/`),+ so always read the roots from the instance you are talking to.+- **logs** — `~/.adom/bridge-logs/<name>.log`.++Do **not** hunt for a bridge with `find ~`. On macOS an unbounded home walk opens+TCC-protected bundles (`~/Music/…/Music Library.musiclibrary`, `~/Pictures/Photos+Library.photoslibrary`, `~/Library/Calendars`) and pops a Media / Photos / Calendars+consent dialog on the user's screen — attributed to whichever app forked the shell+(Hydrogen, when the Bridge is embedded). `shell_execute` / `run_script` refuse such a+walk (`errorCode: unbounded_home_find`) unless it is pruned+(`find ~ \( -path ~/Music -o -path ~/Pictures -o -path ~/Library \) -prune -o … -print`)+or the args carry `allowUnboundedFind: true`.+ ## The key verbs ```bash
skills/adom-bridge-cli-guide/SKILL.md+1−1@@ -14,7 +14,7 @@ Desktop; the CLI binary name is unchanged.) | Where | Binary | How it got there | How it reaches the desktop | |---|---|---|---|-| Cloud Linux container | `adom-bridge` | `adom-wiki pkg install adom/adom-bridge-macos` (installs the CLI + the skill set) | Over the relay it serves (WS :8765 / HTTP :8766) |+| Cloud Linux container | `adom-bridge` | `adom-wiki pkg install adom/adom-bridge` (installs the CLI + the skill set) | Over the relay it serves (WS :8765 / HTTP :8766) | | The Mac, inside the app bundle | `adom-bridge-cli` at `Adom Bridge.app/Contents/Resources/` | Bundled by the Bridge installer (standalone: `/Applications`; Hydrogen-managed: `~/Library/Application Support/adom/bridge/`) | Auto-probes the local app's loopback direct API (`127.0.0.1:47200`, discovery file `~/.adom/direct-api-port`, fallback scan 47200-47209) | Verify which build you have:
skills/adom-bridge-discovery/SKILL.md+5−6@@ -60,7 +60,7 @@ For any multi-minute command, prefer verbs that return immediately and let you p The canonical container install is the wiki package (bundles the Linux CLI **and** the skill set, which link into `~/.claude/skills/`): ```bash-adom-wiki pkg install adom/adom-bridge-macos # installs the adom-bridge CLI + the adom-bridge-* skills+adom-wiki pkg install adom/adom-bridge # installs the adom-bridge CLI + the adom-bridge-* skills adom-bridge --version # confirm ``` @@ -80,13 +80,12 @@ The pkg install lays down sibling skills that document the features in depth; th | Skill | Covers | |---|---|-| `adom-bridge-macos` (main) | the full verb reference + shell/process/file/window patterns, status+durationMs result shape |-| `adom-bridge-cli-guide` | the CLI itself: verb discovery, --target, serve --supervise, response shape |-| `adom-bridge-window-capture` | screenshots on macOS: every capture verb + the on-disk contract |+| `adom-bridge` (main) | the full verb reference + shell/process/file/window patterns, status+durationMs result shape |+| `adom-bridge-pup` | Puppeteer real-Chrome automation (browser_open_window/navigate/screenshot/eval, multi-session profiles) |+| `adom-bridge-kicad` | KiCad bridge (open schematics/boards, DRC, library install, 3D viewer, automation) |+| `adom-bridge-fusion` | Fusion 360 bridge (.f3d/.f3z, STEP/GLB/.lbr import-export, BOM/params, Fusion API) | | `adom-bridge-direct-api` | the loopback HTTP API for sibling apps (Adom Hydrogen) | -The KiCad / Fusion / pup bridges ship their OWN skills — `adom-bridge sync_skills` pulls each connected desktop's bridge skill packages.- But you don't need to read those to act: **`adom-bridge commands` is always the live source of truth** for what verbs exist right now. Bridges installed on the Mac ship their own skills; `adom-bridge sync_skills` pulls them. ## 6: Why this skill exists
skills/adom-bridge-installer/._SKILL.mdadded⋯ 1 unchanged line ⋯
skills/adom-bridge-installer/SKILL.mdadded+452@@ -0,0 +1,452 @@+---+name: adom-bridge-installer+description: How to invoke the Adom Bridge NSIS installer from a parent installer (especially Adom Hydrogen's setup steps) — silent install, /NOLAUNCH switch for Hydrogen-managed bundling, live progress via the structured log file at %TEMP%\adom-bridge-install.log. Use when bundling Adom Bridge inside another Windows installer, writing an install supervisor / setup-step UI, detecting Adom Bridge's installed version, or upgrading an existing Adom Bridge install programmatically.+---++# Adom Bridge installer integration++Adom Bridge (Adom Bridge) ships as a single NSIS installer:++```+Adom Bridge_<version>_x64-setup.exe (~4.8 MB on v1.8.45)+```++It installs to `%LOCALAPPDATA%\Adom Bridge\` — no UAC prompt, no admin needed. Tauri-2 bundled NSIS with our customisations in `src-tauri/installer-hooks.nsh`.++## Command-line surface++| Flag | Meaning |+|---|---|+| (none) | Installs silently, auto-launches `adom-bridge.exe` after install |+| `/S` | Standard NSIS silent flag — same behavior since the top-level `SilentInstall silent` directive already suppresses UI |+| `/NOLAUNCH` | **v1.8.45+** — install silently, do NOT auto-launch after install. Caller is responsible for spawning Adom Bridge with the desired flags |+| `/D=<path>` | Install destination override. Default is `$LOCALAPPDATA\Adom Bridge`. Rarely needed |+| `<uninstall.exe> /S` | Silent uninstall. The uninstaller lives at `%LOCALAPPDATA%\Adom Bridge\uninstall.exe` |++**UI behavior**: zero UI in all cases. `SilentInstall silent` is set at the top of the NSIS script, so even `setup.exe` invoked from Explorer runs without a window. No Welcome page, no progress bar, no Finish page.++**Auto-launch**: by default the installer calls `ExecShell "" "$INSTDIR\adom-bridge.exe"` at the end of POSTINSTALL. Pass `/NOLAUNCH` to suppress this. Used by Hydrogen when bundling Adom Bridge because Hydrogen wants to spawn Adom Bridge with `--embedded --start-hidden --relay-url ... --session-token ...` itself, not let Adom Bridge launch standalone first and get respawned via single-instance.++## Live progress log++The installer writes structured progress to `%TEMP%\adom-bridge-install.log` (Windows expands `%TEMP%` to the user's temp dir, typically `C:\Users\<name>\AppData\Local\Temp`). Each line is `[STAGE] message\r\n`:++```+[START] adom-bridge installer 1.8.45 starting+[PREINSTALL] Cleaning legacy desktop shortcuts+[PREINSTALL] Done. Tauri will now copy bundle files.+[POSTINSTALL] Bundle files copied. Cleaning Tauri's auto-created desktop shortcut.+[POSTINSTALL] /NOLAUNCH passed — skipping auto-launch (caller will spawn)+[DONE] adom-bridge installer complete (no-launch mode)+```++**`[START]`** opens with a truncated write (fresh log per install). **`[DONE]`** is always the final line — readers tailing the file can use that as the completion sentinel without needing to wait for the process exit.++Stage tags currently emitted:+- `[START]` — once, at the top+- `[PREINSTALL]` — Tauri's beforeBuildCommand hook ran, files about to be copied+- `[POSTINSTALL]` — files copied; Tauri's standard shortcuts created and our hook is cleaning the unwanted ones+- `[DONE]` — final line, installer about to exit++The uninstaller writes the same log with `[PREUNINSTALL]` / `[POSTUNINSTALL]` stages.++## Recipe: Hydrogen's setup-step UI tails the log++Spawn the installer with `/NOLAUNCH`, poll the log file on a 200 ms timer, surface each new line to Hydrogen's UI. When `[DONE]` shows up OR the process exits, the install is complete.++### Rust (Hydrogen's installer driver)++```rust+use std::{env, fs, path::PathBuf, process::Command, time::Duration};+use std::os::windows::process::CommandExt;+use tokio::time::sleep;++const CREATE_NO_WINDOW: u32 = 0x08000000;++async fn install_adom_bridge<F: FnMut(String)>(+ installer_path: &str,+ mut on_line: F,+) -> Result<(), String> {+ let log_path: PathBuf = [&env::var("TEMP").map_err(|e| e.to_string())?, "adom-bridge-install.log"]+ .iter()+ .collect();+ // Pre-clear so we don't read stale lines from a previous install.+ let _ = fs::remove_file(&log_path);++ let mut child = Command::new(installer_path)+ .args(["/S", "/NOLAUNCH"])+ .creation_flags(CREATE_NO_WINDOW)+ .spawn()+ .map_err(|e| format!("spawn installer: {e}"))?;++ let mut last_pos = 0usize;+ loop {+ // Tail any new content+ if let Ok(content) = fs::read_to_string(&log_path) {+ if content.len() > last_pos {+ let slice = &content[last_pos..];+ if let Some(last_nl) = slice.rfind('\n') {+ let consumed = &slice[..=last_nl];+ for line in consumed.lines() {+ on_line(line.to_string());+ if line.starts_with("[DONE]") {+ // Don't return yet — wait for process exit+ // so we surface the actual exit code, but+ // we know progress streaming is finished.+ }+ }+ last_pos += consumed.len();+ }+ }+ }+ // Check if installer has exited+ if let Some(status) = child.try_wait().map_err(|e| e.to_string())? {+ return if status.success() {+ Ok(())+ } else {+ Err(format!("installer exited with {}", status))+ };+ }+ sleep(Duration::from_millis(200)).await;+ }+}++// Caller:+// install_adom_bridge(r"C:\path\to\Adom Bridge_1.8.45_x64-setup.exe", |line| {+// hydrogen_setup_step.append_live_output(&line);+// }).await?;+```++### Bash (debugging / scripting)++```bash+# Pre-clear stale log+rm -f "$TEMP/adom-bridge-install.log" 2>/dev/null++# Spawn installer in background, tail the log+"./Adom Bridge_1.8.45_x64-setup.exe" /S /NOLAUNCH &+INSTALLER_PID=$!++# Tail until process exits OR [DONE] appears+while kill -0 $INSTALLER_PID 2>/dev/null; do+ [ -f "$TEMP/adom-bridge-install.log" ] && tail -F "$TEMP/adom-bridge-install.log" &+ sleep 0.2+done+wait+echo "Installer exited"+```++## Recipe: detect if Adom Bridge is already installed (and at what version)++The signed NSIS installer (`installMode: "currentUser"`) writes Adom Bridge's uninstall info to `HKCU` — no admin needed. **⚠ The key name is the product name `Adom Bridge` (WITH a space), NOT `AdomBridge`** (verified live 2026-07-03). Two history notes so you don't chase the wrong key:+- The NSIS uninstall key = the product name → `…\Uninstall\Adom Bridge` (space).+- Adom Bridge's OWN launch-time code used to ALSO write an `AdomBridge` (no-space) key to **HKLM** — that write was **DELETED in v1.9.68** (the no-UAC fix). So `AdomBridge` no longer exists anywhere; read `Adom Bridge`.++```+HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\Adom Bridge+ DisplayName = "Adom Bridge"+ DisplayVersion = "1.9.74"+ InstallLocation = "C:\Users\<name>\AppData\Local\Adom Bridge"+ UninstallString = "C:\Users\<name>\AppData\Local\Adom Bridge\uninstall.exe"+ Publisher = "Adom Industries, Inc."+```++```rust+use winreg::enums::HKEY_CURRENT_USER;+use winreg::RegKey;++fn adom_bridge_installed_version() -> Option<String> {+ let hkcu = RegKey::predef(HKEY_CURRENT_USER);+ // NOTE the SPACE — the NSIS uninstall key is the product name "Adom Bridge".+ let key = hkcu+ .open_subkey(r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Adom Bridge")+ .ok()?;+ key.get_value("DisplayVersion").ok()+}+```++Compare with `semver::Version::parse` to decide whether to skip / upgrade / leave-alone.++## Recipe: Hydrogen's setup-step ladder for embedding Adom Bridge++The full pattern Hydrogen's setup-step UI runs when embedding Adom Bridge:++```+1. Check installed version → registry read above+2. Decide:+ no install → install bundled Adom Bridge+ older than bundled → install bundled Adom Bridge (upgrade)+ same or newer → skip install entirely+3. Run installer (silent + /NOLAUNCH) → tail log, surface to UI+4. Write embedded marker file → %LOCALAPPDATA%\Adom Bridge\embedded.json+5. Delete Adom Bridge's user-facing shortcuts → Start Menu, Desktop, Startup folder+ (so user can't double-launch standalone)+6. Spawn Adom Bridge with embedded flags → adom-bridge.exe --embedded+ --start-hidden+ --relay-url ws://127.0.0.1:8765+ --relay-name adom-hydrogen+ --session-token <hd's token>+7. Poll Adom Bridge's direct API for liveness → GET http://127.0.0.1:47200/health+```++Steps 4 and 5 are also covered by Adom Bridge's installer if you pass `--embedded` at Adom Bridge's first launch (the embedded-mode detection writes the marker and Adom Bridge's autostart-upgrade routine handles its own shortcuts). Doing them in Hydrogen's installer is belt-and-suspenders.++## Bundle-resource pattern++Hydrogen's `tauri.conf.json` should include the Adom Bridge installer as a bundle resource so it ships inside Hydrogen's NSIS installer:++```json+"bundle": {+ "resources": {+ "resources/Adom Bridge_1.8.45_x64-setup.exe": "resources/"+ }+}+```++At runtime Hydrogen finds it under its install dir at `$INSTDIR\resources\Adom Bridge_<ver>_x64-setup.exe` — pass that path to the install function above.++## Versioning + upgrade strategy++| Scenario | What Hydrogen's installer should do |+|---|---|+| No Adom Bridge installed | Install bundled version |+| Adom Bridge older than bundled | Install bundled version (upgrade) |+| Adom Bridge same as bundled | Skip install step; still spawn with embedded flags |+| Adom Bridge newer than bundled | Skip install step; spawn with embedded flags. Don't downgrade — embedded mode works for any Adom Bridge ≥ v1.8.42 |++The bundled Adom Bridge installer is the minimum supported version for the Hydrogen release. Hydrogen can spawn newer ADs without modification.++## Exit codes++NSIS returns:+- `0` — install succeeded+- `2` — user cancelled (won't happen with `SilentInstall silent` since there's no UI to cancel)+- Other — install error (rare with our hooks; typical cause is disk full or `%LOCALAPPDATA%` not writable)++Treat anything non-zero as a hard failure and surface the log file's last 20 lines to the user.++## Where the installer lives++**v1.8.115+** — the end-user binary home is the **V2 wiki blob store** (the+GitHub repo is private; GH release URLs 404 for anonymous downloads). Two+discovery surfaces, both anonymous:++**Preferred for parent installers (Hydrogen setup): the packages API.** Resolve+the latest registered version and its assets in one or two GETs — note+`?org=adom` is REQUIRED (the slug exists in multiple orgs; bare URLs 400):++```+https://wiki.adom.inc/api/v1/packages/adom-bridge/dist-tags?org=adom+ → {"dist_tags":{"latest":"X.Y.Z"}}+https://wiki.adom.inc/api/v1/packages/adom-bridge/manifest?org=adom+ → { version, ..., assets: { "windows-x64": { filename, url, sha256, size } } }+```++The `assets` map is server-derived from the page's blobs (sha256 computed+from the actual bytes — integrity-checkable). `assets[...].url` is+server-relative (`/blob/app/adom-bridge/<file>`); prefix the git-wiki host.++**Fallback / updater path: version.json**, V2-first with V1 fallback:++```+https://wiki.adom.inc/api/v1/pages/adom-bridge/files/version.json+https://wiki.adom.inc/api/v1/pages/adom-bridge/files/version.json+```++The V2 manifest's `windows.installer_url` / `cli.<plat>.binary_url` point at+the V2 files endpoint; V1's point at V1 static copies. **Follow the+manifest's URLs — do NOT construct binary URLs by hand.**++Network quirk: HEAD on V2 blob/file URLs may return 404 while GET works+(git-wiki #40 — fix in flight). Probe with a Range GET (`Range: bytes=0-0`,+expect 206), never bare HEAD. Asset filenames are dot-form everywhere+(`Adom.Bridge_<ver>_windows_x64-setup.exe`); V1 dot-normalized since v1.8.72.++The wiki page's `metadata.install.windows_installer` field points at the+canonical filename for the current published version. Hydrogen's release build+should fetch the latest via the V2-first pattern:++```bash+# Try V2 first, fall back to V1.+fetch_manifest() {+ curl -fL --max-time 15 \+ "https://wiki.adom.inc/api/v1/pages/adom-bridge/files/version.json" \+ 2>/dev/null \+ || curl -fL --max-time 15 \+ "https://wiki.adom.inc/api/v1/pages/adom-bridge/files/version.json"+}++MANIFEST=$(fetch_manifest)+INSTALLER_URL=$(echo "$MANIFEST" | jq -r .windows.installer_url)+INSTALLER_SHA=$(echo "$MANIFEST" | jq -r .windows.installer_sha256)+# Each manifest carries a self-consistent installer_url that points at the+# SAME backend the manifest was served from — V2 manifest → V2 installer URL,+# V1 manifest → V1 installer URL. No URL mixing across backends.++curl -fL "$INSTALLER_URL" -o resources/ad-setup.exe++# Verify SHA256 before bundling — defends against partial fetches + tampering+DOWNLOAD_SHA=$(sha256sum resources/ad-setup.exe | awk '{print $1}')+[ "$DOWNLOAD_SHA" = "$INSTALLER_SHA" ] || { echo "SHA mismatch"; exit 1; }+```++The desktop Adom Bridge's built-in auto-updater (`src-tauri/src/updater.rs`) walks an+ordered `MANIFEST_URLS` list — V2 first, V1 fallback — and uses whichever+responds first. Once V2 has been the source of truth for a release cycle+without incident, V1 will be retired from the pipeline.++`version.json` also carries the SHA256 hash of the installer so Hydrogen's build+pipeline can verify the download before bundling.++## Asset naming convention (v1.8.113+)++The Adom Bridge wiki page hosts TWO distinct types of artifacts per release:++1. **Full Adom Bridge GUI installer** — the actual desktop app (Tauri window, bundled+ bridges, relauncher). One per OS / architecture.+2. **CLI companion binary** — a separate executable that talks to Adom Bridge over+ the relay. Used by Hydrogen's WSL2 container, cloud builds, sibling-app+ installers, cron jobs. Does NOT contain Adom Bridge itself.++Both ship together at the same version (single `python scripts/bump-version.py+patch` bumps both, single `cargo tauri build` + `cargo build --release` in+`cli/` produces both, single `scripts/release-publish.sh` uploads both).++### Filename pattern++| Artifact | Wiki asset name pattern | Example (v1.8.113) |+|---|---|---|+| Full Adom Bridge GUI — Windows | `Adom.Bridge_<ver>_windows_x64-setup.exe` | `Adom.Bridge_1.8.113_windows_x64-setup.exe` |+| Full Adom Bridge GUI — macOS | `Adom.Bridge_<ver>_macos_<arch>.dmg` | `Adom.Bridge_1.8.113_macos_aarch64.dmg` (future) |+| Full Adom Bridge GUI — Linux | `Adom.Bridge_<ver>_linux_<arch>.AppImage` | `Adom.Bridge_1.8.113_linux_x86_64.AppImage` (future) |+| CLI companion — Linux | `adom-bridge-cli_<ver>_linux_<arch>` | `adom-bridge-cli_1.8.113_linux_x86_64` |+| CLI companion — macOS | `adom-bridge-cli_<ver>_macos_<arch>` | `adom-bridge-cli_1.8.113_macos_aarch64` (future) |+| CLI companion — Windows | `adom-bridge-cli_<ver>_windows_<arch>.exe` | `adom-bridge-cli_1.8.113_windows_x64.exe` (future) |++The `_cli_` token in the middle is the only signal you need to know "this is+the companion, not the app." `linux_x86_64` always means Linux x86_64,+regardless of whether the file is a GUI installer or a CLI binary.++### After install, the binary name is `adom-bridge`++The wiki asset name disambiguates at FETCH time. At INSTALL time the binary+is renamed to `adom-bridge` so the command users type doesn't change:++```bash+# Linux example — install the CLI companion+curl -fL "https://wiki.adom.inc/api/v1/pages/adom-bridge/files/adom-bridge-cli_1.8.113_linux_x86_64" \+ -o /tmp/ad+sudo install -m 755 /tmp/ad /usr/local/bin/adom-bridge+adom-bridge --version+# → adom-bridge 1.8.113 (<sha>, built <ts>)+```++### Version.json `.cli` slot++The auto-update manifest (v1.8.113+) carries a typed `cli` sibling section+for the CLI binaries:++```json+{+ "manifest_version": 1,+ "version": "1.8.113",+ "windows": { "installer_url": ".../Adom.Bridge_1.8.113_windows_x64-setup.exe", ... },+ "cli": {+ "linux_x86_64": {+ "binary_url": ".../adom-bridge-cli_1.8.113_linux_x86_64",+ "binary_filename": "adom-bridge-cli_1.8.113_linux_x86_64",+ "binary_sha256": "...",+ "binary_size": 8012408+ }+ },+ "changelog_url": "...",+ "minimum_required": "1.3.30"+}+```++Pre-v1.8.113 readers ignore `.cli` entirely (additive schema; no+`manifest_version` bump required). The desktop Adom Bridge's own auto-updater only+reads `.windows / .macos / .linux` for its GUI installer URL — it doesn't+touch `.cli`.++For Hydrogen / cloud / Docker-side fetchers, prefer the typed lookup:++```bash+# Best: read the URL out of the manifest (so the filename can evolve)+MANIFEST=$(fetch_manifest)+LINUX_CLI_URL=$(echo "$MANIFEST" | jq -r '.cli.linux_x86_64.binary_url')+LINUX_CLI_SHA=$(echo "$MANIFEST" | jq -r '.cli.linux_x86_64.binary_sha256')+curl -fL "$LINUX_CLI_URL" -o /tmp/ad+[ "$(sha256sum /tmp/ad | awk '{print $1}')" = "$LINUX_CLI_SHA" ] || exit 1+sudo install -m 755 /tmp/ad /usr/local/bin/adom-bridge+```++### Windows CLI is shipped with the NSIS (v1.8.114+)++Installing Adom Bridge on Windows now drops the CLI companion next to the GUI exe+and adds the install dir to the user's PATH so PowerShell / Claude Desktop /+cmd find it without configuration:++```+%LOCALAPPDATA%\Adom Bridge\adom-bridge.exe ← GUI (Windows subsystem)+%LOCALAPPDATA%\Adom Bridge\adom-bridge-cli.exe ← CLI companion (Console subsystem)+%LOCALAPPDATA%\Adom Bridge\ad-relauncher.exe ← visible self-restart helper+```++NSIS's POSTINSTALL hook prepends `%LOCALAPPDATA%\Adom Bridge` to+`HKCU\Environment\Path` (no UAC — user-level only) and broadcasts+`WM_SETTINGCHANGE` so freshly-opened shells pick up the change. Already-+open shells need a re-launch (OS limitation).++After install, in a fresh PowerShell:++```powershell+adom-bridge-cli --version+# adom-bridge 1.8.114 (<sha>, built <ts>)+adom-bridge-cli status+adom-bridge-cli screenshot_window '{"title-contains":"VS Code"}'+```++The CLI auto-discovers the local Adom Bridge via direct-API probe at+`127.0.0.1:47200` — no relay needed when Adom Bridge is on the same box. ~5ms RTT+for compatible verbs; verbs in Adom Bridge's `cliRequired` list still flow through+a relay if one's running (Hydrogen's WSL2 distro typically owns the relay).++**Relay self-detect (v1.8.114+):** `adom-bridge-cli serve` checks for+another adom-bridge relay on `127.0.0.1:8766/health` before binding. If+one already answers, it prints a friendly note and exits cleanly without+starting a second relay (which would just sit idle — Adom Bridge's WS client only+attaches to the first one it finds). Pass `--force-bind` to override.++PREUNINSTALL strips the PATH entry back out and rebroadcasts so an+uninstall of Adom Bridge also gets PATH cleanup.++### Legacy alias migration window++Releases prior to v1.8.113 published the Linux CLI as `adom-bridge-linux`+(no version, no architecture). Hydrogen's WSL2 setup and a few cron-based+installers have that literal filename baked in.++To keep them working through the rename, v1.8.113 publishes BOTH names:++| Name | Status | Wiki URL (V2 / V1) |+|---|---|---|+| `adom-bridge-cli_<ver>_linux_x86_64` | **canonical, v1.8.113+** | V2 / V1 |+| `adom-bridge-linux` | **deprecated alias** — byte-identical copy | V2 / V1 |++Both files are byte-identical with the same SHA256. The legacy alias stays+through **v1.8.116**, then disappears. Migrate by then. Recommended pattern:++```bash+# Migration period (v1.8.113 → v1.8.115): try new name, fall back to alias+LINUX_CLI_URL_NEW=$(echo "$MANIFEST" | jq -r '.cli.linux_x86_64.binary_url // empty')+if [ -n "$LINUX_CLI_URL_NEW" ]; then+ curl -fL "$LINUX_CLI_URL_NEW" -o /tmp/ad+else+ # Falling back — likely talking to a pre-v1.8.113 manifest+ curl -fL "https://wiki.adom.inc/api/v1/pages/adom-bridge/files/adom-bridge-linux" \+ -o /tmp/ad+fi+```++Post-v1.8.116, the fallback branch dies; only the canonical name will exist.
skills/adom-bridge-macos/._SKILL.mdadded⋯ 1 unchanged line ⋯
skills/adom-bridge-macos/SKILL.mdadded+2031@@ -0,0 +1,2031 @@+---+name: adom-bridge-macos+description: Use when the user wants to send files to their Mac, control KiCad or Fusion 360, send desktop notifications, or troubleshoot the Adom Bridge connection. Provides CLI tools for bridging the Docker container to the user's local machine. The CLI binary is still named adom-bridge.+---++# Adom Bridge++Bridge between Claude Code (running in an Adom Docker container) and the user's Mac via WebSocket. The product is **Adom Bridge** (formerly named "Adom Desktop"); the CLI binary name is unchanged: `adom-bridge`. This build is macOS-only (Apple Silicon).++**Install surface:** the canonical install page is `adom/adom-bridge-macos` on the Adom wiki. One-line install on the Mac:++```bash+curl -fsSL https://wiki.adom.inc/api/v1/pages/adom-bridge-macos/files/install.sh | sh+```++or download the dmg from that page. Adom Hydrogen also installs Bridge automatically (Hydrogen-managed installs live under `~/Library/Application Support/adom/bridge/`). `adom-bridge setup_desktop` returns the install command and this container's server config.++**Bridge is a menu-bar agent.** A signed-in launch goes straight to the menu bar: no window, no Dock icon. The window is summoned from the menu-bar item (or by launching the app again) for settings, approvals, and sign-in. First run (signed out) shows the sign-in window. Launch-at-login is on by default via a LaunchAgent (`~/Library/LaunchAgents/inc.adom.bridge.plist`). One Adom sign-in is shared with Adom Hydrogen through a shared macOS keychain item; the session survives uninstall and reinstall, and sign-out deletes it everywhere.++**First-time setup?** If the user hasn't installed Bridge yet, see the Setup section below to walk them through installing and connecting the app.++Quick check if desktop is connected:+```bash+adom-bridge ping+```++**Companion skills** (installed alongside this one from the wiki):+- `adom-bridge-kicad` - KiCad bridge: launch editors, open designs, install libraries, run DRC, window capture + keyboard/click automation (`plugins/kicad/SKILL.md`)+- `adom-bridge-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 (installed by `sync_skills`)++## How It Works++```+Claude Code -> adom-bridge <command> -> Relay Server (HTTP :8766) -> WebSocket :8765 -> Adom Bridge App -> KiCad / Fusion 360 / Browser / Shell+```++The `adom-bridge` binary is a single Rust CLI that does everything:+- `adom-bridge serve` -- Start the relay server (WebSocket :8765 + HTTP API :8766)+- `adom-bridge <command> '<json>'` -- Send commands to the desktop app via the running relay++The relay server runs in the Docker container. The Adom Bridge app runs on the user's Mac (as a menu-bar agent) and connects out via WebSocket.++## Starting the Relay Server++The relay must be running before the desktop app can connect. **In a container, start it SUPERVISED** (v1.9.168+):++```bash+adom-bridge serve --supervise+```++**Why not `adom-bridge serve &`?** A backgrounded (`&`) relay is still a child of your shell/session. When your session is torn down (you finish, or the container reaps it), the relay dies with it - and then every connected desktop hits HTTP 500 on `/proxy/8765/` and loops "Reconnecting in 30s" **for hours** until a human restarts it (wiki issue #24, hit live 2026-07-23). `--supervise` **detaches the relay into its own session (reparented to init)** so it survives your session, and starts a watchdog that:+- restarts the relay with **exponential backoff** (2s → … → 5 min cap; a clean run resets it - a broken relay is never bludgeoned), and+- **toasts the connected desktops on every restart** with the reason from the relay log, so the owner finds out something went wrong.++```bash+adom-bridge serve --supervise # detached + self-healing (use this in containers)+adom-bridge serve --supervise --owner AdomLapper # send restart toasts to one desktop+adom-bridge serve --stop # stop the watchdog + relay+```++`serve` (plain, foreground) still works and prints a loud hint telling you it will die with the session - fine for a quick local relay you'll sit and watch. **Caveat:** a full CONTAINER REBOOT still needs a boot hook (cron `@reboot` / the container's init); the watchdog covers session teardown, not host reboot.++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:+```bash+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.++> **As soon as you're connected, run `adom-bridge sync_skills` (v1.9.37+).** Installing the+> adom-bridge core pkg gives you only CORE skills - it does NOT give you a DYNAMIC/cloud+> bridge's skill (kicad, fusion, blender, any third-party). `sync_skills` runs `bridge_list`+> and installs each bridge's own skill pkg into `~/.claude/skills/`, so you can actually+> discover + drive whatever bridges THIS desktop has. (The CLI also runs this automatically+> once/day in the background, but doing it on connect makes the bridge skills available to+> the current session immediately.) Re-run it any time the desktop gains a bridge;+> `adom-wiki pkg update` keeps them fresh.++### Step 1: Check if already connected++```bash+adom-bridge 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++```bash+curl -sf http://127.0.0.1:8766/health+```++If not running (in a container, ALWAYS use `--supervise` so it survives your session): `adom-bridge serve --supervise`++### Step 3: Confirm the machine++Ask the user: **"Is your machine a Mac on Apple Silicon?"**++- **macOS on Apple Silicon** -- proceed. This build of Adom Bridge is macOS-only.+- **Windows / Linux / Intel Mac** -- point them at the Windows product page (`adom/adom-bridge` on the wiki) or tell them support for their platform is separate from this build.++### Step 4: Install & connect++Run `adom-bridge setup_desktop` - it auto-detects this container's relay URL and returns a ready-to-run **`register_command`** plus a fallback `server_config` JSON. Prefer the zero-paste path:++---++**Option 0: Zero-paste via your Adom account (PREFERRED)**++Run the **`register_command`** that `setup_desktop` returned (it's `adom-bridge register_container` with the name + URL filled in). That injects this connection into the **user's Adom cloud profile**. Then the user just:+1. Installs Adom Bridge (the one-liner: `curl -fsSL https://wiki.adom.inc/api/v1/pages/adom-bridge-macos/files/install.sh | sh`, or the dmg from the `adom/adom-bridge-macos` wiki page) and opens it,+2. **Signs in with their Adom account** (first run shows the sign-in window; after that, the app lives in the menu bar),++and the connection appears in their list and auto-connects - **no JSON paste at all**. This is the modern path and the one to try first. (`register_container` needs an Adom cloud container - it uses the mounted account token - and returns `ok:false` on a non-Adom box; if so, use Option A/B below.) One sign-in is shared with Adom Hydrogen via the keychain, so a user who already runs Hydrogen is already signed in.++---++**Option A: Automatic setup via a local agent (paste fallback)**++If the user has **Claude Code** or **Claude Desktop** on the Mac, they can paste this prompt:++> Install Adom Bridge and connect it to my cloud container. (Adom Hydrogen users get Bridge installed and connected automatically; use this prompt when driving the Mac from a cloud container.) Run `curl -fsSL https://wiki.adom.inc/api/v1/pages/adom-bridge-macos/files/install.sh | sh`, then write this JSON to `~/.adom/config.json`:+> ```json+> {"servers":[<server_config JSON>]}+> ```+> Then open "Adom Bridge" from /Applications.++*(Replace `<server_config JSON>` with the actual value from `setup_desktop`.)*++**Option B: Manual setup**++1. Download the dmg from the `adom/adom-bridge-macos` wiki page and drag **Adom Bridge.app** to /Applications+2. Open **Adom Bridge**; sign in when prompted (or skip to paste JSON)+3. Summon the window from the menu-bar item if it isn't already showing+4. Paste this JSON into the text field that says **"Paste server JSON to add ..."** and press **Enter**:+ ```json+ <server_config JSON>+ ```+5. 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++```bash+adom-bridge ping+# Expected: { "echo": "pong", "roundTripMs": ..., "status": "connected" }++adom-bridge status+# Expected: one client with the user's hostname and capabilities++adom-bridge notify_user '{"title":"Hello from Docker!","body":"Your desktop is connected."}'+```++Tell the user what you see. If `ping` succeeds:++> "Your Mac 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: Node.js for browser features++Nothing for the user to do: Bridge manages its own portable Node and Python runtimes (no admin rights, no Homebrew required). If a `browser_*` command ever reports Node missing, call `adom-bridge desktop_install_node '{}'` - it fetches the managed runtime and the bridge picks it up on the next call.++### Common connection issues++| Symptom | Fix |+|---------|-----|+| `ping` returns `"No bridge client connected"` | User hasn't added this container in the Bridge app yet |+| Bridge app shows "disconnected" | Check the URL uses `wss://` (not `ws://` or `https://`), port is `8765` |+| Relay server not running | `adom-bridge serve --supervise` (survives your session; see Starting the Relay Server) |+| Multiple stale connections | `adom-bridge kick_all` -- app auto-reconnects within seconds |+| Bridge app not installed | `curl -fsSL https://wiki.adom.inc/api/v1/pages/adom-bridge-macos/files/install.sh \| sh`, or the dmg from the `adom/adom-bridge-macos` wiki page |+| App seems "not running" but no window | Normal: Bridge is a menu-bar agent with no Dock icon. Look for the menu-bar item; launching the app again summons the window. |++## CLI Tool++```bash+adom-bridge <command> '<json-args>'+```++**Examples:**+```bash+adom-bridge ping+adom-bridge status+adom-bridge browser_open_window '{"sessionId":"dart2","url":"https://example.com"}'+adom-bridge browser_eval '{"sessionId":"dart2","expr":"document.title"}'+adom-bridge browser_screenshot '{"sessionId":"dart2"}'+adom-bridge browser_list_windows+adom-bridge browser_close_window '{"sessionId":"dart2"}'+adom-bridge notify_user '{"title":"Hello","body":"From Docker"}'+adom-bridge shell_execute '{"command":"echo hello"}'+adom-bridge pull_file '{"filePaths":["/Users/john/Downloads/image.png"],"saveTo":"/tmp"}'+```++**Output:** JSON to stdout. Screenshots are auto-pulled to a local cache dir (see "Screenshot cache" below) and the response returns the file path.++**Config location:** The Bridge app stores server config at `~/.adom/config.json`. This is separate from the binary - config survives updates and reinstalls.++## Available Commands++**Get the full structured command list with descriptions, args, and prerequisites:**+```bash+adom-bridge commands # aliases: list_commands, help+adom-bridge --target <name> commands # + THAT box's installed bridges (installed_bridges_on_target)+```+Returns categorized JSON with every command, its required/optional args, return values, prerequisites, and workflow notes. Run this FIRST when you're unsure what a verb is called.++**Two kinds of verbs (v1.9.103 - don't confuse them):**+- **BUILT-IN Bridge-core verbs** are always available on every Adom Bridge, no install, NOT bridges. This is where screenshots, windows, input, accessibility (AX), files, shell, tunnel, update, and notify live. A whole-screen shot is `desktop_screenshot_screen`; one window (with chrome) is `desktop_screenshot_window {"hwnd":N}` or `{"titleContains":"..."}` (get window ids from `desktop_list_windows`; on macOS the `hwnd` field carries the CGWindowID, same arg name for compatibility).+- **BRIDGE verbs** (`kicad_`/`fusion_`/`browser_`/`blender_`/`nbrowser_`…) need that bridge installed - see `installed_bridges_on_target` in `commands`, or run `bridge_list`.++If a verb returns `errorCode:"unknown_verb"`, **read the error's `_hint`** - it carries a fuzzy "did you mean" (e.g. `desktop_screenshot` → `desktop_screenshot_screen`) and tells you whether the right verb is built-in or a bridge. Don't conclude a capability is "a missing bridge" from a guessed verb name; call `commands` and check.++### 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**. The `desktop.apps` object shows:+ - `kicad.installed` / `kicad.version` / `kicad.bridgeRunning`+ - `fusion360.installed` / `fusion360.running` / `fusion360.bridgeRunning` / `fusion360.addinInstalled` / `fusion360.addinConnected`+ - `browser.bridgeRunning`+- `kick_all` -- Force-disconnect all WebSocket clients. Active Adom Bridge apps auto-reconnect within seconds.++#### Programmatic server registration (v1.8.22+)++Three verbs let external apps (Adom Hydrogen's workspace, in particular) register their relay server with adom-bridge without the user pasting JSON into the GUI by hand. Mirrors the GUI's Quick Add bar + connect/disconnect buttons.++- `server_add` -- Upsert a relay server by `name`. If a server with that name already exists, the URL (and optionally authToken) is updated rather than creating a duplicate.+ ```bash+ adom-bridge server_add '{"name":"hydrogen-workspace","url":"ws://localhost:8765","autoConnect":true}'+ # → {ok:true, name, url, id, connected:bool, created:bool, _hint}+ ```+ - `name` (required) - dedup key. Repeat calls with the same name are idempotent.+ - `url` (required) - relay WebSocket URL.+ - `authToken` (optional) - defaults to a dev token (leave unset unless your relay requires one).+ - `autoConnect` (optional, default `true`) - connect right after upsert. Set `false` to add the entry without dialling out.+ - Behavior: same URL + already connected → no churn, returns ok. URL changed + autoConnect=true → disconnect old loop + spawn new one. URL changed + autoConnect=false → disconnect old, leave entry registered but not connected.+- `server_remove` -- Disconnect (if connected) and delete an entry by name. Returns `{ok, removed:bool, wasConnected:bool}`. Idempotent (removing a non-existent entry returns ok with removed:false).+ ```bash+ adom-bridge server_remove '{"name":"hydrogen-workspace"}'+ ```+- `server_list` -- Persisted server list with live connection status: `{servers:[{name, url, id, autoConnect, enabled, connected:bool, clientCount:0|1, status}]}`. `status` is the fine-grained `connected | reconnecting | disconnected` state machine value.++Persists to the same `~/.adom/config.json` the GUI uses, so entries survive a GUI restart. The `ws_client` supervisor (runs every 30s inside the GUI) auto-reconnects entries with `autoConnect:true` on next launch.++Hydrogen-style usage:+```bash+# At Hydrogen container startup+adom-bridge server_add '{"name":"hydrogen-workspace","url":"ws://localhost:8765","autoConnect":true}'++# At Hydrogen container shutdown+adom-bridge server_remove '{"name":"hydrogen-workspace"}'+```++#### Addressing multiple Adom Bridges on one relay (v1.8.133+)++One relay can have **several** Adom Bridges connected at once - e.g. a laptop and an Azure VM, eventually many. Each Bridge has a **name** (its `--client-name` / Settings value, defaulting to the OS hostname). The cloud CLI addresses a specific one, or fans out to all:++- `adom-bridge targets` - list connected Bridges: `{count, targets:[{name, hostname, version, os, connectedAt}]}`. Discover names here first.+- `adom-bridge --target <name> <verb>` - route the verb to that Bridge only. `--target` is a **global** flag (goes before the verb; case-insensitive; matches `clientName`, else `hostname`). `ADOM_TARGET=<name>` sets a default (the flag wins).+- `adom-bridge --target all <verb>` - fan out to **every** connected Bridge; returns `{broadcast:true, targets:[...], results:{<name>:<verb-result>}, summary:{total,ok,failed}}`. A per-Bridge timeout/failure is isolated to that Bridge's entry.++```bash+adom-bridge targets+adom-bridge --target azurevm desktop_screenshot_screen+adom-bridge --target all hydrogen_status # every box at once, results keyed by name+```++**No-target behavior:** with exactly ONE Bridge connected, no `--target` is needed (back-compat). With MULTIPLE connected and no `--target`, the call FAILS with `{error:"ambiguous_target", connected:[...], _hint}` (exit 1) - it won't guess which box. A bad name → `{error:"target_not_found", requested, connected:[...]}` (exit 1). Set each Bridge's name with the `--client-name <name>` launch flag (Hydrogen passes this when spawning the embedded Bridge), the Settings field, or `config.json` `client_name`. Full design + the relay-routing internals: [`skills/MULTI_AD_ADDRESSING.md`](MULTI_AD_ADDRESSING.md).++#### Direct HTTP API for sibling apps (v1.8.25+)++If you're authoring a sibling Tauri app on the same machine (Adom Hydrogen, or any future "Adom-family" app), you can skip the CLI binary entirely and POST commands straight into the running adom-bridge GUI on `127.0.0.1:47200` (was `127.0.0.1:8770` through v1.8.32 - moved to be a better neighbor to Hydrogen on 47080+ and avoid the 8000-range collision risk). The endpoint runs inside the GUI process (loopback-only bind), uses the same dispatcher the WS path uses, and returns the same JSON shape as `adom-bridge <verb>` - including every `_hint` field.++| Method | Path | Body | Returns |+|---|---|---|---|+| `GET` | `/health` | - | `{"ok":true,"service":"adom-bridge"}` cheap probe |+| `GET` | `/status` | - | service banner + version + schema + `directApi.cliRequired` list |+| `POST` | `/command` | `{"app":"<ns>","command":"<verb>","args":{...}}` | The verb's normal payload (200 OK), or `{error, errorCode, _hint}` (4xx/5xx) |++**v1.8.33+ port discovery (sibling apps READ THIS):** Don't hardcode 47200. The GUI may have bound to 47200-47209 instead if the default port was taken (zombie socket, dev instance, third-party collision). Discovery protocol:++1. Read `~/.adom/direct-api-port` - single-line `host:port`, written by the GUI at bind time, removed at graceful shutdown+2. If file missing or its port doesn't `/health`, scan `127.0.0.1:47200..=47209` for any port answering with `{"ok":true,"service":"adom-bridge"}`+3. Validate `service == "adom-bridge"` to disambiguate from other apps that might bind a port in our range++The CLI does this automatically (see `cli/src/direct_probe.rs`). For sibling-app Rust code, see [`skills/DIRECT_API.md`](DIRECT_API.md) for a copy-paste-ready helper.++**Per-verb timeouts (v1.8.31+):** the direct API's command-timeout is no longer a hardcoded 120s - it mirrors the CLI dispatcher's per-verb table (`walk_cloud_tree`/`search_cloud_files`=620s; heavy exports=320s; `bridge_install`/`fusion_start`=300s; default=120s). Caller can override via `args.timeout` (seconds, clamped to 1800).++Example - programmatic server registration without the CLI:++```bash+curl -X POST http://127.0.0.1:47200/command \+ -H 'Content-Type: application/json' \+ -d '{"app":"desktop","command":"server_add","args":{"name":"hydrogen-workspace","url":"ws://localhost:8765","autoConnect":true}}'+# → identical JSON to `adom-bridge server_add '{...}'`, including the _hint+```++**What's safe to send directly:** essentially everything sync (`server_*`, `bridge_list`, `hydrogen_status`, `hydrogen_build_status`, `desktop_list_windows`, `desktop_screenshot_*`, `kicad_*`, `fusion_*`, `browser_screenshot`, `notify_user`, ...) plus async-dispatching verbs that return a job id in <500 ms (`bridge_install`, `hydrogen_build`, `desktop_install_kicad`, ...).++**What requires the CLI fallback:** verbs returning a structured `errorCode:"cli_required"` from this endpoint - currently `pull_file`, `send_files`, `shell_execute`. These use binary streaming or multi-minute approval polling that doesn't fit a single synchronous HTTP request. `GET /status` returns the full list at runtime so callers can branch defensively.++Full integration guide (loopback trust model, recipes for Hydrogen startup/shutdown, retry/backoff patterns, port-discovery code in Rust + TypeScript) lives in [`skills/DIRECT_API.md`](DIRECT_API.md) in this repo, and as a wiki asset attached to `apps/adom-bridge`.++#### Embedded mode - Bridge managed by Adom Hydrogen (v1.8.42+)++When Adom Hydrogen (Hydrogen) manages Bridge, Bridge enters "embedded mode" at boot. Hydrogen owns the visible surface (Bridge's menu-bar item is suppressed), Hydrogen owns the Adom Cloud login (the shared keychain session), Hydrogen owns auto-updates, and Hydrogen spawns Bridge hidden (the window opens on demand via `desktop_window_show`). Standalone Bridge users see zero change - none of the signals fire.++**Detection** - three-signal cascade (any one triggers embedded mode):++1. `--embedded` CLI flag (what Hydrogen passes on every spawn)+2. `ADOM_EMBEDDED=1` env var (backup channel)+3. `~/.adom/embedded.json` marker file (survives Bridge restarts)++**CLI flags Hydrogen passes when spawning Bridge**:++```bash+"Adom Bridge.app/Contents/MacOS/adom-bridge" --embedded --start-hidden \+ --relay-url ws://127.0.0.1:8765 --relay-name adom-hydrogen \+ --session-token <hd's-stored-token>+```++- `--embedded` → enter embedded mode (also writes the marker)+- `--start-hidden` → boot with main window invisible (the normal daemon state; Hydrogen owns the surface and opens the window via `desktop_window_show`). The machine owner can still flip "Always show the window at launch" in Settings (config `always_show_window_on_launch`, default OFF) - a personal-dev-box preference, not the product default.+- `--shell-approval <prompt|1h|24h|permanent>` → Hydrogen's passthrough of its user's shell-approval choice (v1.8.116+, session-only)+- `--relay-url <url>` → upsert this relay via existing `server_add` dedup; default name `adom-hydrogen`+- `--relay-name <name>` → override the default name+- `--session-token <tok>` → already covered by Phase 4 handoff (env / CLI arg / file)++**Docker introspection** - `adom-bridge desktop_embedded_status` returns `{embedded, owner, source, enteredVia, pendingRelayUrl, pendingRelayName, startHidden, markerPath, markerExists}` so cloud-side callers can branch on whether Bridge is standalone or Hydrogen-managed. **v1.8.73+:** `enteredVia` reports `"launch-flag"` (Hydrogen spawned Bridge as a child via `--embedded`) or `"runtime-adopt"` (Bridge was running standalone; Hydrogen adopted it at runtime) - see "Runtime adopt + release" below.++**New verbs Hydrogen uses to drive Bridge** (all also usable standalone):++| Verb | Purpose |+|---|---|+| `desktop_window_show` | Bring Bridge's main window to foreground |+| `desktop_window_hide` | Hide Bridge's main window (app keeps running) |+| `desktop_connect_all` | Spawn ws_loop for every enabled server |+| `desktop_disconnect_all` | Drop every live WS connection (entries stay in config) |+| `desktop_shutdown` | Explicit Bridge termination (stop bridges, then exit) |+| `desktop_embedded_status` | Introspect current embedded state (incl. `enteredVia`) |+| `desktop_embedded_enter` | **v1.8.73+** runtime adopt: standalone → embedded as `runtime-adopt` |+| `desktop_embedded_release` | **v1.8.73+** revert to standalone. **v1.8.77+: always reverts, never exits Bridge** (regardless of `entered_via`) - use `desktop_shutdown` for explicit exit |+| `desktop_logout` | Clear Bridge's `~/.adom/session.json` (Hydrogen's sign-out propagation) |++#### Runtime adopt + release (v1.8.73+) - the LaunchFlag-vs-RuntimeAdopt distinction++Hydrogen has two ways to put Bridge into embedded mode:++1. **Spawn Bridge with `--embedded`** (LaunchFlag) - Hydrogen launches a fresh Bridge process as its child. Bridge enters embedded with `entered_via=launch-flag`. When Hydrogen subsequently dies ungracefully (without calling `/embedded/release` or `desktop_shutdown` first), Bridge's 60s `embedded_monitor` tick notices Hydrogen's control port is dead and **exits Bridge** (the "no hanging chad" rule - no tray-less orphan).+2. **Adopt at runtime via `POST /embedded/enter`** (RuntimeAdopt) - Bridge was already running standalone; Hydrogen calls the direct-API endpoint to flip Bridge into embedded mode without a respawn. Bridge enters embedded with `entered_via=runtime-adopt`, preserving its open state, current PID, all in-flight connections. When Hydrogen subsequently dies ungracefully, Bridge's monitor **reverts to standalone** - menu-bar item restored, footer pill hidden, marker deleted. The Bridge process stays alive.++The rule is simple: Bridge survives its parent unless the spawner explicitly asked otherwise. On ungraceful Hydrogen death Bridge reverts to standalone and keeps serving the relay (it is the machine's recovery channel).++**Two new direct-API endpoints** (v1.8.73+, sugar over `POST /command`):++```bash+# Adopt - runs the same handler as desktop_embedded_enter:+curl -s -X POST http://127.0.0.1:47200/embedded/enter++# Release - runs the same handler as desktop_embedded_release.+# v1.8.77+: ALWAYS reverts to standalone. Bridge never exits via this call.+curl -s -X POST http://127.0.0.1:47200/embedded/release+```++**Probe target (v1.8.75+):** the `embedded_monitor` checks Hydrogen's *control port* (default `47084`, discovered from `~/.config/adom-hydrogen/ports.json` key `control`). Same discovery `hydrogen_api` uses.++**Live frontend update (v1.8.76+):** the "Embedded · Hydrogen" footer pill responds to Tauri `mode-changed` events emitted on every transition. So if Bridge's window is open when Hydrogen adopts it (or Hydrogen releases it), the pill flips immediately - no page reload, no manual refresh.++**Stale-marker safety (v1.8.74+):** a stale `entered_via=launch-flag` marker with no live Hydrogen at boot is cleaned up, so the next launch starts standalone normally.++**Shell auto-approve is a user preference** (v1.8.116+ - replaces the v1.8.44 embedded-permanent default). The safe default everywhere is per-command approval. Resolution order at Bridge boot:++1. `--shell-approval <prompt|1h|24h|permanent>` launch flag - Hydrogen's passthrough of its user's choice when spawning Bridge embedded. **Session-only**: never persisted by Bridge.+2. The durable Settings checkbox ("Auto-approve shell commands permanently", config field `shell_auto_approve_permanent`, default OFF). Also set/cleared via `shell_auto_approve '{"permanent": true|false}'`.+3. A restored timed grant from a previous session (+1h/+24h buttons, `duration_secs` calls).+4. Otherwise: prompt per command.++In embedded mode **Hydrogen owns the preference** (Hydrogen is the parent, Bridge the child): Bridge's Settings checkbox goes read-only with "Managed by Adom Hydrogen", and the footer shows "**Permanent** *(managed by Adom Hydrogen)*" when armed. Hydrogen changes it at runtime with the same verb:++```bash+# revoke everything (timed + permanent + the saved checkbox):+adom-bridge shell_auto_approve '{"duration_secs": 0}'++# set the durable preference (equivalent to ticking the Settings checkbox):+adom-bridge shell_auto_approve '{"permanent": true}'+```++Migration note: pre-1.8.116 embedded boots persisted a permanent grant that leaked into standalone sessions. On first v1.8.116 boot, a permanent-class grant on disk WITHOUT the config preference is cleared - re-opt-in via the checkbox or the verb if you actually wanted it.++**⚠ v1.9.90 - a cloud/relay AI CANNOT grant shell approval itself.** The `shell_auto_approve` examples above are Hydrogen's (a trusted LOCAL caller) or the human's own. From a **relay** connection (the cloud AI), a GRANT (`shell_auto_approve {"permanent":true}` / `{"duration_secs":N>0}`) is REFUSED with `errorCode:"human_only"` and instead POPS the human approval prompt in Adom Bridge. **The AI's path: call `request_shell_approval` (or just let the gate/refusal auto-surface the prompt) → ask the user to click Approve 1 hr / 24 hr / Bypass Permissions → RETRY your original verb.** Only a human clicking in Bridge (or Hydrogen proxying its own user) grants. A REVOKE (`duration_secs:0`) is allowed for the AI.++#### Bridge ports are dynamic - you never need to know one (v1.8.31+)++KiCad, Fusion, Browser/Puppeteer, and all third-party bridges now bind OS-assigned ephemeral ports (not the legacy 8772/8773/8851 you may remember). The runtime port changes every spawn. **Callers never need to know it.**++- The CLI verb namespace (`kicad_*`, `fusion_*`, `browser_*`, plus any third-party `<bridge>_*`) is the contract. Always go through that.+- adom-bridge's direct API forwards to whatever port each bridge is on at the moment of the call.+- `bridge_list` reports `spawn.runtimePort` per bridge for debugging - do NOT hardcode it anywhere.+- Why this changed: Hydrogen's bridges also wanted 8772/8773/8851; we used to collide silently. Dynamic ports = clean coexistence.++### Managing bridges (the AI fully controls bridges on its own)++Bridges are Bridge's plugins (kicad, puppeteer, fusion360 ship bundled; others install from the wiki). You - the AI - can see, update, install, remove, pause, and resume them entirely through verbs. No human step is required.++| Verb | What it does |+|---|---|+| `bridge_list` | Every installed bridge: `name`, `displayName`, `version` (what Bridge runs now), `source` (bundled / cache / third_party), `paused`, spawn kind, verbPrefixes. Start here. |+| `bridge_check_updates` | **READ-ONLY.** Per bridge: `current` vs `latest` (wiki) + `updateAvailable`. Downloads nothing. The "what's behind?" probe. |+| `refresh_bridges` | Apply updates. No args = sync ALL bridges from the wiki. `{"name":"kicad"}` = update just one. |+| `bridge_install` | Install a new third-party bridge from a wiki manifest URL (`{"manifestUrl":"…/<bridge>-bridge-manifest.json"}`). On `success:true` the **capsule appears in the Bridge GUI immediately** (no verb call needed; it's gray/idle until first use, then green). On `success:false` **nothing is registered and no capsule appears** - read `error` (a `read zip body`/`builder error` means the manifest's zip URL couldn't be fetched). Don't treat a parsed manifest as a completed install; confirm with `bridge_list`. The manifest `url` may be relative (resolved against the manifest's dir, v1.8.169+). |+| `bridge_uninstall` | Remove a bridge (`{"name":"…"}`). Bundled ones revert to the bundled copy; third-party ones are fully removed. |+| `bridge_pause` / `bridge_resume` | Flip a routing flag so a bridge's verbs fail-fast with `bridge_paused` (or resume). Does NOT kill the process. |++**Naming convention for published bridges (3rd-party authors, read this):** when you publish a bridge to the Adom wiki, its **page title / catalog descriptive name MUST start with `Adom Bridge - `** (spaced hyphen; the historical catalog prefix that existing bridge pages use) - e.g. `Adom Bridge - Altium Bridge`, matching the bundled bridges (`Adom Bridge - Puppeteer Bridge`, `Adom Bridge - KiCad Bridge`). This keeps every bridge grouped with consistent names in app-index pages instead of scattering as bare slugs. It applies to the **wiki/catalog title only** - NOT `bridge.json` `displayName` (the in-app chip label, which stays tool-descriptive like `Altium Designer`) or `name` (the lowercase slug). Full authoring walkthrough: the `adom-bridge-bridge-sdk` skill (the `adom-bridge-bridges` wiki page) + its fill-in-the-blanks templates.++**Typical "am I on the latest bridges?" flow:**+```bash+adom-bridge bridge_check_updates # read-only: see current vs wiki per bridge+# → {bridges:[{name,current,latest,updateAvailable,source,wikiReachable}], updatesAvailable:N}+adom-bridge refresh_bridges # update ALL that are behind+adom-bridge refresh_bridges '{"name":"kicad"}' # or just one+```++**Version semantics (so the numbers make sense):**+- `current` = what Bridge actually runs = the newer of the writable cache and the bundled copy (version-aware, numeric semver - `1.0.10 > 1.0.9`).+- A bundled bridge that ships AHEAD of the wiki shows `updateAvailable:false` - Bridge correctly keeps the newer bundled copy. **puppeteer is bundled-only** (its node_modules deps exceed the wiki file limit), so it never shows a wiki update; it updates with the Bridge installer.+- After an `updated` action, the bridge SERVER PROCESS still holds the old code in memory until it respawns. `is_bridge_running` is version-aware (v1.8.142+) and forces a respawn on the next verb call, so you normally don't need to kill anything - but if you want it instant, use `bridge_kill '{"name":"..."}'`.++**Auto-update cadence:** a running Bridge also auto-syncs bridges on launch + every 4 hours. `bridge_check_updates` / `refresh_bridges` (or the GUI's **"Check for updates"** button next to "+ Bridge") are the on-demand "I just published, pick it up now" path.++#### Bridge lifecycle - one instance, stable port (v1.8.153+)++A dynamic bridge gets **one stable port** that Bridge reuses across every spawn and across Bridge restarts (persisted in `~/.adom/bridge-ports.json`). A long-lived external client (e.g. a browser extension that dials the bridge) keeps reaching the same port - no chasing a fresh ephemeral port each cycle, no "the client is on bridge A but Bridge routes to bridge B" split. Bridge enforces a **single instance**: it tracks every spawned bridge PID, reaps a stale/wedged one before respawning on the same port, and `bridge_stop_all` reaps orphans too.++`spawn.persistent` (in `bridge.json`) is now surfaced in `bridge_list` (`spawn.persistent`) and means: **Bridge auto-respawns this bridge on death**, on its stable port (a supervisor runs each ~15 s). So "kill the bridge to load new code" → Bridge brings it right back at the same port the client keeps dialing. `bridge_install` / `refresh_bridges` also no longer fail with the cache-dir lock (Bridge reaps the process holding the dir as its cwd, then swaps; in-place merge fallback if a handle lingers).++**v1.8.186/187 - first-class bridges (panel + remote management + optional manifest fields).** The Bridge window has a full-height **Bridges sidebar** that shows each bridge as a mini-app (hero image, LED, rich tooltip); clicking one opens a wide pop-out with a **live console** (its `~/.adom/bridge-logs/<name>.log` + the per-bridge command history), a **ports/process debug view** (live PIDs - `>1` flags a runaway), persistent-vs-one-off + codebase-language badges, and all management actions (install/uninstall/**kill**/restart/pause/resume + "filter activity log to this bridge" + "copy contribution prompt"). The same management is available **remotely** to cloud threads via verbs: `bridge_info`/`bridge_detail` (rich aggregate), `bridge_log_read {name, sinceOffset?}` (**cursor-streaming remote tail -f**), `bridge_kill` (wipe one bridge's processes). As of v1.8.186 **all** bridges write their per-bridge log (bundled kicad/fusion360/puppeteer included, not just dynamic ones). New **optional** `bridge.json` fields (all back-compat, surfaced in the panel): `hero` (a relative filename on your wiki page, or a full URL - your app-icon; absent → a monogram tile), `languages` (override the auto-scan, e.g. `["Python"]`), `releasedAt` (ISO date; absent → bridge.json mtime).++**v1.8.156 - strays + self-documenting hints.** Bridge now tracks every bridge PID it spawns (persisted across Bridge restarts) and **reaps all stray instances** before respawning - so you don't end up with 4 copies of one bridge fighting over the routing. `bridge_list` surfaces `spawn.instanceCount` per dynamic bridge (normally 0 or 1; **>1 = strays piled up** - visible at a glance). If a bridge verb fails to reach a healthy bridge, the error `_hint` tells you to retry / check `instanceCount` / `process_list`; and a verb response right after a (re)spawn carries an `_ad_note` saying the external host reconnects within a few seconds - **retry** instead of treating a transient "not connected" as terminal. For verb discovery from a bridge, `GET <ADOM_DIRECT_API_URL>/commands` lists every reachable verb (see [`skills/DIRECT_API.md`](DIRECT_API.md)).++#### Bridge status chip - self-reported LED + tooltip (v1.8.152+)++Each bridge has a status dot in the Bridge GUI sidebar. **Bridge is a pure renderer of bridge health** - a live bridge decides its own LED color and hover text; Bridge just paints what the bridge reports. This replaced the old scheme where Bridge inferred the LED from whether it was holding a spawn handle, which left fire-and-forget (dynamic/installed) bridges stuck RED even while healthy and routing.++**The contract (all three fields OPTIONAL).** On the endpoint Bridge already polls - your `bridge.json` `healthEndpoint` (default `/health`; the sample bridges use `/status`) - return a 2xx JSON body that MAY include:++| Field | Type | Meaning |+|---|---|---|+| `led` | `"green"` \| `"yellow"` \| `"red"` | The dot color. Green = healthy; yellow = degraded/warning; red = the bridge itself knows it's broken (e.g. lost its upstream). |+| `summary` | string | Short label shown next to the dot. Keep it tiny (≤ ~24 chars) - e.g. `"2 windows"`, `"ext disconnected"`. |+| `tooltip` | string | Full hover text. Newlines allowed; no hard length limit (keep it readable). |++**Rules Bridge enforces:**+- Endpoint returns 2xx **with** `led`/`summary`/`tooltip` → rendered **verbatim**, no parsing or logic.+- Endpoint returns 2xx **without** them → Bridge defaults to **green** (you're reachable) + a generic tooltip. Existing bridges work unchanged - this is opt-in.+- Endpoint **unreachable** → Bridge owns this ONE state: a **gray/offline** dot + Bridge's own tooltip ("Not running" / "Bridge not reachable"). A dead bridge can't self-report, so Bridge never lets a bridge claim "offline" - it detects it.++**Cadence:** Bridge re-polls every ~15 s, and a dynamic bridge flips green the instant it spawns. So a bridge that surfaces live counters (open windows, queue depth, ext-connection state) in `summary`/`tooltip` updates in the GUI within a tick.++Reference implementations: [`scripts/sample-bridges/hello-python/server.py`](../scripts/sample-bridges/hello-python/server.py) and [`hello-rust/src/main.rs`](../scripts/sample-bridges/hello-rust/src/main.rs) both self-report `led`/`summary`/`tooltip` on `/status` - fork either.++### 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 server+ - `targetApp`: "kicad", "fusion360", or "general"+ - `dest`: **absolute** path on the Mac (under a safe root) to land the files exactly where you want (v1.8.148+). Prefer this when you know the target dir.+ - `destinationFolder`: **relative subfolder only** (e.g. `"kicad/symbols"`, `"fusion"`). The Bridge app controls the base directory. **Absolute paths are rejected.** With neither `dest` nor `destinationFolder`, files land in the Mac's **Downloads** folder.+ - Returns `destinationPaths[]` with the absolute path of every saved file, plus (v1.8.168) `savedToDir` (the folder) and a rich `_hint`. **The files land on the MAC, not in your container** - verify with `list_dir`, pull one back with `pull_file`.+ - **`send_files` only COPIES bytes - it does NOT install or register a bridge.** Dropping bridge files with `send_files` will NOT make a capsule appear or verbs route. To add a bridge use `bridge_install '{"manifestUrl":"…/<bridge>-bridge-manifest.json"}'`, then confirm with `bridge_list`.++- `pull_file` -- Pull files from the Mac to the container.+ - `filePaths`: array of absolute paths on the Mac+ - `saveTo`: directory on the container to save files (default: `/tmp`)+ - **Streaming since v1.4.3.** Each file is transferred as 1 MiB binary WS frames straight to disk on the Docker side, with incremental SHA256 verification. The legacy 30s base64-JSON path is gone - large files (50 MB+ datasheets, 75 MB reference manuals) no longer time out. Per-file timeout is 600s.+ - Returns `files: [{name, path, size, sha256, chunks}]`. Use `sha256` to verify the transfer (the desktop side computes it during streaming and the container side verifies on completion; mismatch deletes the partial file and reports failure). `chunks` is the count of 1 MiB binary frames received.+ - When at least one but not all files succeed: `success` is `true`, `errors[]` lists the failures alongside `files[]`. When ALL fail: `success` is `false`.++### Desktop Notifications++- `notify_user` -- Send a native macOS notification (UNUserNotificationCenter, Adom branding). Rich types: action buttons + a round-trip via `notify_response`; `wait:true` blocks until the user answers.+ - `title`, `body`, `level` (info/success/warning/error/emergency), `actions`/`buttons` (label buttons), `id` (correlation id for the round-trip). Some rich fields from the Windows product (toast progress bars, inline inputs) do not exist on macOS; the verb ignores what the platform can't render.+ - Full guide + copy-paste examples per type: the **adom-bridge-notify** skill ([`skills/NOTIFY.md`](NOTIFY.md)).+- `notify_response` -- Poll which notification button the user clicked, by correlation `id`. `{pending:true}` until they click; `{pending:false, action}` once they do (consumed on read). `notify_events` drains the durable click stream.++### KiCad (`kicad_*`) - bridge-owned; light pointer++The KiCad bridge is **cloud-owned** and ships its OWN full skill (wiki pkg `adom/adom-bridge-kicad-bridge`). This page keeps only the pointer: run `adom-bridge sync_skills` after connecting and the full KiCad guide (project workflows, three-tier `kicad_open_*` routing, DRC recipes, library install) lands in `~/.claude/skills/`.++Most-used verbs to get moving:++```bash+adom-bridge kicad_open_board '{"path": "/Users/<you>/projects/board/board.kicad_pcb"}'+adom-bridge kicad_screenshot_all '{}' # every KiCad window incl. error dialogs, as an array+adom-bridge kicad_run_drc '{}'+adom-bridge kicad_window_info '{}'+adom-bridge kicad_open_symbol_editor '{"library": "Device"}'+```++If KiCad is not installed, the bridge knows how to install it for the user - offer, never point them at a download page. Full details: the bridge's own skill.++### Fusion 360 (`fusion_*`) - bridge-owned; light pointer++The Fusion 360 bridge is **cloud-owned** and ships its OWN full skill (wiki pkg `adom/adom-bridge-fusion-bridge`). Run `adom-bridge sync_skills` and the full guide (cloud files, import/export, add-in troubleshooting, APS search) lands in `~/.claude/skills/`.++Most-used verbs:++```bash+adom-bridge fusion_start '{}' # launches Fusion; slow on cold start (minutes)+adom-bridge fusion_get_app_state '{}' # the status/poll verb+adom-bridge fusion_addin_status '{}'+adom-bridge fusion_dismiss_blocking_dialogs '{}'+adom-bridge fusion_send_key '{"key": "Escape"}'+```++Fusion launches take 2-4 minutes cold; respect `timeoutSeconds` + `statusVerb` on responses instead of re-issuing. Full details: the bridge's own skill.++### Launching apps (generic)++Two generic CLI commands exist for launching any executable or app safely - they verify the target exists BEFORE handing it to the OS, so you never trigger a "not found" dialog:++- `adom-bridge find_exe '{"name":"..."}'` -- Resolve a binary by absolute path, glob (newest match wins), or bare name (searches PATH). Returns `{path, source}`. Does NOT launch.+- `adom-bridge launch '{"path":"...", "args":[...], "cwd":"...", "detached":true}'` -- Same resolution rules as `find_exe`, then spawns. Fails in terminal (exit 1) with a clear error if the path doesn't exist. **Always prefer this over a raw shell `open`.**++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 360+- `fusion_open_lbr` -- Open an EAGLE .lbr library file+- `fusion_open_electronics` -- Check if the Electronics workspace is active+- `fusion_electron_run` -- Execute any EAGLE command via Electron.run. Returns rich state: `activeWorkspace`, `activeDocument`, `commandType`, `_hint`, and `fusionOperations` (diff of Fusion's internal operation log showing what actually fired). **Avoid blocking commands** - see list below. **Full EAGLE command reference:** [`skills/eagle-commands.md`](./eagle-commands.md) - popular/safe commands split from blocking/modal ones, with layer reference and chaining syntax.+- `fusion_execute_text_command` -- Low-level app.executeTextCommand() access. Returns the command result plus workspace context.+- `fusion_board_info` -- Get structured board data from the open PCB layout. Returns: component placements (name, package, x, y, rotation), net names, copper traces, layer setup, board thickness, DRC violations. Much richer than a screenshot - gives exact coordinates and connectivity. Requires a .brd board open in PCB Editor.++#### `fusion_electron_run` - EAGLE Command Execution++Executes EAGLE commands inside Fusion 360's Electronics workspace. Works in **Schematic Editor**, **PCB Editor (Board Layout)**, and **Electronics Library** contexts.++**How it works:** Sends the command string via Fusion's `Electron.run` text command. EAGLE's `Electron.run` is fire-and-forget - it never returns output or throws on invalid commands. To compensate, the handler snapshots Fusion's internal operation log (`Diagnostics.RecentOperations`) before and after execution, returning a diff showing what actually fired.++**Usage:**+```bash+# Basic command+adom-bridge fusion_electron_run '{"command": "WINDOW FIT"}'++# Multiple commands in sequence (use semicolons)+adom-bridge fusion_electron_run '{"command": "DISPLAY NONE; DISPLAY 1 16 17 18 20 21"}'++# Navigate in library editor+adom-bridge fusion_electron_run '{"command": "EDIT SOIC8.pac"}'+adom-bridge fusion_electron_run '{"command": "EDIT RESISTOR.sym"}'+adom-bridge fusion_electron_run '{"command": "EDIT MYDEVICE.dev"}'+```++**Response fields:**+- `activeWorkspace` - Current workspace (Schematic Editor, PCB Editor, Electronics Library)+- `activeDocument` - Name of the open document+- `commandType` - Detected type: `view_control`, `layer_control`, `edit`, `design_rule`, etc.+- `editorType` - For EDIT commands: `package`, `symbol`, `device`+- `fusionOperations` - Array of Fusion operations that fired (diff of internal log)+- `hint` - Human-readable description of what happened+- `rawResult` - Raw return from Electron.run (usually empty)++**EAGLE Command Reference - Safe for automation:**++| Command | Context | Description |+|---------|---------|-------------|+| **View / Navigation** | | |+| `WINDOW FIT` | Any | Zoom to fit all content |+| `WINDOW (x1 y1 x2 y2)` | Any | Zoom to specific area (coordinates in current units) |+| `DISPLAY ALL` | Any | Show all layers |+| `DISPLAY NONE` | Any | Hide all layers |+| `DISPLAY 1 16 17 18 20 21` | Board | Show specific layers by number |+| **Grid** | | |+| `GRID MM 0.1` | Any | Set grid to 0.1mm |+| `GRID MIL 25` | Any | Set grid to 25mil |+| `GRID INCH 0.05` | Any | Set grid to 0.05 inch |+| **Board Layout** | | |+| `RATSNEST` | Board | Recalculate airwires (unrouted connections) |+| `RIPUP` | Board | Remove all routed traces |+| `RIPUP *` | Board | Remove all traces (same as RIPUP with no selection) |+| `ROUTE` | Board | Start auto-router |+| `DRC` | Board | Run design rule check |+| `BOARD` | Schematic | Switch to paired board layout |+| `SCHEMATIC` | Board | Switch to paired schematic |+| **Schematic** | | |+| `VALUE value` | Schematic | Set component value |+| `NAME name` | Any | Rename selected element |+| `SMASH` | Any | Detach name/value labels from components |+| **Library Editor** | | |+| `EDIT name.pac` | Library | Open a package (footprint) for editing |+| `EDIT name.sym` | Library | Open a symbol for editing |+| `EDIT name.dev` | Library | Open a deviceset for editing |+| `EXPORT SCRIPT 'path.scr'` | Library | Export entire library as EAGLE script |+| **Scripting / Settings** | | |+| `SET CONFIRM YES` | Any | Suppress confirmation dialogs |+| `SET CONFIRM OFF` | Any | Re-enable confirmation dialogs |+| `SCRIPT 'path.scr'` | Any | Run batch commands from a .scr script file |++**EAGLE Layer Numbers (commonly used):**++| Layer | Name | What it shows |+|-------|------|--------------|+| 1 | Top | Top copper |+| 16 | Bottom | Bottom copper |+| 17 | Pads | Through-hole pads |+| 18 | Vias | Via holes |+| 19 | Unrouted | Airwires (ratsnest) |+| 20 | Dimension | Board outline (required for 3D) |+| 21 | tPlace | Top silkscreen |+| 22 | bPlace | Bottom silkscreen |+| 25 | tNames | Top component names |+| 27 | tValues | Top component values |+| 29 | tStop | Top solder mask |+| 31 | tCream | Top stencil/paste |+| 51 | tDocu | Top documentation |++**Blocking commands - AVOID from automation:**++| Command | Why it blocks |+|---------|---------------|+| `WRITE` | Opens Save As dialog - use `fusion_save_lbr` or `fusion_close_document` instead |+| `ADD` | Opens component picker dialog |+| `SHOW name` | Opens interactive highlight mode |+| `SET` (no params) | Opens settings dialog |+| `GRID` (no params) | Opens grid settings dialog |+| `EDIT new.sym` | Opens "Create new?" confirmation if symbol doesn't exist |+| `CHANGE` | Opens interactive change mode |+| `MOVE` | Opens interactive move mode |++**Tips:**+- Always run `WINDOW FIT` after opening a file or switching views+- Use `DISPLAY NONE` then `DISPLAY <layers>` to show only specific layers+- Combine commands with `;` - e.g., `SET CONFIRM YES; RIPUP *; RATSNEST`+- For board screenshots: `DISPLAY NONE; DISPLAY 1 16 17 18 20 21; WINDOW FIT`+- Use `fusion_board_info` instead of EAGLE commands when you need structured data+- `fusion_export_lbr` -- Export the open Electronics library as an EAGLE .scr script (note: does NOT include 3D package references - those are cloud-linked only)+- `fusion_save_lbr` -- Save the open Electronics library as a .flbr file++#### EAGLE Libraries with 3D Packages++Fusion 360's .lbr format supports `package3d` elements that link footprints to 3D models. Key facts:++- **3D models are cloud-hosted** - each `package3d` has a `wip_urn` (e.g., `urn:adsk.wipprod:fs.file:vf.xxxxx`) pointing to a Fusion cloud document. You cannot embed STEP files directly in .lbr XML.+- **Creating 3D packages requires the Fusion UI** - use `Package3DCreateCmd` in Electronics Library Editor, which opens a new Design workspace where you model/import the 3D shape, then save to link it.+- **`EXPORT SCRIPT` strips 3D references** - the .scr export only contains 2D data (symbols, footprints, devicesets). To preserve 3D links, keep the .lbr XML format.+- **Fusion's built-in examples** have 3D packages - 34 of 37 libraries in the EAGLE examples directory (e.g., `Connector_USB.lbr`, `Resistor.lbr`, `Capacitor.lbr`) include `package3d` references.+- **Example libraries location** (macOS): under the Fusion install's `.../Applications/Electron/LibEagle/examples/libraries/examples/` (find the install with `find_exe` or ask the bridge; the webdeploy hash changes per build)++To open a built-in example library for reference:+```bash+# Find the examples directory first+adom-bridge fusion_execute_text_command '{"command": "Python.RunScript /tmp/find_eagle_libs.py"}'+# Then open one+adom-bridge fusion_open_lbr '{"filePath": "<path>/Connector_USB.lbr"}'+```+- `fusion_open_schematic` -- Open a .sch schematic in Fusion's Schematic Editor. Args: `filePath`.+- `fusion_open_board` -- Open a .brd board layout in Fusion's Board Layout editor. Args: `filePath`.+- `fusion_show_3d_board` -- Switch to 3D PCB board view (must have a .brd open). Board MUST have an outline on layer 20 (Dimension) or 3D generation fails. Auto-zooms to fit after switching.+- `fusion_show_2d_board` -- Switch back to 2D board layout from 3D PCB view. EAGLE commands via `fusion_electron_run` only work in 2D.+- `fusion_close_document` -- Close a document without save dialog. Args: `name` (optional, defaults to active doc), `save` (optional, default false). Essential for automation - avoids modal save dialog that blocks Fusion.+- `fusion_document_info` -- List ALL open documents/tabs with name, type, and active status, plus detailed cloud info for the active document. Returns `openDocuments` array (every tab) and active doc details (cloud project, folder, file ID, version, save status). Lightweight - uses only in-memory data, no cloud API calls. Use this instead of `fusion_walk_cloud_tree` when you just need to know what's open.+- `fusion_activate_document` -- Switch to a specific open document tab. Args: `name` (substring match, case-insensitive), `documentType` ("Electronics", "PCB", "FusionDesign", "Drawing"). Essential for automation - switch between schematic, board, and library tabs without user interaction. If no match found, returns the list of open documents so you can refine.+- `fusion_close` -- Close Fusion 360. **Always call this when done with Fusion 360 commands to clean up.**+- `fusion_dismiss_recovery` -- Dismiss recovery document dialogs (both "Recovered Documents" list and "Open recovery document instead?" prompts). Also relocates recovery files to `~/.adom/recovery/fusion/` for safekeeping.+- `fusion_relocate_recovery` -- Proactively move Fusion crash recovery files to `~/.adom/recovery/fusion/<timestamp>/` without dismissing any dialogs. Call this **before** launching Fusion to prevent recovery dialogs from appearing. Files are preserved (not deleted) so the user can manually restore them if needed.+- `fusion_close_all_documents` -- Close all open documents. Args: `saveChanges` (default: false). Use before force-killing Fusion to prevent recovery files.++#### Fusion 360 UI Interaction++Commands for interacting with Fusion 360's UI - detecting dialogs, taking screenshots, clicking, and sending keyboard input. Essential for handling blocking dialogs and automating CEF-based UI elements.++- `fusion_window_info` -- Returns the Fusion main window HWND, title, rect, and a list of all Qt dialog windows (recovery dialogs, wizards, file pickers). Essential for detecting blocking dialogs before/after operations.+ - Example: `adom-bridge fusion_window_info`+ - Returns: `{hwnd, title, rect, dialogs: [{hwnd, title, className, rect}]}`++- `fusion_screenshot_fusion` -- Captures the Fusion main window or a specific dialog by window id. Background capture (works without bringing to foreground). Saves WebP to `/tmp/adom-bridge-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-bridge fusion_screenshot_fusion` (main window)+ - Example: `adom-bridge fusion_screenshot_fusion '{"hwnd": 12345}'` (specific dialog)++- `fusion_screenshot_all` -- Screenshots the main Fusion window and lists all dialog windows with their HWNDs. Use `fusion_screenshot_fusion {"hwnd": ...}` to capture each dialog.+ - Example: `adom-bridge fusion_screenshot_all`++- `fusion_click_fusion` -- Click at coordinates within the Fusion window or a specific dialog. x/y are relative (0.0-1.0) by default. Set `"relative": false` for pixel offsets. Uses synthetic OS input for CEF dialog compatibility. **Prefer `fusion_send_key` for dialogs** - enter/escape/tab covers most cases.+ - Args: `x` (required), `y` (required), `relative` (optional, default true), `hwnd` (optional - target a specific dialog HWND instead of main window)+ - Example: `adom-bridge fusion_click_fusion '{"x": 0.5, "y": 0.7}'` (main window)+ - Example: `adom-bridge fusion_click_fusion '{"hwnd": 12345, "x": 0.75, "y": 0.85}'` (dialog button)++- `fusion_send_key` -- Send keyboard input to Fusion or a specific dialog via synthetic OS input. **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-bridge fusion_send_key '{"key": "escape"}'` (dismiss CEF dialog)+ - Example: `adom-bridge fusion_send_key '{"key": "enter", "hwnd": 12345}'` (confirm dialog)++#### Fusion 360 Workflow Guide++##### Opening Electronics Projects+1. Open the `.fprj` file: `adom-bridge fusion_open_cloud_file '{"projectName":"Main","fileName":"DRV8411A","fileExtension":"fprj","folderPath":"Molecules/XRP/DRV8411A"}'`+2. Do NOT try to open `.fbrd` or `.fsch` directly - they fail or show a "Select Electronics Design File" dialog+3. Enter the board editor: `adom-bridge fusion_show_2d_board`+4. Enter the schematic editor: After step 3, use `adom-bridge fusion_electron_run '{"command":"EDIT .sch"}'`+5. Switch back to board: `adom-bridge fusion_electron_run '{"command":"EDIT .brd"}'`++##### Auto-Screenshot on Open Commands++**Open commands automatically screenshot Fusion and return the images in the response.** The following commands include `postOpenScreenshot` in their `data` field:+- `fusion_open_cloud_file`, `fusion_open_schematic`, `fusion_open_board`, `fusion_show_3d_board`, `fusion_show_2d_board`++The response `data.postOpenScreenshot` contains:+- `screenshots[]` - array of `{type, savedTo, sizeKB, title?, hwnd?}`. Type is `"main_window"` or `"dialog"`.+- `message` - human-readable instruction to READ each screenshot and check for blocking dialogs+- `dialogBlocking` - `true` if a modal dialog was detected++**After receiving the response, you MUST:**+1. **READ each screenshot file** using the Read tool to visually inspect what Fusion shows+2. Look for blocking dialogs in the screenshots:+ - "What to design?" wizard → dismiss with `fusion_send_key {"key": "escape"}`+ - "PCB out of date" banner → click Update or X+ - "Recovered Documents" → `fusion_dismiss_recovery`+ - "Save changes?" → `fusion_send_key {"key": "escape"}`+ - Any other modal → identify and dismiss+3. **Screenshot again** after dismissing to confirm it's clear++Screenshots are saved as WebP (lossless, ~20-40KB each, downscaled to ≤1568px) for token efficiency. Both the main Fusion window AND all Qt dialog windows are captured separately.++**Why this matters**: Fusion shows Qt dialogs and CEF overlays that are invisible to the API. The `success: true` response from an open command does NOT mean the UI is ready - a dialog may be blocking all further operations. The API cannot detect these. Only a screenshot can.++##### Detecting and Handling Blocking Dialogs+- The auto-screenshot captures dialog windows separately (look for `type: "dialog"` entries in `postOpenScreenshot.screenshots[]`)+- Common blocking dialogs: "What do you want to design?", "Recovered Documents", "Open recovery document instead?", "Select Electronics Design File"+- Dismiss recovery dialogs: `adom-bridge fusion_dismiss_recovery`+- Dismiss CEF dialogs (inside main window): `adom-bridge fusion_send_key '{"key": "escape"}'` or `fusion_click_fusion`+- Dismiss "What do you want to design?" wizard: `adom-bridge fusion_send_key '{"key": "escape"}'`+- For manual screenshot of specific dialogs: `adom-bridge desktop_screenshot_window '{"hwnd": <DIALOG_HWND>}'`++##### Preventing Recovery Documents+- Recovery files are at: `~/Library/Application Support/Autodesk/Autodesk Fusion 360/<USER_ID>/CrashRecovery/`+- Before force-killing Fusion, close all documents: `adom-bridge fusion_close_all_documents '{"saveChanges": false}'`+- **Best practice**: Call `adom-bridge fusion_relocate_recovery` before launching Fusion. This moves recovery files to `~/.adom/recovery/fusion/<timestamp>/` - preserving them for the user while preventing modal dialogs.+- The `fusion_start` command also auto-relocates recovery files before starting Fusion.+- The `fusion_dismiss_recovery` command handles both "Recovered Documents" list and "Open recovery document instead?" prompts, and also relocates files.++##### EAGLE Export Limitations+- **Supported**: `EXPORT IMAGE`, `EXPORT NETLIST`, `EXPORT PARTLIST`+- **NOT supported** (fail silently): `EXPORT DXF`, `EXPORT SVG`, `EXPORT DRILL`+- Always verify export output: the updated `fusion_electron_run` now checks if the output file was created+- Use `fusion_export_bom` and `fusion_export_cpl` for manufacturing data (these use the add-in's XML parser, not EAGLE export)+- Use `fusion_export_gerbers` for Gerber files - produces a ZIP with all gerber layers (GTL, GBL, GTS, GBS, GTP, GBP, GTO, GBO, GKO, XLN). Auto-detects 2-layer vs 4-layer boards.++##### 3D Model Exports+- From the 3D view (activate the .f3d document): `fusion_export_step`, `fusion_export_stl`, `fusion_export_3mf`, `fusion_export_f3d`, `fusion_export_usdz`, `fusion_export_iges`, `fusion_export_sat`+- Switch to 3D view: `fusion_show_3d_board` or activate the .f3d document+- **STEP** - Industry-standard CAD interchange (SolidWorks, CATIA, Creo). Highest geometric fidelity.+- **IGES** - Legacy CAD interchange. Use STEP for modern workflows.+- **SAT** - ACIS solid model format. Used by SolidWorks, SpaceClaim.+- **STL** - Mesh format for 3D printing and visualization. Options: refinement "low"/"medium"/"high".+- **3MF** - Modern 3D printing with color/material support and multi-body.+- **F3D** - Native Fusion 360 archive. Preserves parametric features, sketches, timeline, component refs. Best for archival.+- **USDZ** - **Best for digital twins and GLB conversion.** Preserves full component hierarchy (Board, copper layers, soldermask, Packages), PBR materials, and named nodes. Each component becomes a toggleable node in GLB viewers. Also viewable directly on iOS/macOS (Apple Quick Look / AR).+ - Digital twin pipeline: `fusion_export_usdz` → pull_file → `blender --background --python-expr "import bpy; bpy.ops.wm.usd_import(filepath='board.usdz'); bpy.ops.export_scene.gltf(filepath='board.glb')"`+- **FBX** - Not available in current Fusion builds via the API. The command exists but fails clearly. Use `fusion_export_usdz` instead.+- **DXF/DWG/OBJ/SKP** - Not available via the Fusion API (dialog-only). Commands exist but return clear errors with alternatives.++##### 3D Viewport Captures+- `fusion_take_screenshot` - Capture the Fusion viewport at any resolution without opening a dialog. Uses Fusion's render API (`saveAsImageFile`).+ - Args: `outputPath` (required), `width` (default 1920), `height` (default 1080), `orientation` (optional)+ - Orientations: `home`, `front`, `back`, `top`, `bottom`, `left`, `right`+ - Example: Capture all 6 standard views for use as product images/icons:+ ```bash+ for orient in home front back top bottom left right; do+ adom-bridge fusion_take_screenshot "{\"outputPath\": \"/tmp/3d-${orient}.png\", \"width\": 1920, \"height\": 1080, \"orientation\": \"${orient}\"}"+ done+ ```++##### Electronics Source File Export (`fusion_export_source`)+- `fusion_export_source` - Export the active electronics document as `.fsch`, `.fbrd`, or `.flbr` source file.+ - Args: `outputPath` (required - full path with extension)+ - The extension determines the format: `.fsch` (schematic), `.fbrd` (board), `.flbr` (library)+ - Validates extension, creates output directory, verifies file was created, returns file size++- **Full workflow to export both board and schematic from a cloud project:**+ ```bash+ # Step 1: Open the .fprj (NOT .fbrd/.fsch directly - those fail)+ adom-bridge 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-bridge fusion_screenshot_fusion++ # Step 3: Enter board view (MUST be Board Layout workspace, not 3D)+ adom-bridge fusion_show_2d_board++ # Step 4: Export .fbrd (board first - it's already in board view)+ adom-bridge fusion_export_source '{"outputPath": "/tmp/exports/MyDesign.fbrd"}'++ # Step 5: Switch to schematic (EDIT .s1 = first schematic sheet)+ adom-bridge fusion_electron_run '{"command": "EDIT .s1"}'++ # Step 6: Export .fsch+ adom-bridge fusion_export_source '{"outputPath": "/tmp/exports/MyDesign.fsch"}'++ # Step 7: Pull files to Docker+ adom-bridge pull_file '{"filePaths":["/tmp/exports/MyDesign.fbrd","/tmp/exports/MyDesign.fsch"], "saveTo":"/tmp/exports"}'+ ```++- **Critical gotchas:**+ - You MUST open the `.fprj` first - opening `.fbrd`/`.fsch` directly fails or triggers blocking dialogs+ - You MUST call `fusion_show_2d_board` before exporting `.fbrd` (the Electronics Design overview won't work)+ - Always export `.fbrd` FIRST (from board view), then switch to schematic for `.fsch`+ - If export fails with "file not found", the wrong workspace is active - screenshot to verify+ - After `fusion_open_cloud_file`, always screenshot to check for "Select Electronics Design File" dialog+ - `WRITE` command is blocked - it opens a blocking save dialog. Use `fusion_export_source` instead++- Note: `WRITE` is blocked - it opens a "Version Description" dialog on cloud docs even with a path argument (tested 2026-04-10). Use `fusion_save_to_cloud` to save to cloud, `fusion_export_source` for Fusion-format, or `fusion_export_eagle_source` for plain EAGLE format.++##### Plain EAGLE Source Export (`fusion_export_eagle_source`)+- `fusion_export_eagle_source` - Export the active electronics document as plain EAGLE XML `.sch` or `.brd`.+ - Args: `outputPath` (required - full path ending in `.sch` or `.brd`)+ - Internally: exports `.fsch`/`.fbrd` via `Document.CopyToDesktop`, then extracts the EAGLE XML from the ZIP container, cleans up the temp file.+ - The output is valid EAGLE XML (`<?xml><eagle version="9.7.0">...`) parseable by standalone EAGLE, KiCad import, or any XML tool.+ - Same workflow/prerequisites as `fusion_export_source` - just use `.sch`/`.brd` extensions instead of `.fsch`/`.fbrd`.++ ```bash+ # Board (.brd) - must be in PCB Editor / Board Layout+ adom-bridge fusion_show_2d_board+ adom-bridge fusion_export_eagle_source '{"outputPath": "/tmp/exports/MyDesign.brd"}'++ # Schematic (.sch) - must be in Schematic Editor+ adom-bridge fusion_electron_run '{"command": "EDIT .s1"}'+ adom-bridge fusion_export_eagle_source '{"outputPath": "/tmp/exports/MyDesign.sch"}'+ ```++##### Dialog Dismissal (`fusion_close_window`)+- `fusion_close_window` - Close a specific Fusion dialog by sending WM_CLOSE (equivalent to clicking X).+ - Args: `hwnd` (required - from `fusion_window_info` or `fusion_dismiss_blocking_dialogs` remaining[])+ - Works on dialogs that Escape doesn't close - e.g. "Recovered Documents"+ - Does NOT force-kill - the dialog can still intercept WM_CLOSE++##### Electronics Import (Round-Trip)+- `fusion_import_electronics` - Import Fusion-native `.fsch`, `.fbrd`, or `.flbr` files as new local documents+ - Uses `Document.newDesignFromLocal` under the hood+ - Auto-screenshots after import (catches blocking dialogs)+ - Args: `filePath` (required path on the Mac to .fsch, .fbrd, or .flbr)+ - After import, use `fusion_save_to_cloud` to persist to Fusion cloud+ - **`.fsch` import works standalone** - creates a new schematic project+ - **`.fbrd` import may fail** - boards have external references to schematics/libraries. Error: "New design cannot be created from a local file containing external references"+ - **`.flbr` import works standalone** - creates a new library project+ - For legacy EAGLE files: use `fusion_open_schematic` (.sch), `fusion_open_board` (.brd), `fusion_open_lbr` (.lbr)++##### Library Round-Trip Workflow+Export and re-import Fusion electronics libraries:+```bash+# 1. Open library from cloud+adom-bridge 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-bridge fusion_save_lbr '{"outputPath":"/tmp/library.flbr"}'+adom-bridge fusion_export_lbr '{"outputPath":"/tmp/library.scr"}'+adom-bridge fusion_close_document++# 3. Re-import the .flbr+adom-bridge fusion_import_electronics '{"filePath":"/tmp/library.flbr"}'++# 4. Verify symbols survived round-trip+adom-bridge fusion_export_lbr '{"outputPath":"/tmp/library-verify.scr"}'++# 5. Save to cloud with new name+adom-bridge fusion_save_to_cloud '{"name":"Library-copy"}'+```++##### Demo Projects+171+ electronics molecules are exported in the `adom-bridge-demo` repo (separate from adom-bridge).+Three reference boards with full export formats:++| Board | Cloud Path | Layers | Components | Files |+|-------|-----------|--------|------------|-------|+| DRV8411A | Main / Molecules / XRP / DRV8411A | 4 | 29 | 27 |+| DRV8323SR | Main / Molecules / Experiments / MotorControl / DRV8323SR | 2 | 43 | 27 |+| VL53L8BreakoutMolecule | Main / Molecules / XRP / TimeOfFlightVL53L8 / VL53L8BreakoutMolecule | 2 | 30 | 27 |++Each folder contains: .fsch, .fbrd, bom.csv, cpl.csv, gerbers.zip, 6 board images, 7 3D renders (home/front/back/top/bottom/left/right), STEP, IGES, SAT, STL, 3MF, F3D, USDZ, board screenshot, 3D board screenshot.++#### Cloud Document Management++Manage Fusion 360 cloud documents (hub projects, files, versions). Required for 3D package workflows since EAGLE library 3D models are stored as cloud documents.++- `fusion_save_to_cloud` -- Save the active Fusion document to the cloud. Args: `name` (required), `projectName` (optional, defaults to active project), `folderPath` (optional), `description` (optional). Returns: `fileId`, `versionNumber`, `wipUrn` (if cloud-hosted).+- `fusion_list_cloud_projects` -- List all cloud projects in the user's hub. Returns array of `{name, id}` per hub.+- `fusion_list_cloud_files` -- List files in a cloud project/folder. Args: `projectName` (optional), `folderPath` (optional). Returns: `files` array with `{name, id, versionNumber, fileExtension, dateModified}` and `subfolders` array.+- `fusion_create_cloud_folder` -- Create a folder in a cloud project. Args: `folderName` (required), `projectName` (optional), `parentPath` (optional). Returns `folderId`. Idempotent - returns existing folder if it already exists.+- `fusion_check_recovery` -- Check if a cloud file has a recovery document from a previous crash. Args: `fileName` (required), `projectName` (optional), `folderPath` (optional). Returns `hasRecovery: true/false`. Use this BEFORE opening files to avoid the blocking "Open recovery document instead?" dialog. If recovery exists, call `fusion_open_cloud_file` with `recovery: "open"` (restore unsaved work) or `recovery: "discard"` (delete recovery, open cloud version).+- `fusion_open_cloud_file` -- Open a cloud file in Fusion by name. **If a recovery document exists and no `recovery` arg is given, the command STOPS and reports the recovery instead of opening** - you must decide whether to preserve or discard unsaved work. Args: `fileName` (required), `projectName` (optional), `folderPath` (optional), `recovery` ("open" = restore unsaved work, "discard" = delete recovery and open cloud version).+- `fusion_export_cloud_file` -- Export the active Fusion document to a local file for transfer back to Docker. Args: `outputPath` (required), `format` (optional, default "step"). The exported file can then be pulled back to Docker via `pull_file`.+- `fusion_delete_cloud_file` -- Delete a cloud file by name. Args: `fileName` (required), `projectName` (optional), `folderPath` (optional). File must not be open in Fusion - close it first with `fusion_close_document`.+- `fusion_walk_cloud_tree` -- **Long-running.** BFS walk of a cloud folder tree. Returns a flat list of all files and folders. Runs entirely on the Fusion main thread - **blocks all other add-in commands** until done (check progress with `fusion_addin_status`).+ - Args: `projectName` (optional), `folderPath` (optional, starting folder), `maxDepth` (default 10), `maxFolders` (default 500), `extensions` (optional list, e.g. `["f3d","fprj"]`), `nameContains` (optional substring filter), `includeFiles` (default true)+ - Returns: `{project, rootFolder, folders[], files[], stats: {foldersVisited, foldersSkipped, filesFound, maxDepthReached, truncated}}`+ - **Per-folder timeout (30s):** If a single folder's cloud API calls take >30s (common for large projects), the folder is skipped and counted in `foldersSkipped`. This prevents indefinite hangs.+ - **Progress tracking:** While running, `fusion_addin_status` returns `walkProgress` with `{foldersVisited, filesFound, currentFolder, queueSize}` - poll this every 1–10s to monitor progress (see "Live folder progress streaming" below).+ - **Non-blocking alternative:** Use `fusion_search_cloud_files` for targeted searches, or `fusion_list_cloud_files` for single-folder listings (these are faster but don't recurse).+ - Example: `adom-bridge fusion_walk_cloud_tree '{"projectName":"Main","folderPath":"Molecules","nameContains":"DRV","extensions":["fprj"]}'`++#### Live folder progress streaming with `watch`++For long walks, use the built-in `watch` wrapper. It spawns the inner search command on a worker thread, polls `fusion_addin_status` internally, and emits one JSON event per line to stdout as the walker visits each folder. **No manual polling loop needed.**++```bash+adom-bridge watch '{"command":"fusion_walk_cloud_tree","args":{"projectName":"Main","folderPath":"Molecules","nameContains":"BQ25792"}}'+```++Output is one JSON object per line, three event types:++```jsonl+{"event":"started","command":"fusion_walk_cloud_tree","args":{...},"interval":2,"_hint":"streaming progress events follow, one per line, ending with 'complete' or 'error'"}+{"event":"progress","busyCommand":"walk_cloud_tree","elapsedSeconds":15.0,"foldersVisited":14,"queueSize":50,"filesFound":0,"currentFolder":"Molecules/Sensing"}+{"event":"progress","busyCommand":"walk_cloud_tree","elapsedSeconds":23.4,"foldersVisited":22,"queueSize":108,"filesFound":0,"currentFolder":"Molecules/Examples"}+{"event":"progress","busyCommand":"walk_cloud_tree","elapsedSeconds":31.8,"foldersVisited":30,"queueSize":100,"filesFound":0,"currentFolder":"Molecules/RAPID NAME TAGS/Connor Wood"}+... (one per real change in walkProgress, deduped) ...+{"event":"complete","result":{"folders":[...],"files":[...],"stats":{"foldersVisited":169,"filesFound":12,"truncated":false}}}+```++Read stdout line-by-line. Stop when you see `event:complete` or `event:error`. The full final result is in the `complete` event's `result` field.++**Optional `interval` arg** (default 2s, clamped to [1, 30]):++```bash+adom-bridge 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 substring search on file names across cloud folders. v1.0.2+ of the add-in adds: per-folder timeout, doEvents() every 50 files, per-file try/except, iterative BFS, and a stale-lock watchdog - Fusion stays responsive throughout (no "Not Responding" freeze).+ - **Args (no hard upper caps in v1.0.2+):** `query` (required, substring, case-insensitive), `projectName` (optional, defaults to active), `folderPath` (optional starting subfolder - **narrow with this**), `recursive` (default false), `maxDepth` (default 2), `maxFolders` (default 10), `maxResults` (default 20), `folderTimeout` (default 30s), `searchTimeout` (default 120s), `timeout` (HTTP envelope, default 620s).+ - **Before calling:** if the user knows roughly where the file is (e.g. customer-named folder), **ask them**. Naming a subfolder cuts 5-40 min searches down to ~10s.+ - **Why slow:** Autodesk's free Fusion 360 Python API has no indexed file-search endpoint. We walk the Data API one folder at a time - each = one HTTP round-trip to Autodesk. **This is an Autodesk API limitation, not adom-bridge's.** When telling the user the search is taking a while, attribute it to Autodesk / Fusion 360, not to adom-bridge's bridge. **There is now a FAST native alternative - `aps_search` (see "APS - fast indexed cloud search" below).** It hits Autodesk Platform Services' indexed Data Management API and returns ~instantly, even when Fusion is CLOSED. Call `aps_status` first; if `configured:true`, prefer `aps_search` and only fall back to this folder-walking verb when APS isn't set up.+ - **Interpreting results (critical for no-false-negatives):** the response contains `searchComplete` (bool), `foldersSkipped`, `filesSkipped`, `truncated`, `folderLimitReached`, `searchTimedOut`. `searchComplete:true` ONLY when **all five** are clean - that's the confidence flag. If `searchComplete:false`, the search hit a cap before exhausting the scope; **don't report "file not found"** - re-run with broader caps OR narrower `folderPath`.+ - **Case sensitivity:** fully case-insensitive both directions (query and file names lowercased). Substring match, not whole-word - `"cosm"` matches `COSMIIC`, `COSMOCOIL`, etc.+ - **Cost feedback in response:** `costAnalysis: {elapsedSeconds, foldersPerSecond, estimatedSecondsPer100Folders}` so the AI can budget the next search realistically.+ - **Real numbers from a live test:** searching `Main/Molecules` recursively (173 folders deep, max 8 depth) for "cosmiic" returned in 168 s with `searchComplete:true, totalFound:3`. Fusion stayed `main_thread:responsive` throughout.+ - **Pair with `watch`** for streaming progress updates: see below.++#### APS - fast indexed cloud search (v1.8.182+)++`aps_search` is a **native, ~instant** alternative to the folder-walking `fusion_search_cloud_files`. It queries **Autodesk Platform Services** (APS, formerly Forge) **Data Management API**, which has a real server-side index - so a search that takes minutes by folder-walking returns in well under a second, and it works **even when Fusion 360 is closed** (it's pure HTTPS, no bridge required). adom-bridge runs its own 3-legged **PKCE** OAuth, so the user's APS **client secret never leaves their machine**.++**One-time setup (user side):**+1. Register a free app at <https://aps.autodesk.com> → enable the **Data Management API**.+2. Set the app's **Callback URL** to exactly `http://localhost:8910/aps/callback`.+3. Put the **Client ID** in Bridge config key `aps_client_id` (in `~/.adom/config.json`) or via Settings.+4. Run `aps_auth` once - it opens the system browser for Autodesk sign-in. Tokens cache in `~/.adom/aps_tokens.json` and auto-refresh; you won't be prompted again.++**Verbs:**+- `aps_status` - **call this first.** Non-prompting. Returns `{configured, signedIn}`. `configured:false` → no client id; use `fusion_search_cloud_files`. `configured:true, signedIn:false` → run `aps_auth`. Both true → `aps_search` is ready.+- `aps_auth` - interactive sign-in (opens browser). One-time per device.+- `aps_hubs` - `{hubs[]{id,name}}` (your Autodesk teams).+- `aps_projects {hubId?}` - `{hubId, projects[]{id,name}}` (default: first hub).+- `aps_search {query, projectName?, projectId?, hubId?}` - `{query, hubId, count, results[]{name,id,type,lastModified,project}}`. `query` is a case-insensitive substring of the file's displayName. Without `projectName`/`projectId` it searches up to 8 projects in the hub. Open a hit with `fusion_open_cloud_file {fileName, projectName}`.++**Decision flow for an AI doing a cloud file search:** `aps_status` → if `configured` (sign in with `aps_auth` if needed) use `aps_search`; else fall back to `fusion_search_cloud_files` and tell the user that registering a free APS app (above) would make future searches instant.++**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: workspace → Fusion Cloud → workspace**++```bash+# === workspace to Fusion Cloud ===+# 1. Send file from Docker to the Mac+adom-bridge send_files '{"files": [{"path": "/home/user/component.step"}]}'+# 2. Import into Fusion+adom-bridge fusion_import_step '{"filePath": "/Users/<you>/Downloads/component.step"}'+# 3. Save to cloud (gets a wip_urn for 3D library linking)+adom-bridge fusion_save_to_cloud '{"name": "my-component", "projectName": "Personal"}'++# === Fusion Cloud to Docker ===+# 1. Find and open the cloud file+adom-bridge fusion_walk_cloud_tree '{"projectName": "Personal", "nameContains": "my-component"}'+adom-bridge fusion_open_cloud_file '{"fileName": "my-component", "projectName": "Personal"}'+# 2. Export to local filesystem+adom-bridge fusion_export_cloud_file '{"outputPath": "/tmp/export/my-component.step", "format": "step"}'+# 3. Pull back to Docker+adom-bridge pull_file '{"path": "/tmp/export/my-component.step"}'++# === Cloud management ===+adom-bridge fusion_list_cloud_projects '{}'+adom-bridge fusion_create_cloud_folder '{"folderName": "Electronics", "projectName": "Main"}'+adom-bridge fusion_list_cloud_files '{"projectName": "Main", "folderPath": "Electronics"}'+adom-bridge fusion_delete_cloud_file '{"fileName": "old-file", "projectName": "Main"}'+```++#### Manufacturing Exports (Gerbers, BOM, CPL)++Export manufacturing files from an open PCB board - everything needed to fabricate boards and assemble components via Adom's PCBA service. **All manufacturing commands require a .brd board open in PCB Editor** (use `fusion_open_board` or `fusion_show_2d_board`).++**Recommended workflow order:** `detect_layers` → `set_design_rules` → DRC → `export_gerbers` → `export_bom` → `export_cpl` → `pull_file` all back to Docker.++Every manufacturing command returns structured JSON with:+- `message` - AI-oriented summary explaining what was produced and why it matters+- `nextSteps[]` - ordered list of what to run next in the manufacturing pipeline+- `hint` - tips for interpreting results or recovering from issues+- `data` - structured metadata (paths, counts, layer info) for programmatic use++**Decision guide - which command to run:**+- **Don't know the layer count?** → Run `fusion_detect_layers` first+- **Need to check if the board meets fab specs?** → Run `fusion_set_design_rules` then DRC+- **Ready to generate fab files?** → Run `fusion_export_gerbers`, then `fusion_export_bom`, then `fusion_export_cpl`+- **Need visual review before export?** → Run `fusion_export_board_image` with preset `assembly_top` or `fabrication`+- **Something failed?** → Check `data.hint` and `data.recoverySteps` in the error response++**Commands:**++- `fusion_detect_layers` -- Detect if the open board is 2-layer or 4-layer. Uses ULP script (primary) and CAM comparison (fallback). Returns `layerCount`, `copperLayers[]`, `method` (how it was detected), and `nextSteps[]`. **Run this first** - all other manufacturing commands auto-detect layers too, but running this explicitly gives you the data before committing to exports.++- `fusion_set_design_rules` -- Apply Adom's JLCPCB-derived design rules (.edru XML files) to the open board. Auto-detects 2-layer vs 4-layer and loads the appropriate rule set. Returns `description` (human-readable rule summary), `edruFile`, and `nextSteps[]`.+ - `action`: `"apply"` (default) loads rules into the board; `"export"` saves current board rules to a file; `"show"` displays rule capabilities without modifying anything+ - `layers`: `"auto"` (default), `"2"`, or `"4"` - force a specific rule set+ - After applying: run `fusion_electron_run '{"command": "DRC"}'` to check for violations. DRC markers appear in the board editor.++- `fusion_apply_instapcb_rules` -- Convenience wrapper that applies the bundled Adom InstaPCB design rule sets without needing a local `.edru` file. Args: `layers` (`"2"` or `"4"`). Sends the bundled `.edru` from Docker to the Mac automatically and loads it via the EAGLE `drc load` command. Use this instead of `fusion_set_design_rules` when you just want Adom's standard 2-layer or 4-layer InstaPCB rules.++- `fusion_load_design_rules` -- Generic loader for ANY custom `.edru` file by path. Args: `filePath`. Use this for third-party fab vendor rules (JLCPCB, PCBWay, OSHPark, etc.) that the user has on disk.++- `fusion_export_gerbers` -- Export Gerber (RS-274X) + Excellon drill files as a ZIP. Auto-detects 2-layer vs 4-layer and selects the correct JLCPCB-compatible CAM job. Returns `zipPath`, `zipSizeKB`, `files[]` (list of gerber files in the ZIP with sizes), `layerCount`, and `nextSteps[]`.+ - `outputDir`: directory for the output ZIP (default: `/tmp/adom-gerbers/`)+ - `boardName`: prefix for the ZIP filename (default: from active document name)+ - `layers`: `"auto"` (default), `"2"`, or `"4"` - force CAM job selection+ - Output ZIP contains: GTL, GBL (copper), GTS, GBS (solder mask), GTP, GBP (paste), GTO, GBO (silkscreen), GKO (outline), XLN (drill). 4-layer boards also get G1, G2 (inner copper).++- `fusion_export_bom` -- Export Bill of Materials as CSV. Groups identical parts by value+package with quantity counts. Returns `componentCount`, `uniquePartCount`, and `nextSteps[]`.+ - `outputPath`: (default: `/tmp/adom-bom.csv`)+ - `grouped`: `true` (default) groups by value+package; `false` lists every component individually+ - Output columns: Comment, Designator, Footprint, Quantity, Library - compatible with JLCPCB, PCBWay, Mouser, Digi-Key.++- `fusion_export_cpl` -- Export Component Placement List (pick-and-place) as CSV. Returns `totalPlacements`, `topCount`, `bottomCount`, and `nextSteps[]`.+ - `outputPath`: (default: `/tmp/adom-cpl.csv`)+ - `side`: `"all"` (default), `"top"`, or `"bottom"` - filter for single-sided assembly+ - Output columns: Designator, Mid X, Mid Y, Layer, Rotation - coordinates in mm.++- `fusion_export_board_image` -- Export PNG image of the board with layer presets. Returns `fileSize`, `preset`, and `nextSteps[]`.+ - `outputPath`: (default: `/tmp/adom-board.png`)+ - `dpi`: resolution (default: 300, max: 600)+ - `preset`: layer preset name (see table below)+ - `layers`: custom layer numbers as int array - overrides preset+ - `monochrome`: `true` for black & white+ - `listPresets`: set `true` to get available presets instead of exporting++**Layer presets for `fusion_export_board_image`:**++| Preset | Layers | Description |+|--------|--------|-------------|+| `all` | All | Every layer visible |+| `top_copper` | 1, 17, 18 | Top copper + pads + vias |+| `bottom_copper` | 16, 17, 18 | Bottom copper + pads + vias |+| `top_silkscreen` | 21, 25 | Top silkscreen + component names |+| `bottom_silkscreen` | 22, 26 | Bottom silkscreen + component names |+| `top_soldermask` | 29 | Top solder mask openings |+| `bottom_soldermask` | 30 | Bottom solder mask openings |+| `top_paste` | 31 | Top paste/stencil openings |+| `bottom_paste` | 32 | Bottom paste/stencil openings |+| `board_outline` | 20 | Board outline (dimension layer) |+| `drill` | 44, 45, 17, 18 | Drill holes + vias |+| `assembly_top` | 1, 17, 18, 20, 21, 25, 51 | Top assembly - copper + silk + outline |+| `assembly_bottom` | 16, 17, 18, 20, 22, 26, 52 | Bottom assembly - copper + silk + outline |+| `fabrication` | 1, 16, 17–20, 21, 22, 25, 26, 29, 30, 51 | Full fabrication view |++**Adom InstapcbPCB manufacturing capabilities (used by `fusion_set_design_rules`):**++| Parameter | Metric | Imperial |+|-----------|--------|----------|+| Layers | 1, 2, 4, 6 | - |+| Min trace width | 0.08mm | 3 mil |+| Min trace spacing | 0.08mm | 3 mil |+| Min via drill | 0.2mm | 8 mil |+| Min silkscreen text | 0.1mm | 4 mil |+| Board edge clearance | 0.05mm | 2 mil |+| Board thickness | 1.6mm | 63 mil |+| Copper weight | 0.5 oz (17.5 μm) | - |+| Solder mask | Green | - |+| Surface finish | HASL / ENIG | - |+| Turnaround | 4 hours (fab + assembly) | - |++**Manufacturing workflow - Fusion to Adom PCBA:**++```bash+# 1. Open the board+adom-bridge fusion_open_board '{"filePath": "C:/projects/myboard.brd"}'++# 2. Detect layer count (auto-selects 2-layer or 4-layer rules/CAM)+adom-bridge fusion_detect_layers++# 3. Apply Adom design rules for the detected layer count + run DRC+adom-bridge fusion_set_design_rules '{"action": "apply"}'+adom-bridge fusion_electron_run '{"command": "DRC"}'++# 4. Export manufacturing files (gerbers auto-select correct CAM job)+adom-bridge fusion_export_gerbers '{"outputDir": "/tmp/mfg"}'+adom-bridge fusion_export_bom '{"outputPath": "/tmp/mfg/bom.csv"}'+adom-bridge fusion_export_cpl '{"outputPath": "/tmp/mfg/cpl.csv"}'++# 5. Export board images for review+adom-bridge fusion_export_board_image '{"outputPath": "/tmp/mfg/top-copper.png", "preset": "top_copper"}'+adom-bridge fusion_export_board_image '{"outputPath": "/tmp/mfg/assembly-top.png", "preset": "assembly_top"}'+adom-bridge fusion_export_board_image '{"outputPath": "/tmp/mfg/board-outline.png", "preset": "board_outline"}'++# 6. Pull files back to Docker for Adom PCBA ordering+adom-bridge pull_file '{"path": "/tmp/mfg/bom.csv"}'+adom-bridge pull_file '{"path": "/tmp/mfg/cpl.csv"}'+```++### Desktop Tools++- `desktop_open_folder` -- Open a file or folder in Finder. If given a file path, Finder opens with the file revealed (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"` (the macOS default browser - could be Safari, Chrome, Firefox, Brave; whatever they set), `"chrome"`, `"edge"`, `"firefox"`, `"brave"`).+ - Examples:+ - `adom-bridge desktop_open_url '{"url":"https://claude.ai/"}'` -- opens in user's default native browser, already signed in+ - `adom-bridge desktop_open_url '{"url":"https://docs.example.com","browser":"edge"}'` -- force native Edge+ - Returns `{ok, browser, url, exePath?}`. Allowed schemes: http, https, mailto, ftp, ftps. Other schemes refused.++#### `desktop_open_url` vs `browser_open_window` - two completely different browsers++| | `desktop_open_url` | `browser_open_window` |+|---|---|---|+| **What browser** | Native OS browser the user uses daily - Safari / Google Chrome / Microsoft Edge / Firefox / Brave (whichever is the macOS 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` (defaults to Chrome for Testing). AI-driven automation that **must** be the user's real, signed-in profile (GCP Console with org/projects, internal SSO behind a managed-Chrome policy, anything Context-Aware Access blocks for non-enrolled browsers) → `browser_open_window` with the new `nativeBrowser` + `nativeProfile` args (v1.0.5+, see below).++#### When pup means "the user's real Chrome/Edge" - `browser_open_window` native mode (v1.0.6+, smart flow v1.0.11)++Default `browser_open_window` (no native args) launches Chrome for Testing - same as day one. **Never auto-switch a plain pup request to native.** "Open this URL in pup", "screenshot this", "click that button" all stay on CfT.++Native mode drives the user's **REAL browser** - their actual signed-in profile, with live cookies, org policies, and device enrollment intact (NOT a clone - v1.0.6 replaced the old clone approach). Opt-in via TWO additive args from `desktop_list_browsers`:++```bash+adom-bridge desktop_list_browsers # installed browsers + profiles (+ the signed-in account per profile)+adom-bridge browser_open_window '{+ "sessionId":"gcp",+ "nativeBrowser":"chrome", # chrome / edge / brave+ "nativeProfile":"Default", # profile id: Default / Profile 1 / Profile 2+ "url":"https://console.cloud.google.com"+}'+```++**The smart flow.** CDP (the protocol pup drives) needs the browser running with `--remote-debugging-port`, which can only be set at launch. So the bridge handles each situation differently and tells you which happened:++| Situation | What Bridge does | Response |+|---|---|---|+| Already driving that browser+profile | Reuse the live connection | `ok` + `narrative` |+| Running **with** a debug port (power-user launched it that way, or Bridge did earlier) | Connect, no relaunch | `ok` + `narrative` |+| **Not running at all** | **Launch it fresh with the debug port - NO prompt** (nothing to disrupt) | `ok`, `launchedFresh:true` + `narrative` - just proceed |+| **Open without** a debug port | **Ask the user** - Bridge must close+reopen it | `errorCode:"native_relaunch_required"` + `confirmArgs` + `alternatives` |+| Debug port **refuses to bind** (enterprise policy) | Won't close it again | `errorCode:"native_cdp_blocked"` + `diagnosis` + `alternatives` |++**Handling the two consent/blocked codes:**++- **`native_relaunch_required`** - the browser is open without the port, so driving it means closing + reopening it (with `--restore-last-session`, so all tabs/windows come back). **Ask the user's permission**, then re-call with `"nativeRelaunch":true`. The response's `alternatives` lists other installed browsers you could offer instead.+- **`native_cdp_blocked`** - the debug port won't open, almost always an **enterprise policy** (`RemoteDebuggingAllowed` disabled via Google Workspace / Chrome cloud management - affects **Chrome browser-wide, every profile**). `diagnosis` says whether it's a confirmed local policy or a likely cloud-managed one. Bridge will **not** close that browser again. `alternatives` names other installed browsers - **Edge usually escapes Google management**, so it often drives natively when Chrome can't. Offer the user the alternative, or fall back to generic CfT.++**Always available - generic CfT.** `browser_open_window` with NO `nativeBrowser`/`nativeProfile` drives a clean Chrome-for-Testing instance: no user session, no managed policy, fully controllable. If a task needs the user's logged-in session there, have them sign in once. This is the universal fallback whenever native is blocked.++**Power-user shortcut:** if the user launches their browser at startup with `--remote-debugging-port=<N>`, Bridge reconnects every session with no prompt at all (a productized opt-in "keep native browsers debug-ready at boot" is on the roadmap, default OFF for security).++**Routing rule - DON'T auto-escalate.** Treat native as opt-in:++| User says | What to do |+|---|---|+| "pup" / "open in pup" / screenshot / eval / debug | DEFAULT pup (CfT). Never mention profiles. |+| "native chrome" / "my real browser" / "my work profile" / "my `<account>` profile" | Native. Call `desktop_list_browsers`. If exactly one profile clearly fits the request, use it and say which one ("opening in your `[email protected]` profile on Chrome"). If multiple profiles could fit (e.g. a `google.com` URL with both `[email protected]` and `[email protected]` profiles signed in), **ASK** ("Which profile - work `[email protected]` or personal `[email protected]`?") and then drive that pair. |+| anything else | Default pup. |++**Anti-fingerprint:** native sessions launch without `--enable-automation` and with `--disable-blink-features=AutomationControlled`. `navigator.webdriver === false`. No "controlled by automated software" banner. Sites that probe for automation see a normal browser.++**Firefox** appears in `desktop_list_browsers` but is rejected for native drive with `errorCode:"firefox_no_cdp"` - Firefox doesn't speak the Chrome DevTools Protocol, so puppeteer can't drive it. If the user wants Firefox, hand off via `desktop_open_url` instead.++**Native-mode error codes:**+- `native_relaunch_required` - browser open without a port; ask the user, re-call with `nativeRelaunch:true` (see the smart-flow table above).+- `native_cdp_blocked` - debug port policy-blocked; read `diagnosis`, offer `alternatives` or generic CfT (see above).+- `firefox_no_cdp` - caller passed `nativeBrowser:"firefox"`. Firefox doesn't speak CDP; hand off via `desktop_open_url`.+- `native_browser_not_installed` - that browser isn't installed on this machine.+- `native_user_data_missing` - installed but never launched, so the User Data dir doesn't exist yet. Ask the user to open it once.+- `native_profile_not_found` - `nativeProfile` isn't a real profile id under this browser's User Data. Call `desktop_list_browsers` to see what's valid.++#### Common footgun - `localhost` / `file://` URLs from Docker AIs++If you're running this CLI from a remote Docker container (a cloud relay, dartv4, etc.), `browser_open_window` / `browser_open_tab` / `browser_navigate` will load the URL inside the USER's local pup - NOT inside your container. So a URL like `http://localhost:8080/foo.html` resolves to the user's machine, not your container's service.++**v1.7.10+ detects this.** When you pass a URL whose host is `localhost`, `127.0.0.1`, `0.0.0.0`, `::1`, or scheme is `file://`, the response includes a `_hint` field explaining the issue and suggesting the public-slug URL pattern (`https://<user>-<repo>-<suffix>.adom.cloud/proxy/<port>/<path>`). **The call still proceeds** - rare legitimate cases exist where the user actually does have something running on their localhost.++**Recipe - load something from inside your container into pup:**++```bash+# 1. Start your service inside the container (e.g. a local HTML viewer):+python3 -m http.server 8786 --bind 0.0.0.0 &++# 2. Ask your USER for the container's adom.cloud slug suffix+# (look at their browser URL when they open hydrogen or claude.ai/code).+# Slug looks like: <username>-<container>-<random-id>++# 3. Open the public URL - NOT localhost:+adom-bridge browser_open_window '{+ "sessionId":"demo",+ "url":"https://<your-container-slug>.adom.cloud/proxy/8786/foo.html"+}'+```++If you don't have the suffix handy, just ask the user. Or if the file you wanted to show pup is one your container already wrote to disk, push it to a public location (wiki asset, S3) and load from there.+++- `desktop_bring_to_front` -- Bring a window to the foreground by window id (`hwnd`) or title substring. Activates the owning app and raises the window. Preserves maximized state.+ - Args: `hwnd` (int) OR `titleContains` (string, case-insensitive). One required.+ - Example: `adom-bridge desktop_bring_to_front '{"titleContains": "Fusion"}'`+- `desktop_set_window_state` -- Change a window's show state without bringing it to the foreground (or both, if combined with `desktop_bring_to_front`). Args: `hwnd` (int) OR `titleContains` (string) OR `cacheKey`, plus `state` (one of `"maximize"`, `"minimize"`, `"restore"`, `"show"`, `"hide"`, `"bottom"`). Use to ensure Fusion or KiCad windows are maximized for screenshots, or to hide noisy background windows during demos.+ - Example: `adom-bridge desktop_set_window_state '{"hwnd":133560,"state":"maximize"}'`+ - **`state:"bottom"` (v1.9.114) -- z-order send-to-back, background.** Drops the window BEHIND all others without focusing, moving, or resizing it; works even on an off-screen window. It's an ATOMIC check-and-demote: it reads the OS foreground window first, and if the target IS the foreground (the user is interacting with it) it SKIPS the demotion and returns `wasForeground:true, sentToBack:false` so the caller backs off with no race; otherwise `wasForeground:false, sentToBack:true`. Use to park a background window behind the user's windows (still on-screen, rendering for screenshots) without stealing focus.+ - **`force:true` (v1.9.115)** sends it to back EVEN IF the window is currently the OS foreground (still no activation change; `sentToBack:true` always, `wasForeground` still reported). Use at window-LAUNCH time: the OS often hands the foreground to a brand-new window, so "foreground at launch" is not a real user signal.+ - Example: `adom-bridge desktop_set_window_state '{"titleContains":"(session: abc","state":"bottom","force":true}'`++#### Real OS input - mouse + keyboard (v1.8.170+)++`desktop_click` / `desktop_double_click` / `desktop_right_click` / `desktop_hover` / `desktop_type` / `desktop_press_key` synthesize **real OS input via CGEvent** - so they land on **`chrome://` pages and native dialogs that browser CDP (`nbrowser_*` / `browser_*`) cannot reach**. The verb names mirror the browser extension's `nbrowser_*` so the model transfers between in-page and whole-desktop driving.++- **Gated behind shell-auto-approve** (same risk class as `shell_execute` - keystrokes/clicks hit whatever is focused on the user's live machine). If it's off you get `errorCode:"input_not_approved"`. **You (a cloud/relay AI) can't grant it yourself (v1.9.90)** - call `request_shell_approval` (or let the refusal auto-surface the prompt) and ask the user to click Approve / Bypass Permissions in Adom Bridge, then retry. Every action is written to the activity log. Synthetic input also needs macOS **Accessibility** permission for the Bridge app (System Settings > Privacy & Security > Accessibility).+- **Focus a window first** by passing `{window:"<title substring>"}` (alias `titleContains`) or `{hwnd:<int>}` - reuses `desktop_find_window`'s resolver, so input lands on the intended window, not whatever happened to be focused. Get the window id from `desktop_list_windows` / `desktop_find_window`.+- **Coords are physical screen pixels** - the same space as `desktop_list_windows` rects.++Verbs:+- `desktop_click {x, y, button?:"left"|"right"|"middle", double?, window?|hwnd?}` - left-click by default. `desktop_double_click` / `desktop_right_click` are wrappers; `desktop_hover` moves the cursor without clicking (reveals tooltips).+- `desktop_type {text, window?|hwnd?}` - types a literal Unicode string (any keyboard layout).+- `desktop_press_key {keys:[...], repeat?, window?|hwnd?}` (alias `desktop_send_keys`) - chords/shortcuts. Modifiers `ctrl/alt/shift/cmd` (aliases `meta/super/win` mean Command); named keys `enter/tab/esc/space/backspace/delete/home/end/pgup/pgdn/up/down/left/right/f1-f12`.++Canonical "navigate a browser to a URL" flow (works on `chrome://` where CDP can't):+```bash+adom-bridge desktop_press_key '{"keys":["cmd+l"],"window":"Chrome"}' # focus Chrome + select the address bar+adom-bridge desktop_type '{"text":"chrome://extensions","window":"Chrome"}'+adom-bridge desktop_press_key '{"keys":["enter"],"window":"Chrome"}'+```+For a control that isn't reachable by CDP/coords (e.g. a button inside a shadow DOM), Tab-navigate to it then Enter: `desktop_press_key {"keys":["tab","tab","enter"]}`.++**Visual "Adom cursor" (v1.8.171+)** - a click-through teal cursor + a label that narrates what you're driving (trust cue; byte-faithful to the browser extension's cursor). Pure visual, *not* gated:+- `desktop_cursor {action:"show"|"move"|"hide", x, y, label}` - drive/narrate the cursor on its own ("watch me"). Returns the overlay `hwnd`.+- Or pass `cursor:true` + `label:"Clicking Reload"` on `desktop_click`/`desktop_double_click`/`desktop_right_click`/`desktop_hover` - Bridge glides the teal cursor to the target (narrating the label) *then* does the real click.+- **Auto-hide (v1.8.174+):** the cursor hides itself after `ttlMs` (default **60s**) so it never lingers on the user's screen if you forget to hide it. Each `show`/`move` refreshes the timer. Pass `ttlMs:<ms>` (≤600000) for longer, or `persist:true` to keep it until you explicitly `desktop_cursor {action:"hide"}`. If a cursor you showed was auto-hidden, the **next** cursor/click response prefixes its `_hint` with a NOTE so you learn (no chatty unsolicited message).++#### Background input via macOS Accessibility (AX) - no foreground, no focus steal++The mouse/keyboard verbs above are real OS input, which lands on the **foreground** window - so Bridge foregrounds the target first, yanking it in front of the user's work. When you must NOT steal focus, use the **Accessibility (AX)** verbs instead: `AXPress` / setting `AXValue` are **programmatic** - the app services them via its accessibility tree with **no cursor move, no focus change, no foregrounding**. Chromium browsers expose their a11y tree to AX, so `chrome://` page buttons/links are reachable **by accessible name** (not coordinates). The verb names keep the `desktop_ui_*` spelling for cross-platform compatibility; on this build they are implemented on macOS AX (`desktop_ax`).++- `desktop_find_control {window|hwnd, name|contains, role?}` - **read-only** discovery (ungated). Returns `best:{name, role, rect, invokable, settable}`. Call this first to learn whether a control is AX-reachable.+- `desktop_ui_click {window|hwnd, name|contains, role?}` - AX **press** by accessible name, in the background. Gated behind shell-auto-approve.+- `desktop_ui_set {window|hwnd, name|contains, text}` - AX **set value** on a text field, in the background. Gated.+- `desktop_navigate {window, url}` - background browser navigation via the address bar's AX value. Gated like `ui_set`.++`role` ∈ `button|edit|link|checkbox|menuitem|tab|listitem|combobox|radiobutton|document|group|spinner`. **Caveat (surfaced in `_hint`, not hidden):** some shadow-DOM controls aren't in the AX tree - if `find_control` reports `invokable:false`/not-found, the control genuinely needs a **foregrounded** `desktop_click`. So: prefer `desktop_ui_click` (background) → fall back to `desktop_click` (foreground) only when AX can't reach it.++**Permission:** all AX verbs require the Bridge app to be trusted in **System Settings > Privacy & Security > Accessibility**. Without it every AX call returns `errorCode:"ax_not_trusted"` with a hint - ask the user to enable it there, then retry.++#### The wider AX surface - enumerate, read, drive++Beyond click/set, a near-complete agent-drivable accessibility client is available (same shared selector everywhere):++**Shared selector on every verb:** `{hwnd|window(titleContains)}` + `{name|contains|role}` + `nth:<int>` (0-based - target the Nth match without the exact name). Rects are screen px with `centerX/centerY`; `space:"image"+shotId` is accepted wherever a coordinate is taken.++- **Enumeration (read-only, ungated):**+ - `desktop_ui_tree {window|hwnd, maxDepth?, roleFilter?[], nameContains?, maxNodes?(500)}` - **flagship.** Dump the a11y subtree; nodes carry rect + invokable/settable + value/toggle/selection state + `children[]`. `truncated:true` when it hits `maxNodes` (logged, never silent).+ - `desktop_ui_from_point {x,y | space:"image",shotId,x,y}` - the control under a pixel.+ - `desktop_find_controls {selector, maxResults?(25)}` - ALL matches as an array with `nth` indices (vs `find_control`'s single best).+- **Reading (read-only, ungated):** `desktop_ui_text {selector}`; `desktop_ui_get {selector}` (current value/toggle/range/selection state); `desktop_ui_grid_get {selector, row?, col?}` (table cell + rowCount/columnCount).+- **Actions (gated by shell-auto-approve; background via AX):** `desktop_ui_toggle {selector, state?}`, `desktop_ui_expand {selector, action?:expand|collapse|toggle}`, `desktop_ui_select {selector, action?:select|add|remove}`, `desktop_ui_scroll {selector, dir?, amount?}`, `desktop_ui_set_range {selector, value}`, `desktop_ui_focus {selector}` (usually raises; reports it), `desktop_ui_window {window, action:minimize|maximize|restore|close|move|resize}`.+- **Introspection:** `desktop_ui_capabilities {}` - **read-only, ungated.** Lists the read/action verb sets and what this platform implements.++**Not on macOS:** the event-subscription verbs (`desktop_ui_watch` / `desktop_ui_events` / `desktop_ui_unwatch` / `desktop_ui_selftest`) return a clean `not_implemented_macos` error. To wait for a dialog/window, poll `desktop_find_window` or `desktop_screenshot_window` instead.++#### Zero-math coordinate clicks - `coordMap` + image-space (v1.8.175+)++You no longer recompute scale/offset/DPI to click what you see in a screenshot. Every screenshot verb returns a **`coordMap`**:++```jsonc+"coordMap": {+ "shotId": "shot-w5050328-1",+ "image": { "w": 1372, "h": 1031 }, // the DELIVERED (safe) PNG's pixels+ "screenRect": { "x": 594, "y": 254, "w": 1372, "h": 1031 }, // physical px = input-verb space+ "dpiScale": 1.5,+ "imageToScreen": { "sx": 1.0, "sy": 1.0, "ox": 594, "oy": 254 } // screen = img*s + o+}+```++Two ways to use it:+- **Let Bridge convert (preferred, zero math):** `desktop_click {space:"image", shotId, x, y}` where `x,y` are pixels in that screenshot. Also works on `desktop_double_click`/`right_click`/`hover`. The `shotId` comes from the capture's `coordMap.shotId` (last ~64 shots retained; re-capture if it expires).+- **Compute yourself:** `screenX = x*sx + ox`, `screenY = y*sy + oy`.++`screenRect` is in the SAME physical-px space as `desktop_list_windows` rects and the `desktop_click` default (`space:"screen"`).++> **Driving a GUI app that pops error boxes / modal dialogs? Read the dedicated [`adom-bridge-window-capture`](WINDOW_CAPTURE.md) skill.** `desktop_screenshot_window {hwnd}` captures ONE window without foregrounding it (per-window `screencapture -l`, no focus steal). It is the breakthrough for automating native apps whose dialogs a full-screen grab can't see. The rest of this section is the reference; that skill is the how-and-why.++**Dialogs / secondary windows:** an app's dialogs are separate windows in `desktop_list_windows` on macOS - capture each by its own window id, or use `kicad_screenshot_all` (returns every KiCad window incl. error dialogs as a `screenshots[]` array, each with its own `coordMap`+`shotId`). **Use `desktop_screenshot_screen`** (full-desktop grab) to see everything at once.++**Screen geometry on every shot (v1.8.178):** every `desktop_screenshot_window` / `_screen` response now carries:+- **`windowRect: {x,y,w,h,monitor}`** - the window's position + size on the overall (possibly multi-monitor) desktop, in physical screen px.+- **`display: {virtualScreen, primary{w,h,dpi,scale}, monitors[], monitorOfWindow}`** - the full monitor topology: total virtual bounds, and per-monitor `bounds`/`dpi`/`scale`/`primary`. `monitorOfWindow` indexes into `monitors[]`. (Same data as the standalone `desktop_display` verb, folded in.)+- **`drive: "background"`** - screenshots never foreground the window. The companion `_related` field lists sibling screenshot/window verbs.++**Screenshot cache is self-cleaning + self-describing (v1.9.107/108/113):** the CLI auto-pulls every screenshot to ONE user-visible, namespaced dir on the caller's machine (`~/project/screenshots/adom-bridge/`, else `~/.adom/screenshots/`; `ADOM_SHOTS_DIR` overrides) - never `/tmp` anymore. Shots are **bucketed by kind into subfolders**: `window/` (a specific window / app / owned popup) and `screen/` (a full-desktop grab). **Human-readable filenames (v1.9.113):** `<YYYY-MM-DDThh-mm-ss.mmm>[-<hwnd>].png` - a sortable ISO stamp (lexicographic == chronological), plus the `hwnd` for a window capture; the `window`/`screen` kind is NOT repeated in the name because the folder already says it. **One file when small (v1.9.114):** if the shot was already within the safe size cap (the common case) it's a single `<stem>.png` and `localFullPath == localSafePath` - no redundant re-encoded copy. Only a shot large enough to be downscaled produces a `<stem>.full.png` + `<stem>.safe.png` pair (Read the `.safe` one for image analysis; use `.full` for archive). Each shot gets a **`<stem>.json` sidecar** (shotlog-style: `source`, `timestamp`, `kind`, dims + `bytes`, `coordMap`, `rect`; a full+safe pair shares one). **Parent/child grouping (v1.9.111/113):** when `desktop_screenshot_window` returns owned popups in `screenshots[]`, the parent AND each pulled child share the same stem, and each child gets a **`.popup-NN` label** - e.g. parent `2026-07-15T01-14-16.937-986710.safe.png` and children `2026-07-15T01-14-16.937-986710.popup-01.safe.png`, `...popup-02...`. So a plain `ls` shows the whole capture grouped (shared stem) and the role (`.popup-NN`) without opening a sidecar; the sidecar also carries `parent` + `popupIndex`. (`kicad_screenshot_all` returns sibling windows with no parent, so those keep their own names.) A janitor prunes it (7 days / 2 GB / newest kept; sidecars follow their PNG, orphans swept) and drains the old `/tmp/bridge-shots` + `/tmp/adom-bridge-screenshots` backlogs, running opportunistically at most once/24h on any CLI call. **You do not need to clean it up or write your own cron.** Every screenshot response carries a `_cacheHint`; verify coverage with **`adom-bridge janitor`** (status: each subfolder, size, policy, last sweep) or force a sweep with **`adom-bridge janitor '{"sub":"run"}'`**. It never touches other tools' files under `~/project/screenshots/` (e.g. shotlog). Tune via `ADOM_SHOTS_MAX_AGE_DAYS` / `ADOM_SHOTS_MAX_GB` / `ADOM_SHOTS_MAX_FILES`, or disable with `ADOM_JANITOR=0`.++**Background vs foreground driving (v1.8.178):** every desktop driving verb carries a structured **`drive`** field (`"background"` | `"foreground"` | `"n/a"`) - in `GET /commands`, in `adom-bridge help <verb>`, and echoed in the verb's own response. **Background** (no foreground / focus steal / cursor move - AX + capture): `desktop_ui_click`, `desktop_ui_set`, `desktop_navigate`, `desktop_find_control`, `desktop_screenshot_window/_screen`, `desktop_record_window_start`. **Foreground** (synthetic input goes to the focused window, so Bridge foregrounds first): `desktop_click`/`_double_click`/`_right_click`/`_hover`, `desktop_type`, `desktop_press_key`, `desktop_bring_to_front`. Run **`adom-bridge help background`** for the full grouping. Rule: to act without disturbing the user's window, drive by NAME (`desktop_find_control` → `desktop_ui_click`/`ui_set`); fall back to coordinate clicks/keys only when the control isn't in the AX tree.++**Click a control by name → coords:** `desktop_find_control` returns `best.rect{x,y,w,h,centerX,centerY}` in screen px - `desktop_click {x:centerX, y:centerY}` to click it (handy when `invokable`/`settable` are both false). Target by `name`, `contains`, or `role`.++**Keyboard chords:** `desktop_press_key` accepts a chord as one string `["cmd+w"]` OR separate tokens `["cmd","w"]` - both mean Command held + W. A *sequence* of distinct chords uses `+` per element: `["cmd+l","enter"]`. The reply echoes `chords` showing how Bridge interpreted your `keys`.++- `desktop_flash_window` (v1.8.152+) -- Request the user's attention for the Bridge app (a Dock bounce / attention request on macOS) as an "attention / ongoing activity" cue. Args: `hwnd` (int) OR `titleContains` (string); `mode` (`"until_focused"` default, or `"count"`); `count` (int, `mode:"count"` only, default 3); `stop` (bool -- clear an active request). Returns `{ok, hwnd, title, mode}`. Distinct from the puppeteer bridge's `browser_alert_window`, which flashes pup's OWN tracked windows.+ - Example: `adom-bridge desktop_flash_window '{"titleContains":"Fusion"}'` then add `"stop":true` to clear.+- `desktop_taskbar` (v1.8.152+) -- Glanceable status on macOS maps to the **Dock**: `flash` becomes a Dock attention bounce (`"until_focused"` | `"stop"`), and badge/progress channels update Bridge's own Dock tile. Windows-only channels from the original contract (per-window overlay icons, thumbnail tooltips/clips, painting OTHER apps' buttons) do not exist on macOS and are ignored or reported in the response's `applied[]`. Use `desktop_flash_window` / a notification for "come look" cues, and `desktop_caption` for on-screen narration.+- `desktop_revoke_approvals` -- Revoke all shell auto-approve permissions and deny pending approvals. The next shell command will show the approval dialog.+ - Example: `adom-bridge desktop_revoke_approvals`+- `desktop_caption` -- Show a global caption overlay that stays visible above ALL windows (KiCad, Fusion, Chrome). Native always-on-top overlay window, not a browser DOM element. Click-through: mouse events pass to windows below. Captured by screen recordings.+ - Show: `adom-bridge desktop_caption '{"text":"Step 1: Opening the board","position":"top","size":"large","duration":3000}'`+ - Custom position: `adom-bridge desktop_caption '{"text":"Look here","x":0.3,"y":0.2,"size":"large","persist":true}'`+ - Hide: `adom-bridge desktop_caption '{"action":"hide"}'`+ - Multiple simultaneous captions (use `id` to keep them independent):+ ```+ adom-bridge desktop_caption '{"text":"Step 1: Opening board","id":"step","position":"center","size":"large","persist":true}'+ adom-bridge desktop_caption '{"text":"REC ●","id":"status","position":"bottom-left","size":"medium","persist":true}'+ ```+ - Replace only the step caption (status stays):+ ```+ adom-bridge desktop_caption '{"text":"Step 2: Routing","id":"step","position":"center","size":"large","persist":true}'+ ```+ - Hide just the step caption: `adom-bridge desktop_caption '{"action":"hide","id":"step"}'`+ - Args:+ - `text` (string) - caption text+ - `id` (string, optional) - identifies this caption. Captions with the same id replace each other; different ids coexist simultaneously. Default `"_default"` - so bare calls without id still replace each other for backward compat.+ - `position` - preset: `"top"`, `"bottom"` (default), `"center"`, `"top-left"`, `"top-right"`, `"bottom-left"`, `"bottom-right"`+ - `x` (float 0.0–1.0) - normalized screen X, overrides position horizontal. 0.0 = left edge, 1.0 = right edge. Centers the caption box on this point.+ - `y` (float 0.0–1.0) - normalized screen Y, overrides position vertical. 0.0 = top edge, 1.0 = bottom edge.+ - `size` - preset: `"large"` (72px), `"medium"` (32px, default), `"small"` (20px)+ - `fontSize` (int) - custom font size in px (8–400). Overrides `size` preset when provided.+ - `duration` (ms) - auto-dismiss timer. **Omitted or `0` → `30000` (30s); clamped to max `600000` (10 min).** Captions ALWAYS auto-expire (v1.8.138+) so a forgotten one can't be stranded on the user's desktop. To keep one up longer, **re-issue the same `id`** (resets the timer) or pass a bigger `duration`.+ - `persist` (bool, optional) - `true` = sticky, NO auto-dismiss. Supervised use only (e.g. demo recordings); you MUST then dismiss it yourself with `action:"hide"`. This is the only way to a never-expiring caption (`duration:0` no longer does that).+ - `action` - `"hide"` to dismiss a caption (with `id`: hides only that caption; without `id`: hides all captions), `"force-clear"` to destroy ALL caption windows regardless of id (nuclear option - use at top of demo scripts for guaranteed clean slate)+ - Captions with the same `id` replace each other instantly (previous destroyed, no fade overlap). Captions with different ids coexist - multiple captions can be visible simultaneously.++### Desktop Screenshots++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 window ids, then `desktop_screenshot_window` with the id.++- `desktop_list_windows` -- List all visible windows (returns the window id as `hwnd` (the CGWindowID on macOS), title, owning app, position/size, and `z` = Z-order index, 0 = topmost)+- `desktop_find_window` (v1.8.153+) -- Resolve a window's id by `titleContains` (case-insensitive substring), returned in Z-order with `best` = the topmost match. Use this instead of diffing `desktop_list_windows` before/after launching something - it's race-free for "the window I just opened." Returns `{ok, count, best:{hwnd,title,z,rect}|null, windows[]}` (`ok` true only when ≥1 matched). Pass `best.hwnd` to `desktop_flash_window` / `desktop_screenshot_window`.+ - Example: `adom-bridge desktop_find_window '{"titleContains":"GitHub"}'`+- `desktop_activity_filter` (v1.8.155+) -- Scope the **user's GUI activity log** so they watch the slice you care about while you work (e.g. focus it on one bridge before a debugging session). Args: `bridge` (a bridge registry name from `bridge_list` | `"none"` = only global/non-bridge Bridge commands | `"all"`/omit = everything), `text` (free-text substring, ANDs with bridge), `clear` (bool - reset). **Drives the human's GUI view only - it does NOT change which verbs you can call or what they return.** The GUI shows a status bar stating exactly what's shown + what's hidden.+ - Example: `adom-bridge desktop_activity_filter '{"bridge":"kicad"}'` then `adom-bridge desktop_activity_filter '{"clear":true}'`+- `desktop_screenshot_window` -- Capture a specific window by its id (`hwnd`)+- `desktop_screenshot_screen` -- Capture the entire desktop (all monitors)+- `desktop_ad_scroll` (v1.9.86+) -- **Scroll Bridge's OWN window panels.** Bridge is a fully AI-controllable app, so you can drive its own scrollbars - most usefully to page the **bridges sidebar** and screenshot cards that are below the fold. Args: `panel` (`bridges` default | `activity` | `connections`), `to` (`top`|`bottom`, absolute - wins over `dy`), `dy` (relative px, default 400; negative = up). Returns `{ok, panel, scrolled, state:{scrollTop,scrollHeight,clientHeight,atTop,atBottom,found}|null, _hint}` - round-tripped from the live window so you can **loop until `state.atBottom==true`**. A collapsed bridges sidebar is auto-expanded first; `state==null` means the panel didn't report (collapsed/empty).+ - Capture-all-cards workflow: `desktop_ad_scroll '{"panel":"bridges","to":"top"}'` → `desktop_screenshot_window` (Bridge hwnd) → `desktop_ad_scroll '{"panel":"bridges"}'` → screenshot → repeat until `atBottom`.++### App self-update (relay-driven, v1.9.87+)++Bridge auto-updates itself from the wiki manifest via a silent dmg swap (mount, rename-swap the .app, relaunch; manual-open fallback), but historically only a HUMAN clicking the footer "Install now" banner could apply it - so a headless / fleet box could sit a version behind with no path forward from the relay. These verbs give the AI that path (same shape as the shell-approval surfacing verbs). Use `--target <box>`.++- `update_status` (read-only, ungated) -- `{currentVersion, latestVersion, updateAvailable, updateReady, state, busy, updateMode}`. `updateReady:true` = the newer version is downloaded + SHA-verified on disk, appliable with no network.+- `update_check` -- force a manifest check now; if newer, download + verify (stage) it in the background. Poll `update_status` until `updateReady:true`.+- `apply_update` -- the programmatic "Install now": apply the staged update + relaunch Bridge. Respects `busy` (refuses if a command is in flight). Only ever applies Bridge's own downloaded + SHA-verified SIGNED wiki release. Returns `{applying:true}` then Bridge restarts (the relay drops + reconnects; poll `targets`/`ping` for the new version). If not staged yet, it kicks staging and tells you to poll + retry.+- `request_update_approval` -- surface the "Update available - Install now / Later" banner on the target + bring Bridge to the front (for an ATTENDED box where the human should decide). Does NOT apply; `apply_update` installs.+- `update_set_mode {mode: auto|prompt|off}` -- the auto-apply POLICY. `auto` = unattended stage+apply on the 4h/on-launch poll (set this ONCE on headless fleet boxes and they self-update every release with no relay call); `prompt` = show the banner (default, attended); `off` = no checks.+ - Fleet flow: `update_status --target <box>` → if `updateAvailable` and unattended, `apply_update --target <box>` (or set `update_set_mode {"mode":"auto"}` once). A box must already be on ≥1.9.87 to have these verbs; the one-time bootstrap of an older box still needs a human click OR `auto` mode.++### Browser automation (`browser_*`, Puppeteer/pup) - bridge-owned; light pointer++The pup bridge is **cloud-owned** and ships its OWN full skill (its wiki page / skill pkg - `bridge_list` names it; `adom-bridge sync_skills` installs it). This page keeps only the pointer plus first-touch examples; sessions/profiles, credential vault, downloads, recording, CDP details are the bridge's skill.++Most-used verbs:++```bash+adom-bridge browser_open_window '{"sessionId": "s1", "profile": "p1", "url": "https://example.com"}'+adom-bridge browser_navigate '{"sessionId": "s1", "url": "https://example.com/page2"}'+adom-bridge browser_screenshot '{"sessionId": "s1"}'+adom-bridge browser_eval '{"sessionId": "s1", "expression": "document.title"}'+adom-bridge browser_fetch_url '{"url": "https://example.com/data.json"}'+```++`browser_readiness` is the read-only probe (never spawns or downloads). Full details: the bridge's own skill via `sync_skills`.++### Screen recording - Bridge's desktop recorder (tab recording is pup's)++Bridge owns DESKTOP recording: a visible HUD opens with the REASON you supply, so the user always sees that recording is on and why. Full guide: the `adom-bridge-recording` skill. There is also a native screen recorder: `desktop_record_window_start` / `_stop` / `_status` records via macOS `screencapture -v` to a pullable `.mov` (on macOS it records the screen; graceful stop finalizes the file).++```bash+adom-bridge desktop_recorder_open '{"reason": "record the DRC walkthrough for review"}'+adom-bridge desktop_record_start '{"confirmDesktopNotTabRecording": true}'+# ... drive the desktop ...+adom-bridge desktop_record_stop '{}'+adom-bridge desktop_record_list '{}'+adom-bridge desktop_recorder_close '{}'+```++`desktop_record_start` without `confirmDesktopNotTabRecording: true` refuses with `errorCode: desktop_record_needs_confirmation` - that guard exists because recording a BROWSER TAB is the pup bridge's separate feature (`browser_record_start` etc., see pup's own skill).++### 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 shell polling of the Downloads folder. No shell, no user approval prompt, no parsing of `ls` output. They run pure Rust on the desktop side, so they're fast and predictable.++The whole "wait for a file to arrive" flow is one call:++```bash+adom-bridge desktop_watch_files '{+ "path": "~/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` (abs path on the Mac; `~` / `%USERPROFILE%` / `%TEMP%` are expanded - the Windows-style tokens still map to home / temp for cross-platform callers), `glob` (default `*`; shell-style with `*` and `?`), `modifiedSince` (optional unix-seconds OR ISO-8601 string).+- Returns: `{ok, path, glob, files: [{path, name, size, mtime}, ...] sorted newest-first, count}`.+- Does NOT recurse. Lists one directory only.++#### `desktop_watch_files` - block until a match arrives++Polls the directory every `pollMs` (default 1000) until at least one file matches the glob with `mtime > since`, or `timeoutMs` (default 60000, max 600000 = 10 min) elapses.++- Args: same as `desktop_list_files` plus `since` (defaults to **now** - without an explicit value, only NEW files report), `timeoutMs`, `pollMs`.+- Returns on match: `{ok:true, file:{path,name,size,mtime}, elapsedMs}`.+- Returns on timeout: `{ok:false, error:"timeout", elapsedMs, _hint, path, glob}`.+- The `since`-defaults-to-now behavior matches the canonical "click then watch" flow. If you might miss the file by starting the watch slightly late (e.g. fast small downloads), record `$(date +%s)` BEFORE the click and pass it as `since`.++#### `desktop_pull_glob` - one-shot orchestrator (recommended for download flows)++Composes `desktop_list_files` (or `desktop_watch_files` if `wait:true`) with the existing streaming `pull_file` mechanism. The whole "wait for a download then pull it" flow in one call:++```bash+BEFORE=$(date +%s)+# … trigger the download click via browser_input_dispatch …+adom-bridge desktop_pull_glob "{+ \"path\": \"~/Downloads\",+ \"glob\": \"ul_*.zip\",+ \"since\": $BEFORE,+ \"wait\": true,+ \"timeoutMs\": 60000,+ \"saveTo\": \"/tmp/cse-out\"+}"+# → {ok:true, files:[{name,path,size,sha256,chunks}, ...], errors:[], matchedCount}+```++- When `wait:true`: blocks via `desktop_watch_files` until at least one match appears, then re-lists the directory and pulls **everything matching** (so a `.crdownload` + final `.zip` written in the same poll tick both come along).+- When `wait:false` (default): just lists what's there now and pulls those.+- Pulls use the existing `pull_file` streaming pipeline - sha256-verified, ~1MB chunks, resumes the same way `pull_file` does.++#### Glob semantics++- `*` matches any run of non-separator chars (within a single filename - these primitives don't recurse).+- `?` matches a single non-separator char.+- Case-sensitive on macOS/Linux (matches the filesystem) - `*.step` and `*.STEP` are different globs here.+- Examples: `ul_*.zip`, `LIB_*.zip`, `*.pdf`, `datasheet_*.pdf`, `*.step`.++#### Path expansion++- `~` and `~/foo` → home dir.+- `%USERPROFILE%` → home dir (kept for cross-platform callers; case-insensitive).+- `%APPDATA%` / `%LOCALAPPDATA%` → the platform config / local-data dirs (`~/Library/...` on macOS).+- `%TEMP%` → the temp dir (`/tmp`-style on macOS).++#### Time format for `since` / `modifiedSince`++- Number: unix seconds (e.g. `1746483600`). Floats accepted.+- String of digits: same (e.g. `"1746483600"`).+- ISO-8601 / RFC-3339 string: e.g. `"2026-05-04T22:30:00Z"` or with offset `"2026-05-04T15:30:00-07:00"`.+- Files with `mtime <= since` are filtered out (strict `>`).++### Adom Hydrogen (`hydrogen_*`) - proxy + build/lifecycle for the sibling Tauri app++When the user has **Adom Hydrogen** running (the sibling Tauri v2 app), built-in `hydrogen_*` verbs reach into it. NOT a separate bridge process - Bridge proxies HTTP calls directly to Hydrogen's control API.++**Port discovery (v1.8.72+):** Hydrogen's control API lives on **port `47084`** (default; the actual port is read from `~/.config/adom-hydrogen/ports.json` key `control` on every `hydrogen_api` call, fallback 47084).++#### The canonical generic verb - `hydrogen_api` (v1.8.72+)++```bash+adom-bridge hydrogen_api '{"method":"GET","path":"/health"}'+adom-bridge hydrogen_api '{"method":"POST","path":"/setup/run-step","body":{"id":"welcome"}}'+adom-bridge hydrogen_api '{"method":"GET","path":"/workspace/health"}'+```++Generic HTTP pass-through to any Hydrogen endpoint. Raw JSON in `body` - no curl-through-the-relay quote mangling. Returns `{success, status, body, port, method, path}` mirroring Hydrogen's response. If Hydrogen isn't running, returns `{ok:false, error:"Hydrogen not reachable on :47084", _hint:"..."}` instead of timing out.++**Public denylist** - these paths are refused with `{ok:false, error:"not_allowed_in_public", denylist_match:"..."}` on public Bridge builds (use the internal `hd-dev` cargo feature to bypass):++```+/wsl/unregister, /setup/virgin-reset, /container/destroy,+/container/reset, /workspace/reset, /workspace/destroy+```++#### Inspect + drive a running Hydrogen (v1.8.15+, post-v1.8.72 rework)++| Verb | What | Status |+|---|---|---|+| `hydrogen_status` | Composes Bridge-side facts (running, repo SHA) + Hydrogen's `/workspace/health` + `/buildinfo` relayed | ✓ shipped |+| `hydrogen_api` | Generic HTTP pass-through (above) | ✓ v1.8.72+ |+| `hydrogen_log '{"tail":30}'` | Tail Hydrogen's log file on disk (works EVEN when Hydrogen is down) | ✓ shipped |+| `hydrogen_open_url '{"url":"...","browser":"chrome","profileDir":"Default"}'` | POST `/open-in-profile` - open URL in a specific browser profile | ✓ shipped |+| `hydrogen_browser_profiles` | GET `/browser-profiles` - enumerate browsers + profiles | ✓ shipped |+| `hydrogen_screenshot` / `hydrogen_shot` | Find Hydrogen window, capture lossless PNG (background per-window capture); `hydrogen_shot` adds region-scoped variant | ✓ shipped |+| `hydrogen_eval '{"js":"..."}'` | POST `/eval` - run JS in Hydrogen's main webview | ⚠ **Moved to `hd-dev` in v1.8.72.** Public returns `verb_moved_to_hd_dev`. Use `hydrogen_api '{"method":"POST","path":"/eval","body":{"js":"..."}}'` |+| `hydrogen_iframe_eval '{"js":"...","contextIndex":0}'` | POST `/iframe-eval` - JS in code-server iframe via CDP | ⚠ Moved to `hd-dev`. Use `hydrogen_api` equivalent |+| `hydrogen_reload_vscode` | POST `/reload-vscode` | ⚠ Moved to `hd-dev`. Use `hydrogen_api` equivalent |+| `hydrogen_container_exec '{"command":"..."}'` | POST `/container-exec` | ⚠ Moved to `hd-dev`. Use `hydrogen_api` equivalent |+| `hydrogen_launch` / `hydrogen_stop` / `hydrogen_restart` | Hydrogen process lifecycle | ✓ shipped |+| `hydrogen_ship` | Atomic build+relaunch+verify-SHA (see below) | ✓ shipped |+| `hydrogen_build*` (v1.8.16+) | Build pipeline orchestration | ⚠ Moved to `hd-dev` in v1.8.72 (Hydrogen developers only) |++#### Atomic ship - `hydrogen_ship` (v1.8.49+) - START HERE for post-push rebuilds++**One verb, one call, one structured pass/fail.** Replaces the 4-step chain `hydrogen_stop` → `hydrogen_build_rust` → poll `hydrogen_build_status` → `hydrogen_launch` → `curl :47084/buildinfo`. Eliminates the "is the new code actually running?" guesswork after every push.++Sequence inside one call:+1. kill Hydrogen + poll until zero instances (frees the binary lock)+2. `git fetch && git reset --hard origin/main && rev-parse HEAD` - asserts `HEAD == expectSha` (fails fast if GitHub hasn't replicated the push yet)+3. if `clean:true`, `cargo clean -p adom-hydrogen` (defeats stale-relink)+4. `cargo build` and parse stderr for `Compiling adom-hydrogen` to know whether anything actually compiled+5. launch + poll `http://127.0.0.1:47084/buildinfo` until it responds + assert `build_sha == expectSha`++```bash+# End-to-end after a push:+git push && SHA=$(git rev-parse HEAD) && \+ adom-bridge hydrogen_ship "{\"expectSha\":\"$SHA\"}"+```++Args:+- `expectSha` - **REQUIRED** string. Full or prefix SHA of the commit you want running. Bidirectional-prefix match (`abc123` matches the full SHA starting with `abc123` and vice versa).+- `clean` - optional bool, default `false`. `true` forces `cargo clean -p adom-hydrogen` before build. Use when a prior `hydrogen_ship` succeeded but `runningSha != expectSha` (the stale-relink trap - cargo said `Finished` but reused the cached binary).++Returns on success: `{ok:true, builtSha (== expectSha), runningSha (== expectSha), compiled:bool, elapsedSecs, _hint}`. `compiled:false` on success means cargo found nothing to recompile (cached relink or HEAD didn't actually change - usually fine but surface to caller in case the push was a no-op).++Returns on failure: `{ok:false, failedAt:'args'|'kill'|'git'|'clean'|'build'|'launch'|'verify', error, builtSha (set after git step), runningSha (set if launch worked but verify saw the wrong SHA), compiled (set after build step), elapsedSecs, _hint}`. The partials let you reason about WHERE it broke:++| `failedAt` | What it means |+|---|---|+| `args` | `expectSha` missing / malformed |+| `kill` | Couldn't kill Hydrogen (rare - usually permissions) |+| `git` | `git fetch / reset` failed, OR `HEAD != expectSha` after reset (push hasn't replicated to origin/main yet - retry in a few seconds) |+| `clean` | `cargo clean` failed (rare) |+| `build` | `cargo build` failed - read `_hint` for next step (usually `hydrogen_build_log` to see the compile error) |+| `launch` | Cargo finished but the `adom-hydrogen` binary wouldn't start - `binary_missing` or process spawn error |+| `verify` | Hydrogen launched but `/buildinfo` either never responded or reported a different SHA than `expectSha` (the stale-relink trap - retry with `clean:true`) |++Long-running: cold cargo build can take 2-3 min. CLI WS timeout is 12 min - call as a single sync invocation and wait. Don't poll inside `hydrogen_ship`; it does its own internal polling.++#### Lower-level build + lifecycle suite (v1.8.16+, enriched in v1.8.52+)++Use these directly only when you need finer control than `hydrogen_ship` provides (e.g. frontend-only build, watching log stream live, manual launch sequencing). For the common "I pushed code, get it running" path use `hydrogen_ship`.++The relay's `shell_execute` has a 30s timeout; Hydrogen's `pnpm build` + `cargo build` exceed that, so the build is split into async-spawn verbs + sync poll/tail verbs:++```bash+# Frontend-only rebuild, watch progress, relaunch (no hydrogen_ship since hydrogen_ship is Rust-build-only)+adom-bridge hydrogen_stop+adom-bridge hydrogen_build_frontend '{"show":false}' # returns {ok, pid, logPath} immediately+OFFSET=0+while true; do+ RESP=$(adom-bridge hydrogen_build_tail "{\"offset\":$OFFSET}")+ echo "$RESP" | jq -r '.lines[]'+ DONE=$(echo "$RESP" | jq -r '.done')+ OFFSET=$(echo "$RESP" | jq -r '.newOffset')+ [[ "$DONE" == "true" ]] && break+ sleep 3+done+adom-bridge hydrogen_build_status # confirm succeeded:true && relinkOnly:false+adom-bridge hydrogen_launch # start the new debug binary+```++##### `hydrogen_build` / `hydrogen_build_frontend` / `hydrogen_build_rust` - async-spawn build verbs++| Verb | What |+|---|---|+| `hydrogen_build '{"show":false}'` | Full async build: `git pull` + `pnpm build` + `cargo build`. Returns instantly with `{pid, logPath, flavor:'full'}`. Final log line is `BUILD_OK` or `BUILD_FAILED: <step>`. |+| `hydrogen_build_frontend` | Just `pnpm build` (root). Returns `{pid, logPath, flavor:'frontend'}`. |+| `hydrogen_build_rust` | Just `cargo build` (src-tauri/). **v1.8.52+:** dispatch response also carries `builtSha` + `exeMtimeBefore` + `exeSizeBefore` (captured at spawn time into the temp dir's `hydrogen-build-meta.json`) so the subsequent `hydrogen_build_status` / `hydrogen_launch` don't need a separate `git rev-parse` + `stat` call. Returns `{ok, pid, logPath, flavor:'rust', show, lingerSecs, builtSha, exeMtimeBefore, exeSizeBefore, _hint}`. |++##### `hydrogen_build_status` - sync state probe (v1.8.52+ enriched)++Returns:+```+{+ ok, building, succeeded, failed,+ lastLines:[string], # last ~40 lines of stderr+ logPath, pid, pidAlive,+ failureReason, # 'compile_error' | 'exe_locked' | 'orphaned_no_sentinel'+ compiledCrates:[string], # names parsed from 'Compiling X v...' lines+ relinkOnly:bool, # true iff succeeded && compiledCrates is empty+ exeChanged:bool, # mtime OR size after differs from before+ exeMtimeBefore:int, exeMtimeAfter:int, # unix seconds+ exeSizeBefore:int, exeSizeAfter:int, # bytes+ exeLocked:bool, # true when build failed because the adom-hydrogen binary was held+ holderPids:[int], # PIDs holding the binary - kill these+ builtSha:string|null, # SHA hydrogen_build_rust/hydrogen_build was dispatched at+ elapsedSecs:int,+ flavor:string, # 'rust' | 'frontend' | 'full'+ _hint+}+```++**`relinkOnly:true` on success is the silent-fail trap to look for** - your push didn't actually rebuild because cargo found nothing to recompile. Usually means your source changes weren't pulled, or you touched a file the workspace doesn't track. Use `hydrogen_ship` with `clean:true` to force a fresh build.++**`exeLocked:true` on failure** + `holderPids` tells you exactly which PIDs to kill before retry; or just use `hydrogen_ship` which kills Hydrogen before building.++##### `hydrogen_launch` - start the debug binary (v1.8.52+ enriched)++Args: `killExisting` - optional bool, default `false`. When `false` and Hydrogen is already running, refuses with `reason:'already_running'` so you don't accidentally attach to a stale process. When `true`, kill the existing instance with a 600 ms grace period before launching fresh.++Returns on launch: `{ok:true, launched:true, wasAlreadyRunning:bool, killedFirst:bool, pid:int (Hydrogen's actual PID, not the cmd-wrapper), exePath, builtSha:string|null, _hint}`.++Returns on refusal: `{ok:false, launched:false, wasAlreadyRunning:bool, pid:int|null, killedFirst:false, reason, _hint}`. `reason` ∈:+- `build_in_progress` - wait for `hydrogen_build_status.building:false`+- `build_failed` - fix and retry `hydrogen_build` (or use `hydrogen_ship`)+- `already_running` - pass `killExisting:true` to force-kill+relaunch, or use `hydrogen_restart`+- `binary_missing` - the `target/debug/adom-hydrogen` binary not on disk; run `hydrogen_build_rust` (or `hydrogen_ship`) first++**`launched:true, wasAlreadyRunning:false, killedFirst:false` is the proof you need that truly fresh code is running.** Any other combo means you should compare `builtSha` to Hydrogen's actual `/buildinfo.build_sha`.++##### `hydrogen_stop` / `hydrogen_restart` / `hydrogen_build_log` / `hydrogen_build_tail`++| Verb | What |+|---|---|+| `hydrogen_stop` | Force-kills the adom-hydrogen process. `{wasRunning}` distinguishes killed vs no-op. |+| `hydrogen_restart` | Stop + launch in one call. Same guards as `hydrogen_launch` (skip `already_running`). |+| `hydrogen_build_log` | Full log dump from the temp dir's `hydrogen-build.log`. |+| `hydrogen_build_tail '{"offset":N}'` | Incremental stream - `{lines, newOffset, totalBytes, done, succeeded, _hint}`. Pass `newOffset` back for the next chunk. |++The `show: true` arg on any build verb opens a visible terminal window so the user can watch the build scroll. Default `show: false` runs hidden.++**Auto-close (v1.8.23+).** Visible build windows auto-close 30 seconds after the final `BUILD_OK` / `BUILD_FAILED` line - long enough for the user to read the last error, short enough that build-after-build sessions don't accumulate orphan windows. The window prints a clear green/red "auto-closes in 30 seconds" banner before sleeping. Override with `lingerSecs`:+- `'{"show":true, "lingerSecs":60}'` - give yourself 60s instead of 30+- `'{"show":true, "lingerSecs":0}'` - close immediately, no grace period+- `'{"show":true, "lingerSecs":3600}'` - keep open for an hour (cap)+The response includes the applied `lingerSecs` so you can confirm what landed. Hidden builds (`show:false`) ignore `lingerSecs` entirely - no window to linger.++**Don't call `hydrogen_launch` until `hydrogen_build_status` shows `succeeded: true`** - the guard refuses with `reason: "build_failed"` if you do, and the `_hint` tells the AI exactly what to do (`hydrogen_build_log` to see errors, fix, rebuild). Same for `hydrogen_restart`.++#### Named-region screenshots - `hydrogen_shot` (v1.8.54+)++**One verb replaces the `desktop_list_windows | grep 'Hydrogen' → desktop_screenshot_window <hwnd>` chain** (3 calls + fragile HWND parsing). Captures a named region of Hydrogen, produces BOTH a full-res PNG and a Claude-safe-resized copy, and returns rich metadata so the AI never has to chain screenshot → pull → convert → crop → resize → Read manually.++Each shot is **self-identifying**: it includes the running build's SHA + local build time read from Hydrogen's `/buildinfo` so you can confirm you're looking at the build you expect before trusting what you see (no more "this screenshot was from the previous binary, not my fix" confusion).++Regions are deterministic - they resolve via Hydrogen window bounds + CDP element rects queried through Hydrogen's `/eval` endpoint. No AI-supplied pixel coords.++Args:+- `region` - **REQUIRED**. One of:+ - `full` - whole Hydrogen window+ - `titlebar` - top ~40 px strip (Hydrogen's build SHA / timestamp readout lives here)+ - `vscode` - VS Code iframe pane via CDP element rect+ - `setup-panel` - Setup Steps panel (`.setup-panel` / `.setup-panel-output`)+ - `wiki` - wiki / browser tab pane+ - `claude` - Claude Code panel+- `resizeMax` - optional int, default `1400`. Longest side of the safe-resized copy. Use ≈800 to fit more shots in one Claude turn; 1568 for max fidelity.++Returns:+```+{+ ok, region,+ fullPath, # lossless raw PNG - for shotlog/archival, NOT Read+ safePath, # ≤resizeMax PNG - pull_file this, Read this+ fullSize:[w,h], safeSize:[w,h],+ clipRect:[x,y,w,h], # captured-image pixels (NOT screen pixels)+ capturedAtLocal,+ runningBuildSha, # from Hydrogen's /buildinfo - null if Hydrogen doesn't expose it yet+ runningBuildLocal,+ hdWindowFound:bool, regionFound:bool,+ windowOuter:[w,h], capturedSize:[w,h],+ _hints: {+ pull, # exact pull_file command for safePath+ fullForArchive, # note that fullPath is for archive, not Read+ staleWindow, # set when runningBuildSha looks wrong+ regionMiss # set when regionFound:false+ }+}+```++Recipe - verify-which-build-is-running + confirm-config-change-took-effect + full dump:++```bash+# After hydrogen_ship, confirm the titlebar shows the SHA you expect:+adom-bridge hydrogen_shot '{"region":"titlebar"}'+# → runningBuildSha lets you assert this before trusting any other shot++# After flipping a Setup Steps toggle, screenshot the panel to confirm:+adom-bridge hydrogen_shot '{"region":"setup-panel","resizeMax":1568}'++# Full-window dump for general debugging:+adom-bridge hydrogen_shot '{"region":"full"}'+```++Edge cases:+- **Unknown region** - refusal: `{ok:false, error, knownRegions:[...]}`. No fallback - fix the call.+- **`regionFound:false`** - the element rect wasn't in Hydrogen's DOM. The panel/iframe may not be mounted yet, OR Hydrogen's markup changed and `region_selectors()` in `hydrogen_bridge.rs` is out of date. The shot is **still produced** (falls back to the full window) so you have something to look at; check `_hints.regionMiss` for the reason.+- **`runningBuildSha:null`** - Hydrogen's `/buildinfo` isn't exposed yet on the running version. The shot is still produced; you just can't self-verify which build it represents.++**Always pull_file the `safePath` and Read THAT** - it's already sized for Claude's image analysis. The `fullPath` is for shotlog / disk archival, NOT direct Read (may exceed Claude's per-image budget). The `_hints.pull` field gives you the exact `pull_file` command to copy-paste.++**Old workaround to drop:** Do NOT use `desktop_list_windows | filter 'Hydrogen' | desktop_screenshot_window <hwnd>` anymore for Hydrogen - `hydrogen_shot` replaces that entire pattern. Generic `desktop_screenshot_window` still applies to non-Hydrogen windows (other apps, dialogs, etc.).++### TCP Port-forward Tunnels - `tunnel open|close|list|check|rdp` + `display` (v1.8.85+, reworked v1.8.147)++Reach a TCP service addressable only from the **cloud side** through the **Mac side**: Bridge binds a `127.0.0.1:<port>` listener on the Mac and pumps bytes over the existing Bridge↔relay WebSocket; the relay dials the upstream `to`. First use case: RDP into a Windows VM behind an NSG only the cloud container reaches - the RDP client on the Mac connects to a local port and is talking through the cloud. Same path works for SSH, VNC, HTTP, postgres - any TCP.++**`tunnel` is a grouped verb** (`rdp` / `tunnel_close` / `tunnel_list` remain back-compat aliases):++```bash+# Open a VERIFIED tunnel (both legs probed before ok). Auto-picks a free local port.+adom-bridge --target <name> tunnel open '{"to":"127.0.0.1:13389"}'++# RDP: open + verify + launch the RDP client, sized to the Mac's real screen.+adom-bridge --target <name> tunnel rdp '{"to":"127.0.0.1:13389","size":"90%"}'+# size: "90%" (default) | "100%" | "fullscreen" | "maximized"+# OR monitor:<index> (0=primary) OR explicit width+height++adom-bridge --target <name> tunnel list # live tunnels + leak detection+adom-bridge --target <name> tunnel check '{"tunnelId":<id>}' # per-leg health+adom-bridge --target <name> tunnel close '{"tunnelId":<id>}' # or {"all":true}++adom-bridge --target <name> display # monitor geometry (for sizing)+```++**`--target` is mandatory when >1 Bridge is connected.** v1.8.147 fixed a false-success bug: the bind, the upstream probe, and the client launch now ALL resolve to the SAME machine. No target + multiple Bridges → `ambiguous_target` listing the names; `--target all` is rejected (a tunnel binds one machine). `adom-bridge targets` lists connected Bridges.++**Active health verification - every `open`/`rdp`/`check` PROVES the path before reporting `ok`:**+- **local-bind** - Bridge self-probes `127.0.0.1:<boundPort>` right after binding. If it can't connect, Bridge tears the tunnel down and returns `ok:false` (this alone kills the false-success class).+- **upstream-refused** - the relay dials `to` (retry/backoff ~4×/2.5s). If unreachable, the tunnel is torn down (no leaked row) and `ok:false`.+- A failure names the leg: `failedLeg: "relay-to-ad" | "local-bind" | "upstream-refused"`. Health fields surfaced: `listening`, `relayConnected`, `upstreamReachable`, `connCount`, `clientAlive`.++**Progressive hints.** Every response carries `_hint` (what's true now) + `_next` (the exact next verb with real values, copy-pasteable). e.g. a successful open's `_next` hands you the `tunnel rdp` / `tunnel check` / `tunnel close` commands with the real `to`/`target`/`tunnelId` filled in. A missing `to` returns a structured error with an example. Walk the workflow from the responses alone.++**Display-aware RDP sizing.** `tunnel rdp` resolves `size`/`monitor`/`width`/`height` against the Mac's real screen (queried via `display`), writes an `.rdp` file with the geometry, and opens it with the user's RDP client (Microsoft's Windows App / Remote Desktop). e.g. `size:"90%"` on a 2560x1600 primary → a 2304x1440 window. Default 90% of primary. The response carries `clientAlive` (did the client stay up past launch). You never hardcode a resolution - call `display` if you want the layout first.++**`tunnel list` reconciles + reaps leaks.** It cross-checks the relay registry against every connected Bridge's own registry and reports `leaks:[...]` for rows only one side knows about; `tunnel close '{"all":true}'` reaps them. The relay stores the actually-bound `localPort` (so `tunnel close '{"local":<port>}'` works).++Architectural notes for Claude:+- Bytes-only - no payload parsing, so any TCP service works. TLS/cert-pinning still pins to whatever the cloud-side dial resolves (for RDP the upstream cert CN must match what the RDP client expects).+- Each fresh laptop-side TCP connection gets its own connId multiplexed over one tunnelId; concurrent client reconnects / parallel sessions work without re-opening. Backpressure bounded (256-slot mpsc/conn, 64 KiB buf).+- Tunnels don't survive Bridge/relay restarts. If the client disconnects, `tunnel check` (names the dead leg), then re-open.+- The default local port is `13389` + `SO_EXCLUSIVEADDRUSE` + ephemeral fallback (`fellBack:true` in the response; read `localPort` for the real port).++**Old workarounds to drop**: (1) NSG WAN-IP whitelisting for roaming-laptop RDP - the tunnel rides existing auth, reachable regardless of WAN IP. (2) Trusting a bare `ok:true` from the old `rdp` - it could be a dead port; now `ok` means both legs were probed. (3) Hardcoding the RDP resolution - pass `size`/`monitor`; Bridge resolves against the real screen.++### Shell - `shell_execute` (escape hatch only)++- `shell_execute` -- Run a shell command on the desktop. The CLI handles approval polling internally - it returns the final `{success, output, error, exitCode}` once the user clicks Allow on the desktop dialog. **You do not need to poll, retry, or call `get_deferred_result` yourself.** The CLI emits `HINT:` lines to stderr while waiting (every 15s) so an AI Monitor sees progress.+- `shell_kill_all` -- Kill all running shell commands and deny pending approvals.++**v1.7.16 fix.** Earlier versions (v1.7.15 specifically) had a regression where the CLI's approval-wait loop re-sent the command every 1s instead of polling for the deferred result; that produced an infinite "Another shell command is already waiting for approval" loop because each retry created a fresh approval the user could never out-click. v1.7.16 fixes it. If you see that error string with a `>= 1.7.16` CLI, the bug is back - file an issue. If you see it with `< 1.7.16`, upgrade the CLI: `adom-wiki asset get apps/adom-bridge docker_binary -o /usr/local/bin/adom-bridge`.++**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 shell 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 `python3` for Python -- a bare `python` may not exist on macOS.++#### `run_script` - base64-payload script runner (v1.8.46+)++When you want to send a multi-line script to the Mac without the shell-quoting hell that `shell_execute` requires, use `run_script`. The script crosses the wire as base64 so NO escaping survives to be mangled by any shell layer. The caller never hand-escapes anything.++It requires shell auto-approve to be active - same trust level as `shell_execute`. It refuses cleanly with an actionable error if not approved; no per-call modal (it is a programmatic verb).++**Reading the result - `status` + `durationMs` (v1.8.162+).** EVERY CLI result carries a single authoritative **`status`** field so you never have to guess whether an empty `stdout` was a real empty-output success or a timeout:++| `status` | meaning | what to do |+|---|---|---|+| `ok` | completed (stdout may legitimately be empty) | use `exitCode`/`stdout`/`stderr` |+| `timeout` | did NOT finish in time (NOT an empty success) | raise `timeoutSeconds`, or use a detached job (`launch_process`) |+| `busy` | desktop couldn't service it (no client connected) | check `adom-bridge status`; retry |+| `error` | the command/verb failed | read `error` / `stderr` / `_hint` |++`durationMs` is the wall-clock of the call. `exitCode`/`stdout`/`stderr` pass through unchanged; on `timeout` the verb's `partialStdout`/`partialStderr` hold what was written before the kill. A `timeout`/`busy` result also carries a recovery `_hint`. **For multi-minute work don't fight the timeout** - use `launch_process` (detached, returns instantly) and poll.++**`run_script`** - base64 script + interpreter (`bash` default | `zsh` | `sh`):++```bash+SCRIPT_B64=$(python3 -c "import base64; print(base64.b64encode(b'''+echo \"multi-line script\"+VAR=\"value with spaces and \$dollars\"+echo \"VAR=\$VAR\"+exit 0+''').decode())")+adom-bridge run_script "{\"interpreter\":\"bash\",\"scriptB64\":\"$SCRIPT_B64\"}"+# → {success, ok, stdout, stderr, exitCode}+```++Args: `interpreter` (`bash` (default, /bin/bash 3.2) | `zsh` (the macOS login shell) | `sh` (POSIX)), `scriptB64` (required), `timeoutSeconds` (default 60).++The decoded script bytes are piped to the interpreter's **stdin** (no temp file); the interpreter reads and executes without ever opening a file, so nested quotes, `$`, backslashes, newlines and pipes all survive verbatim.++## Detecting App Installation++After connecting, always run `adom-bridge status` to check what's installed. The `desktop.apps` object tells you exactly what the user has:++```bash+adom-bridge 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:++```bash+# 1. Discover what's installed + which profiles are configured.+adom-bridge desktop_list_browsers '{}'+# → {+# "default": "chrome",+# "browsers": [+# { "name": "chrome", "displayName": "Google Chrome", "version": "146...",+# "exePath": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",+# "profiles": [+# { "id": "Default", "name": "Alice (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-bridge desktop_open_url '{+ "url":"https://docs.google.com/document/d/...",+ "browser":"chrome",+ "profile":"Default"+}'+# Or YouTube channel management for the media account:+adom-bridge desktop_open_url '{+ "url":"https://studio.youtube.com",+ "browser":"chrome",+ "profile":"Profile 2"+}'+```++Profile-picking heuristics for Docker Claude:+- **Workspace / @adom.inc URLs** → match `profile.gaia.endsWith("@adom.inc")` and not the `media@` one → typically `Default`.+- **Personal Gmail / Drive / etc.** → match `profile.gaia === "[email protected]"` (or whatever the user's personal address resolves to).+- **YouTube Studio / channel-specific work** → match the media account profile.+- **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):**++Rare on this build: Bridge manages its own portable Node runtime (fetched on first launch, no admin rights). If a `browser_*` command ever returns `errorCode:"node_not_found"`, trigger the managed install:++```bash+adom-bridge desktop_install_node '{}'+# → fetches the managed portable Node runtime (~40 MB, ~30-90s; idempotent).+# → returns {ok:true, installed:true, source:"managed-portable", nodePath, _hint} on success.+```++After install, **just retry the original `browser_*` command** - the bridge auto-spawns with the managed node. Node detection also honors a system Node on PATH and Homebrew installs (`/opt/homebrew/bin`, `/usr/local/bin`). If detection misses a real install on a user's machine, get the actual `node` path from the user - that's a bug to file.++**KiCad not installed:**++`desktop_install_kicad` is not available on macOS (it was a winget verb). Install it FOR the user instead of pointing them at a download page: with shell approval granted, run `brew install --cask kicad` via `shell_execute` (or download the macOS dmg from https://www.kicad.org/download/macos/ and open it for them). Then call `kicad_list_versions` to verify - the kicad bridge caches detection results, so a fresh scan may be needed to pick up a fresh install.++**KiCad detection on macOS** scans `/Applications/KiCad/` and `/Applications/` directly. If detection misses a real install, that's a bug to file - get the actual install path from the user.++**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 relaunch Adom Bridge (or just wait for the bridge's next scan), then run `adom-bridge status` to verify.++### Handling "not running" errors++When `errorCode: "fusion_not_running"`, launch it programmatically with the first-class startup command:+```bash+adom-bridge fusion_start+# Discovers the Fusion install on the Mac and launches it.+# From 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-bridge watch` (see "Live folder progress streaming with `watch`" above) which streams live progress automatically. If another session started it, poll `fusion_addin_status` every 2–5s to check progress.+- Use commands that **don't need the main thread** while waiting:+ - `fusion_addin_status` - check busy state and walk progress+ - `fusion_window_info` - get window HWND, title, dialogs+ - `fusion_screenshot_fusion` - capture what Fusion looks like+ - `fusion_click_fusion` - click in the Fusion window+ - `fusion_send_key` - send keyboard input+ - `fusion_close_window` - close a specific dialog by HWND++The response includes progress info:+```json+{+ "errorCode": "main_thread_busy",+ "busyCommand": "walk_cloud_tree",+ "elapsedSeconds": 42.3,+ "walkProgress": {+ "foldersVisited": 15,+ "filesFound": 87,+ "currentFolder": "Molecules/XRP",+ "queueSize": 8+ },+ "_hint": "Add-in is busy with a long-running command. Do NOT retry..."+}+```++**Stalled walk detection**: If `fusion_addin_status` returns `busy: true` but `walkProgress` is missing and `mainThreadStalled: true`, a modal dialog is blocking the event loop - the walk was dispatched but never started. Call `fusion_dismiss_blocking_dialogs`, then the walk auto-resumes.++### Auto-recovery (`_autoRecovery` field)++When a fusion_* command fails with "not responding" / "not connected", the CLI automatically:+1. Calls `fusion_dismiss_blocking_dialogs` to clear any modal+2. If successful, retries the original command+3. Attaches `_autoRecovery: {action, dismissed[]}` to the retry result++If you see `_autoRecovery` in a response, the command already succeeded after auto-dismissal - no manual intervention needed. The field is informational.++## Troubleshooting++### Check connection status++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 Bridge apps reconnect within seconds.++### No bridge client connected++1. Confirm the Adom Bridge app is running on the user's Mac (menu-bar item present; no Dock icon is normal)+2. Confirm it's pointed at the correct WebSocket URL+3. Check if port 8765 is exposed and reachable++### Relay not running++```bash+curl -sf http://127.0.0.1:8766/health+# If fails: adom-bridge serve --supervise+```++### Shell commands on the Mac++`shell_execute` runs via `sh -c` on the Mac. For multi-line work prefer `run_script` (base64, no quoting hell). Use `python3`, not `python`.++## Building from Source++```bash+cd cli && cargo build --release+# Binary at: cli/target/release/adom-bridge+```++## Repo++[github.com/adom-inc/adom-bridge](https://github.com/adom-inc/adom-bridge)++## Run wiki ops on the user's Mac - `wiki_exec` (v1.9.116+)++Bridge bundles the `adom-wiki` CLI inside the app (Contents/Resources), and `wiki_exec` runs it ON THE MAC - so version-controlling a local project (KiCad board, docs folder) on wiki.adom.inc needs NO per-file `pull_file` round trip to the container and there is no container/Mac drift.++```bash+adom-bridge wiki_exec '{"args":["repo","push","adom/my-board","--files","board.kicad_pcb","board.kicad_sch","-m","rev B"],"cwd":"/Users/me/boards/my-board"}'+```++- `args` = adom-wiki argv (any pillar: repo, pkg, release, discussion). `cwd` = Mac-side project dir so `--files` are project-relative. `timeoutSec` optional (default 120, max 600).+- Returns `{success, exitCode, timedOut, stdout, stderr, exe, durationMs}`.+- Auth is automatic: Bridge injects the signed-in user's session token as `ADOM_WIKI_TOKEN` - the caller never handles credentials.+- Gated like `shell_execute` for relay callers (it writes to the wiki as the user). `errorCode:wiki_cli_missing` means the desktop Bridge predates 1.9.116.+- Container-side publishing of the CONTAINER's own files still uses your local `adom-wiki` directly; `wiki_exec` is for files that live on the Mac.++Note (v1.9.117): Bridge's Settings panel no longer has a KiCad section - bridges own their app-specific config (Bridge stays generic). `kicad_library_paths` / `project_watch` keep working from `~/.adom/config.json` and the `set_project_watch` / `trigger_project_watch` verbs.
skills/adom-bridge-recording/._SKILL.mdadded⋯ 1 unchanged line ⋯
skills/adom-bridge-recording/SKILL.md+2−2@@ -112,11 +112,11 @@ stops manually, treat the clip as complete and react accordingly. - Pull clips to your container with `pull_file`: ```bash-adom-bridge pull_file '{"filePaths":["<the filePath from start/stop>"], "saveTo":"/tmp"}'+adom-bridge pull_file '{"filePaths":["C:\\...\\rec-desktop-....webm"], "saveTo":"/tmp"}' ``` - Adom Bridge's disk janitor auto-prunes its recordings directory- with generous caps: 2 days age,+ (`%TEMP%\adom-bridge-recordings`) with generous caps: 2 days age, 40 files, 3 GB total. Pruning is oldest-first, so the newest files (including an in-progress recording) always survive. Still: pull anything the user wants to keep promptly rather than treating the desktop as
skills/adom-bridge-window-capture/SKILL.md+1−1@@ -30,7 +30,7 @@ This is the deep reference for everything Adom Bridge can capture on a Mac and e - Capture runs through the system `screencapture` tool (`-l <id>` per window; `-x` no shutter sound, `-o` no drop shadow, PNG output). Both window and screen captures are downscaled at capture time to at most **1568 px** on the longest side, so a Retina grab is never shipped raw. - **App modal dialogs:** a sheet attached to a window renders INSIDE that window's capture. A separate dialog window is its own on-screen top-level window: find it with `desktop_list_windows` / `desktop_find_window` and capture it by its own id. There is no owned-popup `screenshots[]` array on macOS (that was a Windows mechanism); `kicad_screenshot_all` is the one verb that returns a multi-window array. - **Multi-monitor:** `desktop_screenshot_screen` captures the MAIN display only. There are no `{monitor:N}` or `{perMonitor:true}` options on macOS. Enumerate displays with `desktop_list_monitors` (below).-- **Video recording is separate from this skill.** `desktop_record_window_start/_stop/_status` records the screen via macOS `screencapture -v` to a pullable `.mov`; the HUD-based whole-desktop `.webm` recorder is `desktop_record_start` (see the `adom-bridge-recording` skill); recording a browser TAB is the pup bridge's own CDP feature (`browser_record_start`).+- **No video recording on macOS.** The native recording verbs return a clean `recording_unsupported` error. Recording a browser TAB is the pup bridge's own CDP feature (`browser_record_start`), unrelated to this skill. ---
skills/adom-bridge-window-identity/._SKILL.mdadded⋯ 1 unchanged line ⋯
skills/adom-bridge-window-identity/SKILL.mdadded+106@@ -0,0 +1,106 @@+---+name: adom-bridge-window-identity+description: Make a window an AI drives present itself as YOUR app on Windows — its taskbar button, Alt-Tab entry, hover name, jump-list (right-click menu), taskbar overlay badge, and pins. Covers the AUMID model, the register+stamp two-halves rule, per-session identities, jump-list tasks + header branding, taskbar progress/overlay, and the icon-format rules (why an icon renders as a generic white document and how to fix it). Use when a bridge borrows a host window (e.g. Chrome for Testing) and it reads as the wrong app.+---++# Window identity and the taskbar — Adom Bridge's AUMID verbs++This is an advanced corner of the Windows shell that few APIs expose cleanly. Adom Bridge (Adom Bridge) wraps it in a small, **generic** verb family: you pass a name + icon + (optionally) a command, and Adom Bridge makes any window — even one owned by another process — present itself as *your* app across every shell surface. Adom Bridge knows nothing about which app you are; it applies what you pass.++The motivating case: a bridge drives a **Chrome for Testing** window, which otherwise reads *"Test"* with Chrome's icon in the taskbar and Alt-Tab — two brands and a confusing word. These verbs make it read as your bridge instead.++## The one concept you must hold: the AUMID++Windows groups and labels windows by their **Application User Model ID** (AUMID) — a short dotted string like `Adom.Pup.session42`. The AUMID is what the shell resolves to get:++- the **taskbar button** grouping, its **icon**, and its **name**+- the **Alt-Tab** entry (icon + name)+- the **jump list** (the right-click menu on the taskbar button) and its **header row** (the app tile above *Pin to taskbar* / *Close window*)+- **toasts** you raise, and how a user **pins** the window++A window has *some* AUMID whether you set one or not (it inherits the owning process's). Your job is to (1) give the window a **specific** AUMID and (2) tell Windows what that AUMID **means** (its name + icon). Those are two different operations — miss either and the identity is half-wrong.++## The two halves — you need BOTH++| Half | Verb | What it does |+|---|---|---|+| **Stamp the window** | `desktop_set_window_identity {hwnd, appId, iconPath, displayName, relaunchCommand}` | Puts the AUMID on the *live* HWND (`PKEY_AppUserModel_ID`) + sets the title-bar/Alt-Tab icon (`WM_SETICON`) + the Relaunch trio for the jump-list header. |+| **Register the AUMID** | `desktop_register_app_identity {appId, displayName, iconPath, shortcut?}` | Teaches Windows what that appId *means*: an HKCU `AppUserModelId\<appId>` key (name + icon) and, when `shortcut:true`, a Start Menu shortcut carrying the AUMID (what makes it **pinnable**). |++Stamp without register → Alt-Tab / the taskbar fall back to the owning exe's icon (the "Test / Chrome icon" problem). Register without stamp → nothing points the window at your AUMID. **Do both.**++`desktop_unregister_app_identity {appId}` removes the registration (HKCU key + any Start Menu shortcut carrying that AUMID) — call it when a per-session identity is done.++Everything here is **HKCU only** — no elevation, no UAC prompt, ever.++## Per-session identities (`shortcut:false`)++For a window that comes and goes (one browser session), you want a **distinct** AUMID per window so each keeps its own taskbar button, but you do NOT want a Start Menu shortcut per session (that would spray dead entries). Register with `shortcut:false`:++```+desktop_register_app_identity {appId:"Adom.Pup.session42", displayName:"Adom Pup", iconPath:"C:\\...\\pup.ico", shortcut:false}+desktop_set_window_identity {hwnd:<cft hwnd>, appId:"Adom.Pup.session42", iconPath:"C:\\...\\pup.ico", displayName:"Adom Pup", relaunchCommand:"<how to reopen>"}+```++Registry-only registration gives Alt-Tab + taskbar hover the right name + icon with no Start Menu spam. Pair every `shortcut:false` register with an `unregister` at session close. Such an identity is **not pinnable** (no shortcut) — keep a separate shortcut-mode registration for a launcher identity you want users to pin.++## Jump lists — the right-click menu + its header++`desktop_set_window_jumplist {appId, tasks, hwnd?, headerIcon?, headerCommand?, headerName?}` attaches a custom right-click menu to the AUMID. Each task: `{title, target?, args?, iconPath?, description?}` (target defaults to Adom Bridge's bundled CLI, so a task can just be a verb + JSON). `tasks:[]` clears the list.++- Tasks appear on the taskbar right-click menu of **every** window stamped with that AUMID, so a per-session appId gives a per-window menu.+- **The header row** (the app tile above Pin/Close) is a *separate* surface. It is painted from the **window's Relaunch trio**, not the AUMID registry icon — so a `shortcut:false` AUMID shows a generic icon there unless you brand it. Pass `hwnd` + `headerIcon` (and optionally `headerCommand`/`headerName`) and Adom Bridge stamps the window's Relaunch trio so the header renders your icon **without** a Start Menu shortcut. `headerBranded:true` in the response confirms it stuck.++## Taskbar status — progress + overlay badge++`desktop_taskbar {hwnd, progress?, overlay?, flash?}` paints **glanceable status** on any window's taskbar button (works cross-process), so you can show what the AI is doing on the real app window it drives:++- `progress:{state:"normal"|"paused"|"error"|"indeterminate"|"none", value?}` — a colored bar (green / yellow = human-in-the-loop like a captcha / red / marquee).+- `overlay:{badge:"adom"|"none", icon?:<.png/.ico path or base64>, avatar?, tooltip?}` — a small badge in the button corner. For Chrome/Edge, that overlay slot holds the **profile avatar**; pass `avatar` and Adom Bridge composites your mark onto it so the user keeps work-vs-personal.+- `flash:"until_focused"|"count"|"stop"` — flash the button for attention.++`desktop_flash_window` is the lightweight flash-only cousin.++## Toasts++`notify_user` raises a native Windows toast under Adom Bridge's own identity (AUMID `inc.adom.bridge`, the Adom logo). See the `adom-bridge-notify` skill for the full toast catalog (progress, sticky reminders, buttons that round-trip a click).++## Icon formats — why an icon renders as a generic white document++This is the #1 source of "my icon doesn't show." The identity verbs return an **`iconCheck`** block that inspects the file you passed and lists concrete problems. The rules it encodes:++- **Give a multi-size `.ico`** (16 / 24 / 32 / 48 / 256). A single-size icon looks bad where the shell wants a different size.+- **The 256×256 size MUST be PNG-compressed.** Since Windows Vista, a 256px `.ico` entry stored as an uncompressed BMP/DIB renders as the **generic white-document icon** on the jump-list header and taskbar. This is subtle: the icon looks fine in an image viewer but wrong in the shell. Re-export the `.ico` with the 256px size PNG-compressed (most icon tools do this; if unsure, drop the 256px size). `iconCheck` flags this specifically.+- **A per-window `RelaunchIconResource` (the jump-list header) for a `.ico` uses a BARE path** — Adom Bridge handles this; you just pass the `.ico`. A stray `path,0` (the PE-resource form) breaks a `.ico`, because the shell then looks for "resource index 0 of a PE module," which a `.ico` is not → generic icon. (This was a real bug, fixed Adom Bridge 1.9.151.)+- **A PE module (`.exe`/`.dll`) is referenced as `<path>,<index>`.** Adom Bridge uses `,0`; pass your own index if your icon isn't resource 0. A standalone `.ico` is simpler.+- **SVG does not work** — the Win32 shell can't load it. Convert to `.ico`/`.png`.+- **The AUMID registry icon (taskbar/Alt-Tab) accepts `.ico` or `.png`; an `.exe` path there renders the blank-doc icon.** A taskbar **overlay** wants a small (~16px) image; a `.png` is ideal (Adom Bridge converts it to `.ico` internally).++If `iconCheck.ok` is false, fix the file per `iconCheck.issues` and re-stamp — don't ship the broken icon.++## The live-tile trap (the one timing gotcha)++Re-stamping the **same** appId on a **live** window with a new icon updates the title bar + Alt-Tab but **not** the taskbar tile: Win11 bakes the tile when the button is *created* and never re-reads it (WM_SETICON, ITaskbarList DeleteTab+AddTab, SHChangeNotify, RedrawWindow — all measured no-ops). The only non-disruptive way to change a live tile is to make the shell create a **new** button: register a **different** appId (registry-only, new icon) and stamp the window with it — the button is destroyed and recreated with the new tile in ~1s, no window flicker. Cost: a pin made under the old appId shows the running window as a separate button, so use a **stable** appId for identity you want pinnable and a **varying** one only when the tile must track state.++## Recipe: brand a borrowed window end-to-end++```+# 1. Teach Windows the identity (once per install for a stable id; per session for shortcut:false)+desktop_register_app_identity {appId:"MyBridge.Session7", displayName:"My Bridge", iconPath:"C:\\bridge\\icon.ico", shortcut:false}++# 2. Stamp the live window (taskbar + Alt-Tab + title bar) and brand the jump-list header (Relaunch trio)+desktop_set_window_identity {hwnd:<hwnd>, appId:"MyBridge.Session7", iconPath:"C:\\bridge\\icon.ico", displayName:"My Bridge", relaunchCommand:"mybridge --reopen 7"}++# 3. Attach a right-click menu (optional) + brand its header row+desktop_set_window_jumplist {appId:"MyBridge.Session7", hwnd:<hwnd>, headerIcon:"C:\\bridge\\icon.ico",+ tasks:[{title:"New tab", args:"browser_open_window {}"}]}++# 4. Show live status on the button while you work (optional)+desktop_taskbar {hwnd:<hwnd>, overlay:{badge:"adom", tooltip:"My Bridge is driving"}, progress:{state:"indeterminate"}}++# 5. On session close+desktop_taskbar {hwnd:<hwnd>, progress:{state:"none"}, overlay:{badge:"none"}, flash:"stop"}+desktop_unregister_app_identity {appId:"MyBridge.Session7"}+```++Resolve `<hwnd>` with `desktop_list_windows` / `desktop_find_window`.
skills/adom-bridge-windows-host/._SKILL.mdadded⋯ 1 unchanged line ⋯
skills/adom-bridge-windows-host/SKILL.mdadded+163@@ -0,0 +1,163 @@+---+name: adom-bridge-windows-host+user-invokable: false+description: "Read this BEFORE doing any work on adom-bridge from a Claude Code session running directly on the user's Windows machine. Explains how to route everything through the adom-bridge CLI so paths, file operations, and process spawns reach the user's real filesystem instead of getting silently virtualized into Claude Code's Windows AppContainer sandbox. Skipping this causes hour-long debugging spirals where 'open install folder' fails, bridges install to the wrong place, screenshots come back blank, and synthesized clicks don't reach the WebView2."+---++# Working on adom-bridge from Claude Code on Windows++This skill applies when Claude Code is running **on the user's Windows machine** — i.e. you have a `bash`/`PowerShell` tool that hits the local filesystem directly, instead of running inside the Adom Docker container.++## The single most important fact++Claude Code on Windows runs inside a **Windows AppContainer sandbox**. Every filesystem write from your `bash` or `PowerShell` tool gets transparently redirected:++| What you write | Where it actually lands |+|---|---|+| `C:\Users\<you>\AppData\Local\Adom Bridge\...` | `C:\Users\<you>\AppData\Local\Packages\Claude_<pkgid>\LocalCache\Local\Adom Bridge\...` |+| `C:\Users\<you>\Documents\...` (any AppData/user-roaming write) | The Packages\Claude_<pkgid>\LocalCache equivalent |++**From your perspective both paths show identical contents.** The redirect is invisible inside the sandbox. From the user's real filesystem (their explorer.exe, their PowerShell from Start menu), only the `Packages\Claude_<pkgid>\...` location exists.++**Consequence:** If you launch the GUI from your `bash` (`start "" "C:\...\adom-bridge.exe"`), the GUI inherits your AppContainer. Bridges it installs land in the virtualized cache. The user clicks "Open install folder" and Explorer says **"Location is not available"** because the path the GUI wrote into the registry doesn't exist in the user's real session.++This is the entire reason "Open install folder" took an hour to ship in v1.8.11. Don't repeat it.++## The fix: drive the GUI through `adom-bridge` CLI like Docker does++Instead of using your own `bash`/`PowerShell` for anything user-visible, route every operation through the `adom-bridge` CLI. The CLI talks to a relay (HTTP) which dispatches to the running GUI (WS). The GUI executes commands in **its own process context** — which, when launched correctly, is the user's normal Windows session, unsandboxed.++### Setup (do this once per session)++1. **Pick alt ports if 8765/8766 are taken.** Docker Desktop typically holds `8765` and `8766` for its internal use. Check with `netstat -ano | grep -E ":876[56] "`. Use `8865`/`8866` (or any free pair).++2. **Start the local relay on the alt ports:**+ ```bash+ nohup C:/Github/adom-bridge/cli/target/release/adom-bridge.exe serve \+ --http-port 8866 --ws-port 8865 > /tmp/relay.log 2>&1 &+ ```+ Verify: `cat /tmp/relay.log` should show `Adom relay server started`.++3. **Add the alt-port localhost server to `~/.adom/config.json`** so the GUI auto-connects on launch:+ ```json+ {+ "auth_token": "<your-token>",+ "auto_connect": true,+ "enabled": true,+ "id": "local-dev-server",+ "name": "localhost",+ "url": "ws://localhost:8865"+ }+ ```+ (Edit the existing `localhost` entry if it's already there — just enable it and bump the port.)++4. **Launch the GUI via `cmd /c start ""`** — this routes through the Windows Shell broker, which spawns the new process in the user's normal session, breaking your AppContainer inheritance:+ ```bash+ cmd //c start "" "C:\Users\<you>\AppData\Local\Adom Bridge\adom-bridge.exe"+ ```+ **DO NOT** use bare `start "" "..."` from your bash — that inherits your sandbox.++5. **Set `ADOM_API` env var** so the CLI hits your alt port:+ ```bash+ export ADOM_API="http://127.0.0.1:8866"+ ```+ Pin this near the top of every command sequence; it doesn't persist across shells.++6. **Verify the GUI is unsandboxed.** Run via shell_execute and check the token:+ ```bash+ adom-bridge shell_execute '{"command":"powershell -NoProfile -ExecutionPolicy Bypass -Command \"Add-Type -TypeDefinition @\\\"using System; using System.Runtime.InteropServices; public class T { [DllImport(\\\\\\\"advapi32.dll\\\\\\\")] public static extern bool OpenProcessToken(IntPtr p, int a, out IntPtr t); [DllImport(\\\\\\\"advapi32.dll\\\\\\\")] public static extern bool GetTokenInformation(IntPtr h, int c, IntPtr b, int l, out int r); [DllImport(\\\\\\\"kernel32.dll\\\\\\\")] public static extern IntPtr GetCurrentProcess(); }\\\"@; $t=[IntPtr]::Zero; [void][T]::OpenProcessToken([T]::GetCurrentProcess(), 8, [ref]$t); $b=[Runtime.InteropServices.Marshal]::AllocHGlobal(8); $l=0; [void][T]::GetTokenInformation($t, 29, $b, 8, [ref]$l); Write-Host TokenIsAppContainer=$([Runtime.InteropServices.Marshal]::ReadInt32($b))\"","timeout_secs":5}'+ ```+ Expect `TokenIsAppContainer=0`. If it returns `1`, you launched the GUI from a sandboxed parent — kill it and relaunch via `cmd //c start ""`.++### Everything you do from here goes through the CLI++| Need to... | Use this | NOT this |+|---|---|---|+| Run PowerShell on user's machine | `adom-bridge shell_execute '{"command":"powershell ...","timeout_secs":N}'` | Your own `PowerShell` tool |+| Screenshot a window | `adom-bridge desktop_screenshot_window '{"hwnd":N}'` | Win32 `PrintWindow` from your PowerShell |+| Screenshot the screen | `adom-bridge desktop_screenshot_screen` | Your own screen capture |+| Find windows | `adom-bridge desktop_list_windows` | UIA enumeration from your PowerShell |+| Install/uninstall a bridge | `adom-bridge bridge_install`, `bridge_uninstall` | Manually copying zip contents to the cache dir |+| Inspect the bridge registry | `adom-bridge bridge_list` | Reading `bridge.json` from disk paths |+| Open a folder in Explorer | (test by invoking the same Rust code path; see below) | `Start-Process explorer.exe` from your bash |++**Quoting trick for `shell_execute`:** the JSON arg gets parsed once, then PowerShell parses it again. Backslashes need quadrupling (`\\\\` for a literal `\`). When the command is non-trivial, **write it to a `.ps1` file and `shell_execute` the file** instead of inlining:+```bash+cat > /c/tmp/probe.ps1 << 'EOF'+# ... powershell here, single-quoted heredoc so no shell interpolation+EOF+adom-bridge shell_execute '{"command":"powershell -NoProfile -ExecutionPolicy Bypass -File C:\\\\tmp\\\\probe.ps1","timeout_secs":10}'+```++## Specifically: opening folders in Explorer++This is the one that took TWO hours across multiple iterations. Lessons crystallized:++1. **`explorer.exe /select,<path>`** — the only reliable form. Opens a NEW Explorer window navigated to the path's parent, with the path itself pre-selected. Use this in `open_path_in_explorer`-style Tauri commands.+2. **`explorer.exe <path>` (no flag)** — silently no-ops when an Explorer process is already up. Don't use.+3. **`ShellExecuteW("open", <path>)`** — returns success (HINSTANCE > 32) but often doesn't visibly open a window. Don't use for folders.+4. **`cmd /c start "" "<path>"`** — same fate as #2 for folders. Fine for URLs (used by `open_external_url`).++### The `raw_arg` trap++`std::process::Command::new("explorer").arg("/select,C:\\path with space\\hello").spawn()` does **NOT** work. Rust auto-wraps any arg containing spaces in double quotes, emitting:++ explorer.exe "/select,C:\path with space\hello"++Explorer parses the whole quoted string as one token, fails to match `/select,`, and falls back to opening the user's Documents folder. The user reports "Open install folder takes me to Documents."++What Explorer actually wants:++ explorer.exe /select,"C:\path with space\hello"++— quotes around the **path portion only**, not the whole arg. The fix is `std::os::windows::process::CommandExt::raw_arg`, which emits the literal command-line bytes without Rust's escaping rules:++```rust+use std::os::windows::process::CommandExt;+let raw = format!(r#"/select,"{}""#, win_path);+std::process::Command::new("explorer").raw_arg(&raw).spawn()?;+```++**General rule**: when invoking a Windows exe with a quirky / legacy command-line parser (`explorer`, `msbuild`, `cl`, some `cmd` builtins), assume `Command::arg` will mis-quote. Use `raw_arg` and construct the command line by hand. You can verify the emitted command line via `shell_execute` running a PowerShell simulation:++```powershell+$psi = New-Object System.Diagnostics.ProcessStartInfo+$psi.FileName = "explorer.exe"+$psi.Arguments = '/select,"' + $path + '"' # ← matches raw_arg output+$psi.UseShellExecute = $false # ← matches Rust's CreateProcessW path+[void][System.Diagnostics.Process]::Start($psi)+```++Then enumerate `(New-Object -ComObject Shell.Application).Windows()` to see what location Explorer actually navigated to. If it's the path you wanted: ship. If it's `Documents` / `Home`: your quoting is wrong.++Before passing the path to explorer, normalize:+- Replace `/` with `\` — `BridgeRegistry` stores cache paths with forward slashes via `dirs::data_local_dir()`.+- Strip `\\?\` prefix — Tauri's `resource_dir()` returns paths in this verbatim form for bundled bridges; Explorer rejects it.++**Always check the path exists BEFORE shelling out**, and return a structured error if it doesn't. The Rust side:+```rust+if !p.exists() {+ return Err(format!("Path does not exist: {}", path));+}+```+catches the sandbox-write-to-wrong-place class of bugs early. If you see this error, you almost certainly installed the bridge while your GUI was sandboxed; reinstall via the CLI-driven user-session GUI.++## Debugging checklist when something "doesn't work"++If a user-visible action (open folder, screenshot, bridge install) seems broken, before iterating on the Rust code:++1. **Is the GUI unsandboxed?** Run the TokenIsAppContainer probe above. If `1`, the entire path is poisoned — relaunch via `cmd //c start ""` and reinstall any cache state.+2. **Are you using `ADOM_API=http://127.0.0.1:8866`?** If your CLI hits the default 8766 and Docker has it, you get `Connection Failed`.+3. **Does the path EXIST on the user's real filesystem?** Use `shell_execute` to run `Get-ChildItem <path>` — that's the source of truth. Don't trust `ls` from your own bash.+4. **Did `taskkill /F /IM adom-bridge.exe` collateral-kill the relay?** The CLI binary and the GUI binary share the same exe name. Kill by PID, not by image name, so the relay survives.+5. **Are you reading explorer's view via COM?** Explorer.exe caches folder listings aggressively and can show stale state. Trust `Get-ChildItem` (kernel API) over `Shell.Application.Windows().Document.Folder.Items()` (cached UI view) — the FS API is the truth.++## Why this gets re-learned every session++Every Claude on the Windows host starts in the sandbox without knowing it. Path operations *appear* to work — your `Test-Path` returns True, your `ls` shows files. The redirect is invisible until something user-visible (Explorer, another non-sandboxed process) tries to use the same path and finds it missing.++Read this skill at session start when you're working on adom-bridge locally. It saves an hour.++## Related memory++- `~/.claude/projects/C--Github-adom-bridge/memory/feedback_dont_kill_gui_during_iter.md` — don't kill the GUI repeatedly during iteration. The CLI-driven approach above means you almost never *need* to kill the GUI: changes that don't touch Rust ship without a rebuild via `release-bridge.sh`, and changes that do touch Rust use FAST tauri (~1-2 min) with a single GUI restart.