app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
← Commit history
Update 127 file(s)
23 files changed
+2336−1464
BRIDGE_VERSION+1−1addin/AdomBridge/AdomBridge.manifest+1−1addin/AdomBridge/commands/manufacturing.py+15−7bridge.json+1−1contracts/describe.json+1427−1413dashboard/Cargo.lock+1−1dashboard/Cargo.toml+1−1describe.py+2−2install.sh+1−1package.json+1−1page.json+11−27rust/Cargo.toml+2−2rust/PORTING.md+8rust/bridge/Cargo.toml+2rust/bridge/src/main.rs+223−4rust/core/src/aps.rs+208rust/core/src/lib.rs+1rust/lbr/Cargo.toml+1rust/lbr/src/lib.rs+280server.py+124−1tools/freeze-contract.py+22tools/promote.py+1tools/release.sh+2−1BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.351+1.9.352
addin/AdomBridge/AdomBridge.manifest+1−1@@ -6,7 +6,7 @@ "description": { "": "Adom Desktop Conduit bridge add-in. Exposes a localhost HTTP API for external automation of Fusion 360." },- "version": "1.0.17",+ "version": "1.0.18", "runOnStartup": true, "supportedOS": "windows" }
addin/AdomBridge/commands/manufacturing.py+15−7@@ -849,9 +849,15 @@ def handle_set_design_rules(app: adsk.core.Application, args: dict) -> dict: layer_count = detection["layerCount"] detection_method = detection["method"] - # Resolve the .edru file- edru_filename = f"Adom-InstaPCB-{layer_count}-Layer.edru"- edru_path = os.path.join(resources, "dru", edru_filename)+ # Resolve the .edru file: a fab pack the bridge staged (druPath, fetched from the wiki page+ # adom/pcb-design-rules), else the bundled Adom InstaPCB rules for this layer count.+ dru_path_arg = (args.get("druPath") or "").replace("\\", "/")+ if dru_path_arg:+ edru_path = dru_path_arg+ edru_filename = os.path.basename(edru_path)+ else:+ edru_filename = f"Adom-InstaPCB-{layer_count}-Layer.edru"+ edru_path = os.path.join(resources, "dru", edru_filename) if not os.path.exists(edru_path): return {@@ -881,13 +887,15 @@ def handle_set_design_rules(app: adsk.core.Application, args: dict) -> dict: return { "success": True, "output": (- f"Adom {layer_count}-layer design rules loaded from {edru_filename}. "+ f"{'Fab' if dru_path_arg else 'Adom'} {layer_count}-layer design rules loaded from {edru_filename}. " f"Detection method: {detection_method}." ), "message": (- f"Adom InstaPCB {layer_count}-Layer design rules are now active. "- f"Min trace: 0.08mm (3mil), min spacing: 0.08mm (3mil), min drill: 0.2mm (8mil). "- f"Rule violations will now show as DRC markers in the board editor."+ (f"Design rules from {edru_filename} are now active. Rule violations will now show as "+ f"DRC markers in the board editor.") if dru_path_arg else+ (f"Adom InstaPCB {layer_count}-Layer design rules are now active. "+ f"Min trace: 0.08mm (3mil), min spacing: 0.08mm (3mil), min drill: 0.2mm (8mil). "+ f"Rule violations will now show as DRC markers in the board editor.") ), "data": { "edruFile": edru_filename,
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.351",+ "version": "1.9.352", "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.", "homepage": "https://wiki.adom.inc/adom/fusion-bridge", "author": "Adom Inc.",
contracts/describe.json+1427−1413@@ -6,421 +6,260 @@ "verbs": [ { "example": {- "libraryPath": "C:/Temp/PART.lbr"+ "limit": 20 }, "input": {- "libraryPath": "str (Windows .lbr path)"+ "limit": "optional int (default 50)" }, "longRunning": true,- "name": "fusion_preflight_lbr",+ "name": "fusion_action_ledger", "output": {- "preflight": "{ok, checks[], errors[]}",- "success": "bool"+ "actions": "[{at, action, detail}]" }, "statusVerb": null,- "summary": "PRE-ANALYZE a .lbr: will Fusion open it? Offline and instant - no Fusion needed, nothing launched. Runs the named checks the bridge learned from real Fusion rejections: file-readable, size-sane (truncated-transfer corpse detection), xml-parses, root-is-eagle, packages-nonempty (Fusion's silent 'No Components'), symbols-present, devicesets-present (missing ones make EDIT raise Create-new prompts), connects-resolve (dead wiring renders fine and connects nothing). fusion_open_lbr and fusion_create_library run the same gate automatically; call this directly to gate a batch BEFORE staging.",- "timeoutSeconds": 10- },- {- "example": {- "libraryName": "MyLib",- "zipPath": "C:/Temp/MyLib.zip"- },- "input": {- "capture": "optional bool (default true: bind + prove; false: place only)",- "lbrPath": "str (staged .lbr, STEP models beside it)",- "libraryName": "optional str (default: file stem)",- "zipPath": "str (staged bundle zip on THIS machine) - or"- },- "longRunning": true,- "name": "fusion_install_library_bundle",- "output": {- "fusion": "{installed, running, addinReady, status, message}",- "installed": "bool",- "libraryPath": "str",- "partMetadata": "{ok, summary, warnings[]}",- "partsToBind": "int",- "partsTotal": "int",- "runId": "str",- "statusVerb": "fusion_prove_result",- "unbound": "[{package, why}]"- },- "statusVerb": "fusion_prove_result",- "summary": "COMPOSITE, the Fusion twin of kicad_install_library_bundle (#68): ONE call takes a staged Cloud Library bundle (zip of <Lib>.lbr + <Part>.step models, or a bare lbrPath with STEPs beside it), unpacks it safely, places it under ~/Documents/Adom Libraries/<name>/, pairs EVERY deviceset with its STEP by name (deviceset, package, normalised, prefix; a one-part library pairs its single model regardless), lints part numbers (partMetadata: MPN, manufacturer, Mouser/DigiKey/LCSC - reported, never blocking), pre-flights Fusion state (fusion{status}), and starts the same bind -> prove chain fusion_create_library runs. ASYNC BY CONTRACT: returns IMMEDIATELY with runId + statusVerb because a STEP bind is 3-15 minutes and cannot fit a relay window; poll fusion_prove_result (measured percent, four streamed proof frames per part, provedParts[], and an addin_restart_required payload the CALLER turns into the ask). PARTIAL SHIPS: a deviceset with no model is a NAMED skip in unbound[], the rest still bind; partsBound < partsTotal is the signal. Each part is minted on its own single-deviceset library (the Package3D generator loads the FIRST package of the open library, so a merged library would put every chip on part one's footprint), with the STEP placed exactly as published (orient:false, origin trusted), then all urns are bound into the merged library, and after prove the bound library is SAVED TO THE CLOUD under <project>/Adom Libraries so it appears in Fusion's Library Manager (cloudLibrary{saved, fileId, duplicatesBefore} in fusion_prove_result). Per-part scaffolding is deleted; the user is left with one library plus its per-package f3d folder. capture:false only places the files and returns. This is the one contract the wiki CLI (`library install --format fusion`) and the Export button both read; the recipe has one owner.",- "timeoutSeconds": 60+ "summary": "Read the append-only record of what the bridge did in the BACKGROUND on this machine - dialogs it answered, Untitled scratch docs it swept, Fusion auto-starts, add-in restarts. Survives bridge respawns (~/.adom/fusion-bridge/actions.jsonl). This is the audit trail for why something changed on the user screen.",+ "timeoutSec": 15 }, { "example": {- "libraryPath": "C:/Temp/PART.lbr",- "stepPath": "C:/Temp/PART.step"+ "name": "TJA1050" }, "input": {- "kicadFootprintPath": "optional str",- "kicadSymbolPath": "optional str",- "libraryPath": "optional str (shape A, or output path for B)",- "name": "optional str",- "parts": "optional [{package, lbrPath, modelPath}] - THE canonical shape, any count",- "pinMap": "optional {pinName: padName}",- "stepPath": "optional str (required for proven)"+ "name": "required str (document name as reported by fusion_get_app_state)" },- "longRunning": true,- "name": "fusion_create_library",+ "longRunning": false,+ "name": "fusion_activate_document", "output": {- "started": "bool",- "statusVerb": "str"+ "activeDocument": "str",+ "success": "bool" },- "statusVerb": "fusion_prove_result",- "summary": "ONE-CALL library creation for web-control (one call = one consent). EVERY LIBRARY IS MULTI-PART - a single-part library is simply parts:[one entry], and there is no separate single-part verb to choose. Pass parts:[{package, lbrPath, modelPath}] for any number of parts; the older {libraryPath, stepPath} shape still works and is normalised into a one-element parts list, so both run the identical code path and the ETA scales with the part count either way. TWO entry shapes: A) {libraryPath, stepPath} - a ready .lbr plus the 3D; B) {kicadSymbolPath, kicadFootprintPath, stepPath, pinMap?} - KiCad sources; needs adom-lbr, which is Linux-container-only by platform rule, so on Windows/Mac boxes shape B refuses IMMEDIATELY (adom_lbr_unavailable, before any staging or Fusion launch - fusion_readiness reports kicadIngest up front). Convert at AUTHORING time in a container and use shape A. pinMap = explicit {pinName: padName} for when pin numbers do not match pad names (BGA-style). Chains convert -> bind 3D -> prove (four evidence frames + verdict) in the BACKGROUND and returns immediately; poll fusion_prove_result (stage narrates; top-level `verdict` proven/not_proven is the score - `stage:done` only means the chain finished). EVERY PART IS PROVED, not just the first: a 10-component library streams 40 evidence frames, FOUR per component (symbol, footprint, 3d, ganged) in assembly order, each tagged with `part` so a front end can group them as they arrive, and `provedParts[]` reports per-part pass/fail. The library is proven only if EVERY part is - one good part does not vouch for nine unchecked ones. On the FINAL fetch pass {inlineFrames:true} to get each evidence frame as a base64 thumbnailDataUrl a browser can render (plain polls return box paths only). The chain closes its library document on success AND failure - batches need no between-part sweeping. stepPath is required for a PROVEN library - no real 3D body without it.",- "timeoutSeconds": 30+ "statusVerb": null,+ "summary": "Make a named open document the ACTIVE one before driving it. EDIT and most modelling verbs act on whatever is active, so in a batch with several documents open this is how you stop a verb landing in the wrong file (page issue #47). Prefer passing expectDocument on the verb itself, which fails closed instead of merely re-targeting.",+ "timeoutSec": 60 }, { "example": {}, "input": {}, "longRunning": true,- "name": "fusion_start",+ "name": "fusion_addin_status", "output": { "addinReady": "bool",- "status": "str",- "success": "bool"+ "busy": "bool",+ "port": "int",+ "version": "str" }, "statusVerb": null,- "summary": "Launch Fusion 360 (if needed) and wait for the AdomBridge add-in to be ready.",- "timeoutSeconds": 150+ "summary": "Is the AdomBridge add-in loaded and answering inside Fusion? Reports addinReady, port and version, plus busy when a long command holds the main thread. If Fusion is running but this says not ready, the add-in was installed while Fusion was open - Fusion loads add-ins only at launch - so call fusion_restart_for_addin (ask the user first when one is present).",+ "timeoutSec": 15 }, { "example": {}, "input": {- "skipCleanClose": "optional bool"+ "sinceSec": "optional float (default 300)" }, "longRunning": false,- "name": "fusion_stop",+ "name": "fusion_alerts", "output": {- "output": "str",- "success": "bool"+ "alerts": "[{at,text}]" }, "statusVerb": null,- "summary": "Gracefully STOP Fusion 360: close docs cleanly + WM_CLOSE, then wait. NO force-kill - the clean opposite of fusion_start. If a window will not close it returns failure; use fusion_kill then.",- "timeoutSeconds": 60+ "summary": "Fusion's OWN alert toasts, read from its log. Fusion draws error/warning toasts in CEF: they are invisible to Win32 enumeration AND to UIA, so a verb can look successful while Fusion is visibly complaining. This reads them, newest last.",+ "timeoutSec": 30 }, { "example": {}, "input": {- "skipCleanClose": "optional bool"+ "folderId": "optional str",+ "hubId": "optional str",+ "projectId": "optional str" }, "longRunning": false,- "name": "fusion_kill",+ "name": "fusion_aps_browse", "output": {- "output": "str",- "success": "bool"+ "items": "[{name,type,id}]" }, "statusVerb": null,- "summary": "Force-KILL Fusion 360 (taskkill /F) - the desperate path for when fusion_stop cannot close it (stuck modal / wedged process). Closes docs cleanly first when reachable.",- "timeoutSeconds": 30+ "summary": "Browse hubs/projects/folders without launching Fusion.",+ "timeoutSec": 60 }, { "example": {- "hwnd": 123456+ "name": "Adom AI Workspace",+ "parentFolderId": "urn:...",+ "projectId": "a.YnVz..." }, "input": {- "hwnd": "required int (the dialog window)"+ "name": "required str",+ "parentFolderId": "required str",+ "projectId": "required str" }, "longRunning": false,- "name": "fusion_close_window",+ "name": "fusion_aps_create_folder", "output": {- "success": "bool"+ "folderId": "str" }, "statusVerb": null,- "summary": "Close a DIALOG window by hwnd (WM_CLOSE, like clicking X) - for dialogs without Cancel/Escape handling (e.g. Recovered Documents). NOT a document closer; refuses without hwnd. Get the hwnd from a screenshot's dialogs array or desktop_list_windows. (Doc drift caught by the verb test suite 2026-08-14: this was described as 'close the active document' while the handler required hwnd.)",- "timeoutSeconds": 30+ "summary": "Create a subfolder in a project (free; needs data:create scope). Use this to make an 'Adom AI Workspace' folder + per-task subfolders so the bridge never dumps loose files in the shared project root. See fusion-cloud-hygiene.",+ "timeoutSec": 50 }, {- "example": {},- "input": {},- "longRunning": false,- "name": "fusion_get_app_state",- "output": {- "activeDocument": "str",- "activeWorkspace": "str",- "isElectronics": "bool"+ "example": {+ "query": "BME690 Molecule"+ },+ "input": {+ "fileName": "optional str",+ "query": "str OR projectId+urn",+ "saveDir": "optional str" },- "statusVerb": null,- "summary": "Current app state: running, active document, workspace, isElectronics. The poll/status verb for long-running calls.",- "timeoutSeconds": 20- },- {- "example": {},- "input": {}, "longRunning": false,- "name": "fusion_readiness",+ "name": "fusion_aps_download", "output": {- "bridgeVersion": "str",- "hostApp": "str",- "installed": "bool",- "ready": "bool",- "running": "bool"+ "bytes": "int",+ "path": "str" }, "statusVerb": null,- "summary": "FAST readiness check (does NOT launch Fusion): is the Fusion host app installed + running + the bridge ready to drive? ab DETECTS Fusion (never installs it) via bridge.json 'detect', and auto-installs Python if missing (ab >=1.9.47). Call this before driving Fusion; if installed-but-not-running, fusion_start. SELF-HEALS the seat/licensing dialog (background UIA + screenshot-verify) whenever it detects one. Pairs with ab's bridge_readiness.",- "timeoutSeconds": 15+ "summary": "Download a cloud file to the local machine (no Fusion needed).",+ "timeoutSec": 90 }, { "example": {- "brdPath": "C:/Users/me/adom-lib/MyBoard.brd",- "schPath": "C:/Users/me/adom-lib/MyBoard.sch"+ "query": "BME690 Molecule" }, "input": {- "brdPath": "optional str (defaults to sibling .brd)",- "schPath": "required str (Windows path to .sch)"+ "query": "str OR projectId+urn" }, "longRunning": false,- "name": "fusion_new_electronics_from_eagle",+ "name": "fusion_aps_file_info", "output": {- "imported": "list",- "success": "bool"+ "tip": "{...}",+ "versionCount": "int",+ "versions": "[...]" }, "statusVerb": null,- "summary": "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D.",- "timeoutSeconds": 200+ "summary": "Version history + who/when/size for a file (tip + recent versions).",+ "timeoutSec": 40 }, {- "example": {- "body": "Please complete the Autodesk 2FA prompt on winvm - everything else is done.",- "title": "Fusion needs you"- },- "input": {- "body": "required str",- "level": "optional str info|warning|error",- "title": "optional str"- },+ "example": {},+ "input": {}, "longRunning": false,- "name": "fusion_notify_owner",+ "name": "fusion_aps_forget_browser", "output": {- "success": "bool",- "targets": "list"+ "cleared": "bool",+ "success": "bool" }, "statusVerb": null,- "summary": "LAST-RESORT human escalation: toast the user's MAIN computer (fans out cross-ab to every peer on the relay, so it reaches them even when this bridge runs on an unattended VM). ONLY for true human walls - password/2FA entry, UAC elevation, a physical step. Exhaust programmatic options first (UIA background clicks, seat auto-resolve, warm-SSO sign-in). After sending, WAIT and poll fusion_readiness; do not re-toast within a few minutes.",- "timeoutSeconds": 20+ "summary": "Forget the remembered Autodesk sign-in browser (re-detect next time).",+ "timeoutSec": 10 }, {- "example": {},+ "example": {+ "path": "/project/v1/hubs"+ }, "input": {- "auto": "optional bool (do click+capture+reopen in one on-box call - recommended)",- "profile": "optional str (chrome:<email>/edge:<email> - the profile your Autodesk account is in; remembered)",- "waitSec": "optional int (server-side poll budget for the fresh URL, default 32)"+ "path": "required str" },- "longRunning": true,- "name": "fusion_signin",+ "longRunning": false,+ "name": "fusion_aps_get", "output": {- "done": "bool",- "narrate": "str",- "notifyDelivered": "bool",- "signinAuthUrl": "str",- "signinOpenedVia": "str (extension|extension_free)",- "signinProfile": "str",- "signinProfileReason": "str",- "signinWall": "str|null (twofactor_email_code|credentials|protocol_handoff)",- "signinWallHint": "str|null",- "signinWhere": "str",- "signinWrongBrowser": "str",- "stage": "str"+ "body": "obj",+ "status": "int" },- "statusVerb": "fusion_signin",- "summary": "SELF-DRIVING as of 1.9.32: launches Fusion if needed, clicks Sign In, opens the OAuth in the right profile, TYPES THE EMAIL and advances to the exact remaining wall (password/2FA/handoff), toasting the user at that step only - see signinWall + _hint in the response, and the fusion-autodesk-signin skill. Sign Fusion in through the CORRECT browser profile - the fix for Fusion's dumb OAuth that fires at the OS-DEFAULT browser and lands in the wrong Autodesk account. Fusion opens its sign-in in whatever browser is default (e.g. a personal Chrome), but a user's Autodesk seat often lives in a different profile (work). This verb reads the FULL authorize URL (client_id/PKCE/state/request_id) that Fusion wrote into that wrong browser's HISTORY and re-opens it in the RIGHT profile IN THE BACKGROUND; the URL completes back to the running Fusion via the idmgr/callback + autodesk:// handoff, so the browser no longer has to be the OS default. Pass {profile:'chrome:[email protected]'} to fix + remember your Autodesk profile; else it probes each profile for a live Autodesk session (or asks). Staged: if Fusion hasn't opened the browser yet it returns stage:'click_signin' so you click Fusion's Sign In first. BEST: pass {auto:true} to run the WHOLE chain in ONE server-side call - it clicks Fusion's Sign In, polls the browser history on-box for the fresh authorize URL, and re-opens it in the target profile, all without extra remote round-trips, so it fits inside Fusion's ~2-min OAuth request expiry even on a slow/flaky relay (the manual staged path is the fallback). Fusion's request EXPIRES in ~2 min, so a stale captured URL returns stage:'stale' - restart Fusion (fusion_stop/start) for a fresh request and retry auto PROMPTLY. See the 'fusion-multiprofile-signin' skill.",- "timeoutSeconds": 120+ "statusVerb": null,+ "summary": "Raw authenticated GET against a free APS path (dev/verify helper).",+ "timeoutSec": 40 }, { "example": {}, "input": {}, "longRunning": false,- "name": "fusion_mcp_status",+ "name": "fusion_aps_get_browser", "output": {- "enabled": "bool",- "serverInfo": "obj",- "toolCount": "int",- "tools": "[{name,description}]"+ "browser": "{name,exe,profileDir}|null",+ "success": "bool" },- "statusVerb": "fusion_mcp_status",- "summary": "Is Autodesk's Fusion MCP server up, and what does it expose? Autodesk + Anthropic ship a LOCAL MCP server at http://127.0.0.1:27182/mcp exposing Fusion's own text-to-CAD surface (read geometry, execute/update features, read Electronics design data). It binds LOOPBACK on the user's machine and was built for Claude Desktop running there, so a CLOUD AI cannot reach it at all - this bridge runs on that machine and proxies it for you. Returns enabled, serverInfo and the live tool list. OFF by default: if it reports enabled:false, call fusion_mcp_enable and the bridge turns it on for the user.",- "timeoutSeconds": 45+ "statusVerb": null,+ "summary": "Show the remembered Autodesk sign-in browser (None until set).",+ "timeoutSec": 10 }, {- "example": {},- "input": {},+ "example": {+ "query": "BME690 Molecule"+ },+ "input": {+ "query": "required str",+ "wait": "optional bool"+ }, "longRunning": true,- "name": "fusion_mcp_enable",+ "name": "fusion_aps_open", "output": {- "alreadyOn": "bool",- "enabled": "bool",- "serverInfo": "obj",- "steps": "list"+ "match": "{name}",+ "opening": "bool",+ "statusVerb": "str" },- "statusVerb": "fusion_mcp_status",- "summary": "Turn Autodesk's Fusion MCP server ON by driving Fusion's Preferences dialog for the user. Autodesk exposes NO API for this toggle, so the UI is the only route: this opens Preferences (Commands.Start PreferencesCommand), expands General, selects API, ticks 'Fusion MCP Server (runs locally on this device)', then clicks Apply and OK. Needs Fusion running; briefly uses the foreground and announces why with an on-screen caption. NEVER ask the user to do this by hand first - do it for them, then tell them it is handled. Note the setting is DISCARDED if Fusion is killed or restarted before Apply is clicked.",- "timeoutSeconds": 180+ "statusVerb": "fusion_get_app_state",+ "summary": "Find a cloud design by name and open it in Fusion (any folder depth). Fire-and-poll.",+ "timeoutSec": 60 }, {- "example": {},- "input": {},+ "example": {+ "limit": 25+ },+ "input": {+ "limit": "optional int (default 25)"+ }, "longRunning": false,- "name": "fusion_mcp_tools",+ "name": "fusion_aps_recent", "output": { "count": "int",- "tools": "list"+ "results": "[...]" },- "statusVerb": "fusion_mcp_status",- "summary": "Full tool list from Autodesk's Fusion MCP server INCLUDING input schemas, so you can construct a valid fusion_mcp_call. These are Autodesk's tools, not this bridge's.",- "timeoutSeconds": 45+ "statusVerb": null,+ "summary": "Most-recently-modified designs across the team hub, newest first.",+ "timeoutSec": 60 }, { "example": {- "arguments": {- "queryType": "summary"- },- "tool": "fusion_mcp_read"+ "limit": 10,+ "query": "Molecule" }, "input": {- "arguments": "optional obj (per that tool's inputSchema)",- "timeout": "optional int seconds, default 120",- "tool": "required str (from fusion_mcp_tools)"- },- "longRunning": true,- "name": "fusion_mcp_call",- "output": {- "result": "obj",- "tool": "str"- },- "statusVerb": "fusion_mcp_status",- "summary": "Call any tool on Autodesk's Fusion MCP server (fusion_mcp_read, fusion_mcp_update, fusion_mcp_execute, fusion_mcp_electronics_read). The bridge handles the MCP streamable-HTTP handshake for you (initialize -> MCP-Session-Id -> notifications/initialized) and re-establishes a dropped session automatically. MCP acts on the ACTIVE Fusion document, so open one first. PREFER a native fusion_* verb when one exists - they are tested here and return richer hints - and reach for MCP for Autodesk's text-to-CAD surface.",- "timeoutSeconds": 150- },- {- "example": {},- "input": {- "uri": "optional str (omit to list)"+ "hubId": "optional str",+ "limit": "optional int (default 50)",+ "projectId": "optional str",+ "query": "required str" }, "longRunning": false,- "name": "fusion_mcp_resources",+ "name": "fusion_aps_search", "output": { "count": "int",- "resources": "[str]",- "result": "obj"- },- "statusVerb": "fusion_mcp_status",- "summary": "List Autodesk MCP resources, or read one by uri. These are mostly the Electronics entity schemas (resource://mcp.electronics_schema_*) that describe what fusion_mcp_electronics_read can return.",- "timeoutSeconds": 60- },- {- "example": {- "section": "api"- },- "input": {- "section": "optional str (see knownSections in the response)"- },- "longRunning": true,- "name": "fusion_prefs_open",- "output": {- "hwnd": "int",- "knownSections": "[str]",- "screenshot": "str",- "shotId": "str",- "steps": "list"- },- "statusVerb": "fusion_get_preferences",- "summary": "Open Fusion's Preferences dialog and optionally jump to a section, then hand back a SCREENSHOT + shotId you can click in. This is how you reach ANY Fusion preference: the Python API exposes only a thin slice (theme/orbit/units, debuggingPort, developer tools) and everything else, including the MCP server toggle, is UI-only. Sections: general, api, design, manufacture, electronics, render, drawing, material, graphics, network, preview features. Child sections under General are lazily rendered by Qt and never appear in the UIA tree, which is why this returns an image to click rather than control names. ALWAYS finish with fusion_prefs_close {save:true} - nothing is saved otherwise, and killing/restarting Fusion first discards the change.",- "timeoutSeconds": 120- },- {- "example": {- "save": true- },- "input": {- "save": "optional bool, default true"- },- "longRunning": false,- "name": "fusion_prefs_close",- "output": {- "closed": "bool",- "saved": "bool"- },- "statusVerb": "fusion_get_preferences",- "summary": "Apply+OK (save:true, the default) or Cancel Fusion's Preferences dialog. Settings changed via fusion_prefs_open are NOT persisted until this is called with save:true.",- "timeoutSeconds": 60- },- {- "example": {- "enabled": true- },- "input": {- "enabled": "optional bool (omit to read current value)"- },- "longRunning": true,- "name": "fusion_set_auto_update",- "output": {- "autoUpdateFusion": "bool",- "narrate": "str"+ "results": "[{name,id,projectName,lastModified}]" },- "statusVerb": "fusion_readiness",- "summary": "Turn AUTOMATIC Fusion updating on/off for this user. ON BY DEFAULT, and deliberately INVISIBLE: Fusion ships updates constantly and nags with an 'Update Now / Update Later' panel plus a '14 days until Fusion must update' countdown. Every fusion_readiness call checks for that offer and, if present, the bridge clicks it IN THE BACKGROUND (UIA, no foreground, no caption) so the download starts and the newer build comes up on the next launch. The user should never have to click it. Call with no args to read the current setting; {enabled:false} opts out and the choice is STICKY (~/.adom/fusion-bridge/prefs.json) - once a user opts out, NEVER click their update prompts again. Tell the user this is handled for them rather than asking them to update.",- "timeoutSeconds": 30+ "statusVerb": null,+ "summary": "Server-indexed search across the whole team hub (seconds, not a 30-min folder walk).",+ "timeoutSec": 60 }, { "example": {- "code": "123456"- },- "input": {- "code": "required str (the 6 digits; non-digits are stripped)",- "hwnd": "optional int (the 2FA window; auto-detected)"- },- "longRunning": true,- "name": "fusion_signin_2fa",- "output": {- "narrate": "str",- "steps": "list",- "submitted": "bool"+ "exe": "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe",+ "name": "Edge",+ "profileDir": "Default" },- "statusVerb": "fusion_readiness",- "summary": "Submit Autodesk's emailed 6-DIGIT verification code to finish a Fusion sign-in, typed IN THE BACKGROUND (UIA SetValue + Invoke - never steals the user's foreground or moves their cursor). Exists so you can finish a login END TO END for the user instead of parking them on a 2FA screen: if `adom-google` is available in your container, read the code out of their Gmail (`adom-google gmail search 'from:autodesk verification code' --limit 1`), pull the 6 digits, and call this. If adom-google is NOT installed, tell the user that installing it would let you automate logins like this, then let them type it. NEVER ask the user for a code you could read yourself. The bridge ALSO toasts the user's desktop whenever it hits a human wall, so you do not need to ask it to.",- "timeoutSeconds": 90- },- {- "example": {}, "input": {- "captions": "optional bool (default true: one desktop caption per stop)",- "foreground": "optional bool (default false: background envelope, never steals focus)",- "mode": "optional str ('tour' = the async showcase; poll fusion_demo_progress {demoId})",- "preset": "optional str ('electronics' = library import, symbol, footprint, 3D chip, ganged component view, schematic, 2D board, 3D board - the eight-stop showcase; omit for the full tour incl. Data Hub, APS, MCP, exports)",- "query": "optional str (design to demo; else picked from APS search)",- "remote": "optional bool (tour mode: show ab's floaty glass PLAY/PAUSE/NEXT/BACK/STOP panel on the desktop; its presses drive the same transport flags as fusion_demo_control, so the dashboard buttons and the remote can be mixed)",- "stage": "optional str ('skip_aps' to proceed without cloud search)"- },- "longRunning": true,- "name": "fusion_demo",- "output": {- "apsSampleSearch": "[{name,project}]",- "done": "bool",- "narrate": "str",- "screenshots": "[{label,path}]",- "stage": "str",- "steps": "[str]"+ "exe": "str (browser binary) OR how:'extension'",+ "how": "optional 'extension'|'launch'",+ "name": "optional str",+ "profileDir": "optional str" },- "statusVerb": "fusion_demo",- "summary": "FIRST-TIME-USER DEMO in one verb - the thing to run when someone new wants to SEE what this bridge does (HD's installer calls this). Takes them from nothing to a real board: launches Fusion, FINISHES the Autodesk sign-in - it OPENS the sign-in in the user's NATIVE browser IN THE BACKGROUND (never interrupting their work) and returns signinWhere/signinProfile/signinSession so you can tell them exactly where it is waiting, then offer three paths: they finish it, you foreground it, or (with their OK) you drive it. Native browser via ABE/nbrowser_* because that profile is already signed into Autodesk - never pup, which is anonymous, sets up APS cloud search (or explains it if declined) and runs a LIVE sample search so they see 2-second results, then opens an electronics PROJECT and walks SCHEMATIC -> 2D BOARD -> 3D BOARD, screenshotting each view to show them. STAGED + RESUMABLE: every call advances as far as it safely can and returns stage/done/narrate/screenshots plus a _hint with the exact next action; keep calling it until done:true. NEVER leave a first-time user parked on the sign-in screen - that is the failure this verb exists to prevent. See the 'fusion-demo' skill. TOUR MODE (issue #29): pass {mode:'tour'} for the ASYNC six-content-type showcase - it stages the bundled sample library and walks symbol -> footprint -> device -> 3D package (plus schematic/2D/3D board when a query is given), returns {demoId, estimatedSec, steps} immediately, reports structured progress via fusion_demo_progress for a real progress bar, runs in the BACKGROUND by default (foreground:true only when the user wants to watch), and its final progress poll carries the screenshot array.",- "timeoutSeconds": 240- },- {- "example": {},- "input": {}, "longRunning": false,- "name": "fusion_aps_status",+ "name": "fusion_aps_set_browser", "output": {- "configured": "bool",- "neverCharge": "bool",- "signedIn": "bool"+ "browser": "{name,exe,profileDir}",+ "success": "bool" }, "statusVerb": null,- "summary": "APS state: configured, signed in, token live, never-charge meter.",- "timeoutSeconds": 20+ "summary": "Remember the browser+profile that authed to Autodesk so every later sign-in reuses it (power users have many browsers/profiles; the OS default is usually the wrong one). Call after a driven sign-in succeeds.",+ "timeoutSec": 10 }, { "example": {@@ -436,7 +275,7 @@ }, "statusVerb": null, "summary": "Set the org's APS (PKCE) client id used for cloud search.",- "timeoutSeconds": 20+ "timeoutSec": 20 }, { "example": {},@@ -456,789 +295,770 @@ }, "statusVerb": "fusion_aps_status", "summary": "Open the Autodesk sign-in in the REMEMBERED browser+profile (not the OS default); a background listener captures the token. Pass {exe,profileDir,name} to use+remember a specific browser, or {allowDefaultBrowser:true} to fall back to the OS default. If nothing is remembered, returns needsExtensionOpen so the caller opens authUrl in the user's real browser via nbrowser_open_window.",- "timeoutSeconds": 30- },- {- "example": {- "exe": "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe",- "name": "Edge",- "profileDir": "Default"- },- "input": {- "exe": "str (browser binary) OR how:'extension'",- "how": "optional 'extension'|'launch'",- "name": "optional str",- "profileDir": "optional str"- },- "longRunning": false,- "name": "fusion_aps_set_browser",- "output": {- "browser": "{name,exe,profileDir}",- "success": "bool"- },- "statusVerb": null,- "summary": "Remember the browser+profile that authed to Autodesk so every later sign-in reuses it (power users have many browsers/profiles; the OS default is usually the wrong one). Call after a driven sign-in succeeds.",- "timeoutSeconds": 10+ "timeoutSec": 30 }, { "example": {}, "input": {}, "longRunning": false,- "name": "fusion_aps_get_browser",+ "name": "fusion_aps_status", "output": {- "browser": "{name,exe,profileDir}|null",- "success": "bool"+ "configured": "bool",+ "neverCharge": "bool",+ "signedIn": "bool" }, "statusVerb": null,- "summary": "Show the remembered Autodesk sign-in browser (None until set).",- "timeoutSeconds": 10+ "summary": "APS state: configured, signed in, token live, never-charge meter.",+ "timeoutSec": 20 }, {- "example": {},- "input": {},+ "example": {+ "folderId": "urn:...",+ "localPath": "C:/x.f3d",+ "projectId": "a.YnVz..."+ },+ "input": {+ "fileName": "optional str",+ "folderId": "required str (NON-ROOT subfolder)",+ "localPath": "required str",+ "projectId": "required str"+ }, "longRunning": false,- "name": "fusion_aps_forget_browser",+ "name": "fusion_aps_upload", "output": {- "cleared": "bool",- "success": "bool"+ "bytes": "int",+ "itemUrn": "str" }, "statusVerb": null,- "summary": "Forget the remembered Autodesk sign-in browser (re-detect next time).",- "timeoutSeconds": 10+ "summary": "Upload a local file as a NEW cloud file (free Data Management; needs data:write scope). \u26d4 FOLDER HYGIENE: NEVER pass a project ROOT folderId - the bridge REFUSES it (refused_root_upload). Loose files in a shared root clutter the team's cloud. Create/choose a real subfolder first (fusion_aps_create_folder under an 'Adom AI Workspace' folder), or just use fusion_make_3d_package / fusion_build_library_3d which auto-target a non-root workspace. To RELOCATE an existing file, MOVE it (preserves its urn) - do NOT delete + re-upload (mints a new urn, breaks 3D bindings). See the fusion-cloud-hygiene skill.",+ "timeoutSec": 90 }, { "example": {- "limit": 10,- "query": "Molecule"+ "query": "BME690 Molecule" }, "input": {- "hubId": "optional str",- "limit": "optional int (default 50)",- "projectId": "optional str",- "query": "required str"+ "query": "str OR projectId+urn" }, "longRunning": false,- "name": "fusion_aps_search",+ "name": "fusion_aps_versions", "output": {- "count": "int",- "results": "[{name,id,projectName,lastModified}]"+ "versionCount": "int",+ "versions": "[...]" }, "statusVerb": null,- "summary": "Server-indexed search across the whole team hub (seconds, not a 30-min folder walk).",- "timeoutSeconds": 60+ "summary": "Full (uncapped) version history of a file.",+ "timeoutSec": 40 }, { "example": {- "query": "BME690 Molecule"+ "includePhysicalProperties": true,+ "treatAsUnit": [+ "with fasteners"+ ] }, "input": {- "query": "required str",- "wait": "optional bool"+ "exclude": "optional [str] top-level name prefixes to skip",+ "includePhysicalProperties": "optional bool (adds volume_cm3/mass_kg per line)",+ "maxSeconds": "optional float (walk budget, default 20; the verb REFUSES fast with bom_walk_budget_exceeded + a fusion_export_bom redirect on electronics 3D PCBs instead of blocking Fusion for minutes - issue #659)",+ "outputPath": "optional str (Windows CSV path)",+ "treatAsUnit": "optional [str] name substrings counted as one unit (default ['with fasteners'])" },- "longRunning": true,- "name": "fusion_aps_open",+ "longRunning": false,+ "name": "fusion_assembly_bom", "output": {- "match": "{name}",- "opening": "bool",- "statusVerb": "str"+ "design": "str",+ "partCount": "int",+ "parts": "[{componentName, partNumber, description, material, quantity, bodies}]",+ "success": "bool",+ "totalInstances": "int" },- "statusVerb": "fusion_get_app_state",- "summary": "Find a cloud design by name and open it in Fusion (any folder depth). Fire-and-poll.",- "timeoutSeconds": 60+ "statusVerb": null,+ "summary": "Structured, kit-aware MECHANICAL bill of materials for the active Design assembly (the counterpart to the electronics-only fusion_export_bom). Recurses organizational subassemblies but counts a physical part or a purchased kit/unit ONCE, so hardware modeled inside a kit (a '... with fasteners' bracket, a bearing/pulley) is NOT double-counted the way a flat allOccurrences walk counts it. Matches Fusion's Manage -> BOM quantities. Optionally writes a CSV.",+ "timeoutSec": 120 }, { "example": {- "urn": "urn:adsk.wipprod:fs.file:vf.xxxx?version=1"+ "filePath": "C:/Users/<user>/adom-lib/ADS8588SIPM.fusion.lbr",+ "modelPath": "C:/Users/<user>/adom-lib/ADS8588SIPM.step",+ "packageName": "ADS8588SIPM" }, "input": {- "urn": "required str"+ "filePath": "required str (.lbr WINDOWS path C:/...)",+ "modelPath": "required str (.step WINDOWS path C:/...)",+ "packageName": "optional str" }, "longRunning": true,- "name": "fusion_open_by_urn",+ "name": "fusion_attach_3d_package", "output": {- "output": "str",+ "_hint": "the Save-dialog click + verify steps",+ "data": "{steps}",+ "savePending": "bool", "success": "bool" }, "statusVerb": "fusion_get_app_state",- "summary": "Open a cloud ELECTRONICS DESIGN by URN. PCB/ELECTRONICS NAV RULE: a board is NOT a standalone file - the schematic, 2D board and 3D board are VIEWS inside ONE electronics DESIGN. Open the DESIGN document first (fusion_open_cloud_file or fusion_open_by_urn), THEN switch to its schematic and its 2D board (fusion_show_2d_board) and 3D board (fusion_show_3d_board). A Select-Electronics-Design-File picker IS that design list of its schematic + board. An EMPTY 2D board usually means you opened a derivative or the wrong file, or it is out of sync - reopen the parent electronics DESIGN and switch to its board view; do NOT open a .brd or .sch as a standalone file.",- "timeoutSeconds": 240+ "summary": "LOW-LEVEL primitive: attaches ONE model to ONE package. Prefer fusion_create_library with parts[]. Attach a real 3D chip model to a library package, end to end: opens the .lbr (library active), runs Electron.Create3DPackage to enter the Package3DEnvironment showing the footprint, imports the STEP + auto-orients it flat on the footprint, then executes Package3DStop (FINISH). This BINDS the 3D onto the deviceset (Content Manager shows the 3D nested under the package; the Packages 'Package' column flips Placeholder->part-name). ONE desktop-side step remains: Fusion pops a modal Save dialog (an OWNED popup) - the response _hint gives the exact desktop_ui_click to confirm it. AFTER: re-grab desktop_screenshot_window and check ownedPopupCount for errors; the 3D preview LAGS a beat before showing the real chip. Both paths (filePath/modelPath) are WINDOWS paths - stage from the container first.",+ "timeoutSec": 240 }, {- "example": {},- "input": {- "folderId": "optional str",- "hubId": "optional str",- "projectId": "optional str"+ "example": {+ "libraryPath": "C:/Temp/PART.lbr",+ "stepPath": "C:/Temp/PART.step" },- "longRunning": false,- "name": "fusion_aps_browse",+ "input": {+ "deviceset": "optional str (defaults to the library's first)",+ "libraryPath": "required str (WINDOWS .lbr path)",+ "stepPath": "required str (WINDOWS .step path)"+ },+ "longRunning": true,+ "name": "fusion_bind_library_3d", "output": {- "items": "[{name,type,id}]"+ "bound": "bool",+ "reopened": "bool",+ "success": "bool",+ "wip_urn": "str" }, "statusVerb": null,- "summary": "Browse hubs/projects/folders without launching Fusion.",- "timeoutSeconds": 60+ "summary": "LOW-LEVEL primitive: binds ONE STEP onto one deviceset. Prefer fusion_create_library with parts[] (which handles one part or many through a single path); reach for this only when you are driving a single bind deliberately. The CORRECT library in ONE call (the Library button's real bar: the 3D chip renders in the Content Manager preview): mints the part's 3D as a cloud f3d (make_3d_package), injects the wip_urn binding into the .lbr ON DISK (packages3d + package3dinstances), and reopens the library. Needs a signed-in Fusion session - the one cloud-dependent step in the wiki-to-EE circle. Autodesk's thumbnail preview lags ~2-3 min behind a fresh mint. NOTE: distinct from the older fusion_build_library_3d (multi-part batch pipeline).",+ "timeoutSec": 480 }, {- "example": {- "limit": 25- },- "input": {- "limit": "optional int (default 25)"- },+ "example": {},+ "input": {}, "longRunning": false,- "name": "fusion_aps_recent",+ "name": "fusion_board_info", "output": {- "count": "int",- "results": "[...]"+ "rawXml": "str" }, "statusVerb": null,- "summary": "Most-recently-modified designs across the team hub, newest first.",- "timeoutSeconds": 60+ "summary": "Raw board geometry XML (copper/arcs/lines) for the open board.",+ "timeoutSec": 60 }, {- "example": {- "query": "BME690 Molecule"- },- "input": {- "query": "str OR projectId+urn"- },- "longRunning": false,- "name": "fusion_aps_file_info",+ "example": {},+ "input": {},+ "longRunning": true,+ "name": "fusion_board_stackup", "output": {- "tip": "{...}",- "versionCount": "int",- "versions": "[...]"+ "copperCount": "int",+ "copperLayers": "list",+ "copperThickness_mm": "list",+ "dielectricBonds": "list (prepreg/core order)",+ "fr4": "{x_mm,y_mm,dielectric_mm}",+ "layerSetup": "str" }, "statusVerb": null,- "summary": "Version history + who/when/size for a file (tip + recent versions).",- "timeoutSeconds": 40+ "summary": "Read a board's PHYSICAL fabrication stackup (see the pcb-stackup skill): copper layer count + copper/dielectric thicknesses from the EAGLE design rules (layerSetup/mtCopper/mtIsolate) plus the measured FR4 extent. A PCB is Cu/prepreg/Cu/core/.../Cu - the FR4 is NOT one slab. Use to build the stackup table + real-thickness exploded 3D view, and put a stackup table on every exported board's wiki page.",+ "timeoutSec": 200 }, { "example": {- "query": "BME690 Molecule"+ "lbrPath": "C:/tmp/newlib/AdomBasicParts2.lbr",+ "parts": [+ {+ "lbrPath": "C:/tmp/newlib/R_4k7.lbr",+ "modelPath": "C:/tmp/newlib/R_4k7.step",+ "package": "R_4k7"+ }+ ] }, "input": {- "query": "str OR projectId+urn"+ "capture": "optional bool (default true)",+ "folderId": "optional str",+ "lbrPath": "required str (combined .lbr WINDOWS path to bind + open)",+ "openWhenDone": "optional bool (default true)",+ "outLbrPath": "optional str (default: overwrite lbrPath)",+ "parts": "required [{package, lbrPath (per-part footprint .lbr), modelPath (.step)}]",+ "projectId": "optional str" },- "longRunning": false,- "name": "fusion_aps_versions",+ "longRunning": true,+ "name": "fusion_build_library_3d", "output": {- "versionCount": "int",- "versions": "[...]"+ "boundLbr": "str",+ "opened": "bool",+ "parts": "[{package,success,wip_urn,dims_mm,error}]",+ "partsBound": "int",+ "partsTotal": "int",+ "screenshots": "[{package,stage,path}]",+ "success": "bool" },- "statusVerb": null,- "summary": "Full (uncapped) version history of a file.",- "timeoutSeconds": 40+ "statusVerb": "fusion_get_app_state",+ "summary": "Build a RENDERING multi-part 3D library in ONE call - the whole programmatic pipeline. For each part it makes the footprint+chip f3d package (no GUI dialogs, optional before/after screenshots) and collects the wip_urn, then injects ALL bindings into the combined .lbr in one pass (no hand XML surgery) and opens the finished library once. This is the verb for a basic-parts sampler / any many-part library; it replaces the per-part make_3d_package loop + manual binding. projectId/folderId default to the MAIN-project upload folder. Each part is independent, so re-run just the failed parts if Fusion resets mid-run.",+ "timeoutSec": 1800 }, { "example": {- "query": "BME690 Molecule"+ "runId": "a3f1c2d4e5b6" }, "input": {- "fileName": "optional str",- "query": "str OR projectId+urn",- "saveDir": "optional str"+ "runId": "optional str (omit for the latest run in flight)" },- "longRunning": false,- "name": "fusion_aps_download",+ "longRunning": true,+ "name": "fusion_cancel_run", "output": {- "bytes": "int",- "path": "str"+ "cancelRequested": "bool",+ "runId": "str",+ "stage": "str at the time of the request",+ "statusVerb": "fusion_prove_result" },- "statusVerb": null,- "summary": "Download a cloud file to the local machine (no Fusion needed).",- "timeoutSeconds": 90+ "statusVerb": "fusion_prove_result",+ "summary": "STOP a library import that is in flight (fusion_install_library_bundle / fusion_create_library) - the user's escape hatch from a long run, wired to a Cancel button in web control. Cooperative, not a kill: the run finishes the part it is on (a STEP import cannot be interrupted mid-command without leaving a half-made design, up to ~60s), keeps every 3D package already minted and binds them into the library, closes its scratch documents, and leaves the library open in Fusion (not saved to the cloud). The run then reports stage:cancelled, verdict:cancelled, errorCode cancelled_by_user, cancelledAt (which stage) and how many parts were done, on the same fusion_prove_result poll the caller already makes; cancelRequested:true shows on the very next poll before the boundary is reached. Omit runId to cancel the latest run still in flight. Idempotent; a run that already ended answers alreadyFinished.",+ "timeoutSec": 10 }, { "example": {- "folderId": "urn:...",- "localPath": "C:/x.f3d",- "projectId": "a.YnVz..."+ "packages": [+ "ESP32-S3FN8",+ "ATSAMD51J20A-AUT"+ ] }, "input": {- "fileName": "optional str",- "folderId": "required str (NON-ROOT subfolder)",- "localPath": "required str",- "projectId": "required str"+ "packages": "required [<deviceset name>] (or 'package': single name)",+ "settle": "optional float secs after each EDIT before capture (default 1.5; raise if a shot shows the PREVIOUS part)",+ "views": "optional subset of ['symbol','footprint','3d','component'] (default ALL FOUR, in that assembly order - 3d is the dedicated full-size 3D-package render, component is the ganged view last)" },- "longRunning": false,- "name": "fusion_aps_upload",+ "longRunning": true,+ "name": "fusion_capture_library_views", "output": {- "bytes": "int",- "itemUrn": "str"+ "captured": "[{package,view,path,ok}]",+ "success": "bool" },- "statusVerb": null,- "summary": "Upload a local file as a NEW cloud file (free Data Management; needs data:write scope). \u26d4 FOLDER HYGIENE: NEVER pass a project ROOT folderId - the bridge REFUSES it (refused_root_upload). Loose files in a shared root clutter the team's cloud. Create/choose a real subfolder first (fusion_aps_create_folder under an 'Adom AI Workspace' folder), or just use fusion_make_3d_package / fusion_build_library_3d which auto-target a non-root workspace. To RELOCATE an existing file, MOVE it (preserves its urn) - do NOT delete + re-upload (mints a new urn, breaks 3D bindings). See the fusion-cloud-hygiene skill.",- "timeoutSeconds": 90+ "statusVerb": "fusion_get_app_state",+ "summary": "Capture the LIBRARY-EDITOR views EEs trust: the schematic SYMBOL (full pinout), the FOOTPRINT (pads + layer stack), and the COMPONENT/device view (Content Manager: symbol + the package table with the footprint<->package Mapped check + pin/pad counts). The 3D before/after shots don't show these. Requires the .lbr OPEN in the Electronics Library workspace (fusion_open_lbr first). Runs EDIT <pkg>.sym/.pac/.dev + WINDOW FIT + a background hwnd screenshot per view; returns the PNG paths on the box (C:/tmp/conduit-screenshots - pull with desktop_pull_file). EDIT uses the DEVICESET name (e.g. ESP32-S3FN8).",+ "timeoutSec": 300 }, { "example": {- "name": "Adom AI Workspace",- "parentFolderId": "urn:...",- "projectId": "a.YnVz..."+ "fileIds": [+ "urn:adsk.wipprod:dm.lineage:abc123"+ ],+ "projectName": "Adom" }, "input": {- "name": "required str",- "parentFolderId": "required str",- "projectId": "required str"+ "fileIds": "required [<lineage urn>]",+ "folderPath": "optional str (default root - where the files live)",+ "projectName": "optional str (default active project)" },- "longRunning": false,- "name": "fusion_aps_create_folder",+ "longRunning": true,+ "name": "fusion_cleanup_cloud_files", "output": {- "folderId": "str"+ "deletedCount": "int",+ "failed": "[{fileId,error}]",+ "failedCount": "int",+ "success": "bool" },- "statusVerb": null,- "summary": "Create a subfolder in a project (free; needs data:create scope). Use this to make an 'Adom AI Workspace' folder + per-task subfolders so the bridge never dumps loose files in the shared project root. See fusion-cloud-hygiene.",- "timeoutSeconds": 50+ "statusVerb": "fusion_get_app_state",+ "summary": "PRECISELY delete a list of cloud files by lineage urn - SAFE cleanup of AI-created clutter. Deletes ONLY the exact fileIds given (never name-guessing), so it can't touch a teammate's file in a shared folder. Loops server-side. Get the ids from fusion_aps_browse (item .id). Use this to remove f3d files the bridge created. A big list takes minutes (relay may time out at ~60s while deletes continue server-side - re-browse to confirm).",+ "timeoutSec": 1800 }, { "example": {- "path": "/project/v1/hubs"+ "closeUntitled": true }, "input": {- "path": "required str"- },- "longRunning": false,- "name": "fusion_aps_get",- "output": {- "body": "obj",- "status": "int"+ "closeUntitled": "optional bool default false - also close UNMODIFIED Untitled scratch docs",+ "force": "optional bool default false - with closeUntitled, also discard MODIFIED Untitled docs",+ "save": "optional bool default false" },- "statusVerb": null,- "summary": "Raw authenticated GET against a free APS path (dev/verify helper).",- "timeoutSeconds": 40- },- {- "example": {},- "input": {}, "longRunning": false,- "name": "fusion_walk_cloud_tree",+ "name": "fusion_close_all_documents", "output": {- "disabled": "bool"+ "closed": "[str]",+ "success": "bool",+ "wouldPrompt": "[str]|null" }, "statusVerb": null,- "summary": "\u26d4 DISABLED (returns an error): the in-app add-in walk took 30+ min and CRASHED Fusion (WinError 10054). It is hard-blocked in the bridge. Use fusion_aps_search / fusion_aps_open. See the 'fusion-aps-search' skill.",- "timeoutSeconds": 15+ "summary": "Close ALL open documents without save dialogs. DESTRUCTIVE to unsaved user work - confirm with the human before calling on a box you do not own. By default it SKIPS scratch 'Untitled' docs; pass closeUntitled:true to close the UNMODIFIED ones too (page issue #59: the library/bind flow left ~5 per import, 16 after three, and no caller could clear them), and force:true to also discard MODIFIED Untitled docs, which may hold real unsaved work.",+ "timeoutSec": 60 }, { "example": {- "query": "Molecule"+ "save": false }, "input": {- "query": "required str"+ "name": "optional str (default: active document)",+ "save": "optional bool default false" }, "longRunning": false,- "name": "fusion_search_cloud_files",+ "name": "fusion_close_document", "output": {- "disabled": "bool"+ "closed": "str",+ "success": "bool" }, "statusVerb": null,- "summary": "\u26d4 DISABLED (returns an error): the in-app add-in search took 30+ min and CRASHED Fusion (WinError 10054). It is hard-blocked in the bridge. Use fusion_aps_search {query} \u2014 server-indexed, seconds, no Fusion. See the 'fusion-aps-search' skill.",- "timeoutSeconds": 15+ "summary": "Close ONE document WITHOUT the modal save dialog (save:false discards silently - the automation-safe close). Omit name to close the active document.",+ "timeoutSec": 60 }, {- "example": {},+ "example": {+ "hwnd": 123456+ }, "input": {- "capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30)."+ "hwnd": "required int (the dialog window)" },- "longRunning": true,- "name": "fusion_show_schematic",+ "longRunning": false,+ "name": "fusion_close_window", "output": {- "activeWorkspace": "str",- "productType": "str", "success": "bool" }, "statusVerb": null,- "summary": "Switch the OPEN electronics design to its SCHEMATIC view (the symmetric partner of fusion_show_2d_board / fusion_show_3d_board, via Fusion's SwitchSchDocCmd). A VIEW of the project, not a standalone file. Drive it for demos with fusion_electron_zoom / _pan / _select.",- "timeoutSeconds": 70+ "summary": "Close a DIALOG window by hwnd (WM_CLOSE, like clicking X) - for dialogs without Cancel/Escape handling (e.g. Recovered Documents). NOT a document closer; refuses without hwnd. Get the hwnd from a screenshot's dialogs array or desktop_list_windows. (Doc drift caught by the verb test suite 2026-08-14: this was described as 'close the active document' while the handler required hwnd.)",+ "timeoutSec": 30 }, {- "example": {},+ "example": {+ "libraryPath": "C:/Temp/PART.lbr",+ "stepPath": "C:/Temp/PART.step"+ }, "input": {- "capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30)."+ "kicadFootprintPath": "optional str",+ "kicadSymbolPath": "optional str",+ "libraryPath": "optional str (shape A, or output path for B)",+ "name": "optional str",+ "parts": "optional [{package, lbrPath, modelPath}] - THE canonical shape, any count",+ "pinMap": "optional {pinName: padName}",+ "stepPath": "optional str (required for proven)" }, "longRunning": true,- "name": "fusion_show_2d_board",+ "name": "fusion_create_library", "output": {- "activeWorkspace": "str"+ "started": "bool",+ "statusVerb": "str" },- "statusVerb": null,- "summary": "Switch the OPEN electronics design to its 2D PCB Editor (board layout) view. This is a VIEW of the project, not a file to open standalone - if the board is empty, you opened a child/derivative instead of the EcadDesignProductType PROJECT; reopen the project. Drive the view with fusion_electron_run WINDOW commands.",- "timeoutSeconds": 70+ "statusVerb": "fusion_prove_result",+ "summary": "ONE-CALL library creation for web-control (one call = one consent). EVERY LIBRARY IS MULTI-PART - a single-part library is simply parts:[one entry], and there is no separate single-part verb to choose. Pass parts:[{package, lbrPath, modelPath}] for any number of parts; the older {libraryPath, stepPath} shape still works and is normalised into a one-element parts list, so both run the identical code path and the ETA scales with the part count either way. TWO entry shapes: A) {libraryPath, stepPath} - a ready .lbr plus the 3D; B) {kicadSymbolPath, kicadFootprintPath, stepPath, pinMap?} - KiCad sources; needs adom-lbr, which is Linux-container-only by platform rule, so on Windows/Mac boxes shape B refuses IMMEDIATELY (adom_lbr_unavailable, before any staging or Fusion launch - fusion_readiness reports kicadIngest up front). Convert at AUTHORING time in a container and use shape A. pinMap = explicit {pinName: padName} for when pin numbers do not match pad names (BGA-style). Chains convert -> bind 3D -> prove (four evidence frames + verdict) in the BACKGROUND and returns immediately; poll fusion_prove_result (stage narrates; top-level `verdict` proven/not_proven is the score - `stage:done` only means the chain finished). EVERY PART IS PROVED, not just the first: a 10-component library streams 40 evidence frames, FOUR per component (symbol, footprint, 3d, ganged) in assembly order, each tagged with `part` so a front end can group them as they arrive, and `provedParts[]` reports per-part pass/fail. The library is proven only if EVERY part is - one good part does not vouch for nine unchecked ones. On the FINAL fetch pass {inlineFrames:true} to get each evidence frame as a base64 thumbnailDataUrl a browser can render (plain polls return box paths only). The chain closes its library document on success AND failure - batches need no between-part sweeping. stepPath is required for a PROVEN library - no real 3D body without it.",+ "timeoutSec": 30 }, { "example": {}, "input": {- "capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30)."+ "captions": "optional bool (default true: one desktop caption per stop)",+ "foreground": "optional bool (default false: background envelope, never steals focus)",+ "mode": "optional str ('tour' = the async showcase; poll fusion_demo_progress {demoId})",+ "preset": "optional str ('electronics' = library import, symbol, footprint, 3D chip, ganged component view, schematic, 2D board, 3D board - the eight-stop showcase; omit for the full tour incl. Data Hub, APS, MCP, exports)",+ "query": "optional str (design to demo; else picked from APS search)",+ "remote": "optional bool (tour mode: show ab's floaty glass PLAY/PAUSE/NEXT/BACK/STOP panel on the desktop; its presses drive the same transport flags as fusion_demo_control, so the dashboard buttons and the remote can be mixed)",+ "stage": "optional str ('skip_aps' to proceed without cloud search)" }, "longRunning": true,- "name": "fusion_show_3d_board",+ "name": "fusion_demo", "output": {- "activeWorkspace": "str",- "screenshots": "[...]"+ "apsSampleSearch": "[{name,project}]",+ "done": "bool",+ "narrate": "str",+ "screenshots": "[{label,path}]",+ "stage": "str",+ "steps": "[str]" },- "statusVerb": null,- "summary": "Switch the OPEN electronics design to its 3D PCB view (populated board). A VIEW of the project, not a standalone file. Orbit it for demos via fusion_run_modeling_script (app.activeViewport.camera + vp.refresh()).",- "timeoutSeconds": 70+ "statusVerb": "fusion_demo",+ "summary": "FIRST-TIME-USER DEMO in one verb - the thing to run when someone new wants to SEE what this bridge does (HD's installer calls this). Takes them from nothing to a real board: launches Fusion, FINISHES the Autodesk sign-in - it OPENS the sign-in in the user's NATIVE browser IN THE BACKGROUND (never interrupting their work) and returns signinWhere/signinProfile/signinSession so you can tell them exactly where it is waiting, then offer three paths: they finish it, you foreground it, or (with their OK) you drive it. Native browser via ABE/nbrowser_* because that profile is already signed into Autodesk - never pup, which is anonymous, sets up APS cloud search (or explains it if declined) and runs a LIVE sample search so they see 2-second results, then opens an electronics PROJECT and walks SCHEMATIC -> 2D BOARD -> 3D BOARD, screenshotting each view to show them. STAGED + RESUMABLE: every call advances as far as it safely can and returns stage/done/narrate/screenshots plus a _hint with the exact next action; keep calling it until done:true. NEVER leave a first-time user parked on the sign-in screen - that is the failure this verb exists to prevent. See the 'fusion-demo' skill. TOUR MODE (issue #29): pass {mode:'tour'} for the ASYNC six-content-type showcase - it stages the bundled sample library and walks symbol -> footprint -> device -> 3D package (plus schematic/2D/3D board when a query is given), returns {demoId, estimatedSec, steps} immediately, reports structured progress via fusion_demo_progress for a real progress bar, runs in the BACKGROUND by default (foreground:true only when the user wants to watch), and its final progress poll carries the screenshot array.",+ "timeoutSec": 240+ },+ {+ "example": {+ "demoId": "tour-abc123",+ "imagesAs": "base64",+ "sinceShot": 3+ },+ "input": {+ "demoId": "required str (from fusion_demo mode:tour; omitted = the single running tour)",+ "imagesAs": "optional str paths|base64|both",+ "sinceShot": "optional int - shots you already received; response windows to the new ones"+ },+ "longRunning": false,+ "name": "fusion_demo_progress",+ "output": {+ "done": "bool",+ "percent": "int",+ "shotCount": "int",+ "shots": "list",+ "step": "str",+ "stepLabel": "str",+ "stepShot": "dict|absent"+ },+ "statusVerb": null,+ "summary": "Poll a running fusion_demo tour: {percent, elapsedSec, estimatedSec, step, stepLabel, done, failed, shots, shotCount, stepShot}. Poll ~1-2.5s and render a REAL progress bar. LIVE EVIDENCE (issue #30): each frame MAY carry stepShot {step, title, image} - a ~640px base64 capture the BRIDGE took at that step's completion, one per step, riding the first frame after it completes (render these during the run; the page should not guess-snap). shots[] streams too: mid-run it carries paths for every completed step; pass imagesAs:base64 + sinceShot:<count you have> to receive only NEW shots fully encoded. done:true = the full gallery (base64 by default). Finished tours are DISK-SNAPSHOTTED, so the final frame stays pollable even across a bridge respawn.",+ "timeoutSec": 15 }, { "example": {}, "input": {}, "longRunning": false,- "name": "fusion_board_info",+ "name": "fusion_describe", "output": {- "rawXml": "str"+ "success": "bool",+ "verbs": "[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example}]" }, "statusVerb": null,- "summary": "Raw board geometry XML (copper/arcs/lines) for the open board.",- "timeoutSeconds": 60+ "summary": "Self-describe every verb the bridge exposes (this list) for ab's Verbs tab + runner.",+ "timeoutSec": 20 }, { "example": {},- "input": {- "sinceSec": "optional float (default 300)"+ "input": {},+ "longRunning": false,+ "name": "fusion_detect_layers",+ "output": {+ "copperLayers": "[int]",+ "layerCount": "int" },+ "statusVerb": null,+ "summary": "Detect 2-layer vs 4-layer board (ULP + CAM comparison).",+ "timeoutSec": 90+ },+ {+ "example": {},+ "input": {}, "longRunning": false,- "name": "fusion_alerts",+ "name": "fusion_dismiss_blocking_dialogs", "output": {- "alerts": "[{at,text}]"+ "dialogs": "[...]" }, "statusVerb": null,- "summary": "Fusion's OWN alert toasts, read from its log. Fusion draws error/warning toasts in CEF: they are invisible to Win32 enumeration AND to UIA, so a verb can look successful while Fusion is visibly complaining. This reads them, newest last.",- "timeoutSeconds": 30+ "summary": "Detect + classify (and optionally dismiss) blocking Fusion dialogs.",+ "timeoutSec": 30+ },+ {+ "example": {},+ "input": {},+ "longRunning": false,+ "name": "fusion_document_info",+ "output": {+ "isSaved": "bool",+ "name": "str",+ "productType": "str",+ "success": "bool"+ },+ "statusVerb": null,+ "summary": "Metadata for the ACTIVE document: name, product type, save state, version, parent project.",+ "timeoutSec": 30 }, { "example": {- "url": "https://library.io/"+ "x1": 0,+ "x2": 10,+ "y1": 0,+ "y2": 8 }, "input": {- "provider": "optional str google|password (default google)",- "sessionId": "optional str (default autodesk-web)",- "url": "required str (e.g. https://library.io/)",- "username": "optional str (for the Google account chooser)"+ "frameDelayMs": "optional int (default 35)",+ "steps": "optional int (default 16)",+ "x1": "float",+ "x2": "float",+ "y1": "float",+ "y2": "float" }, "longRunning": true,- "name": "fusion_web_signin",+ "name": "fusion_electron_pan", "output": {- "errorCode": "str?",- "steps": "[...]",- "success": "bool",- "url": "str"+ "box": "[...]",+ "output": "str",+ "success": "bool" },- "statusVerb": "fusion_get_app_state",- "summary": "Sign a pup browser window into an Autodesk WEB surface (library.io, the hub portal at <hub>.autodesk360.com, Autodesk Account) via Google SSO. Fusion's DESKTOP app being signed in does NOT sign these in - they are separate web sessions. NEVER guesses a credential: a missing password or a 2FA challenge comes back as a typed stop (human_credential_required / second_factor_required / google_sso_unavailable) naming the one-time command the USER runs. The password never passes through this bridge or the AI - it is stored once with ab's credential_set and filled by pup in the background.",- "timeoutSeconds": 240+ "statusVerb": null,+ "summary": "Smoothly PAN + zoom the view to frame a board-coordinate box (mm; get part positions from fusion_board_info). Animates from where the view last was.",+ "timeoutSec": 60 }, {- "example": {},+ "example": {+ "command": "set confirm yes;RUN 'C:/tmp/jlcpcb_smta_exporter.ulp' 'C:/tmp/jlc'"+ }, "input": {- "description": "optional str",- "name": "optional str (defaults to the library's own name)"+ "command": "required str" },- "longRunning": true,- "name": "fusion_managed_library_create",+ "longRunning": false,+ "name": "fusion_electron_run", "output": {- "alerts": "[...]",- "library": "str",+ "output": "str", "success": "bool" },- "statusVerb": "fusion_get_app_state",- "summary": "Create a NEW managed library on library.io FROM the library in front (which must already be saved in a Fusion Hub). Drives MANAGE > LIBRARY.IO > Push, then completes Fusion's Create Managed Library dialog. PITFALL: a managed library created under one Autodesk account is NOT visible to another account until its managed FOLDER is shared on library.io - proven live, so do not assume hub membership shares it.",- "timeoutSeconds": 240+ "statusVerb": null,+ "summary": "Run an EAGLE/Electron command in the open board (the generic extension point, e.g. RUN <ulp>).",+ "timeoutSec": 90 }, {- "example": {},+ "example": {+ "name": "U1"+ }, "input": {- "name": "optional str (exact library name from the picker; omit to list read-only)",- "onUpdatePrompt": "optional str cancel|merge|replace (default cancel; replace OVERWRITES the local library)"+ "name": "str (refdes, e.g. R1)",+ "properties": "optional bool (default true)",+ "x": "optional float",+ "y": "optional float" }, "longRunning": true,- "name": "fusion_managed_library_link",+ "name": "fusion_electron_select", "output": {- "alerts": "[...]",- "linkedTo": "str?",- "listedOnly": "bool?",- "managedLibraries": "[{name,type,version,managedFolder,description}]",- "success": "bool",- "updatePrompt": "obj?"+ "deviceInfo": "str",+ "output": "str",+ "success": "bool" },- "statusVerb": "fusion_get_app_state",- "summary": "Link the library IN FRONT to an existing managed library on library.io. With no name, opens Fusion's Select Managed Library picker, returns its rows as records (name/type/version/managedFolder/description) and CANCELS - a safe read-only listing of what this Autodesk account can reach. With name, selects that row and confirms. SAFETY: after linking, Fusion offers to force-update your library FROM the managed one; 'Replace Local Library' would destroy your parts, so this verb answers CANCEL by default - pass onUpdatePrompt to choose deliberately. A managed library owned by ANOTHER Autodesk account does NOT appear until its managed FOLDER is shared with this account on library.io; being in the same Fusion hub is not enough.",- "timeoutSeconds": 180+ "statusVerb": null,+ "summary": "SELECT a part (by reference designator, or x/y board mm) and surface its properties - the way clicking a part reveals the properties panel. Great for showing life in a recording.",+ "timeoutSec": 30 }, {- "example": {},- "input": {},+ "example": {+ "factor": 3,+ "steps": 20+ },+ "input": {+ "factor": "float (default 2.0)",+ "fit": "optional bool",+ "frameDelayMs": "optional int (default 35)",+ "steps": "optional int (default 16)"+ }, "longRunning": true,- "name": "fusion_managed_library_unlink",+ "name": "fusion_electron_zoom", "output": {- "alerts": "[...]",- "library": "str",+ "output": "str", "success": "bool" },- "statusVerb": "fusion_get_app_state",- "summary": "Unlink the library in front from its managed library on library.io. Fusion greys this out on a library that was never linked, so a silent no-op there is expected.",- "timeoutSeconds": 180+ "statusVerb": null,+ "summary": "Smoothly ZOOM the schematic/2D board for a recording (the whole motion is one call). factor>1 zooms in, <1 out; or fit:true. The loop runs in the add-in with a repaint per frame.",+ "timeoutSec": 60 }, {- "example": {},+ "example": {+ "command": "Electron.ImmediateRepaint /full"+ }, "input": {- "onConfirm": "optional str create|cancel (default create)"+ "command": "required str",+ "expectDocument": "optional str (refuses wrong_document if the active doc does not match)" },- "longRunning": true,- "name": "fusion_managed_library_push",+ "longRunning": false,+ "name": "fusion_execute_text_command", "output": {- "alerts": "[...]",- "confirmed": "obj",- "library": "str",+ "output": "str", "success": "bool" },- "statusVerb": "fusion_get_app_state",- "summary": "Push a NEW VERSION of the library in front up to its linked managed library on library.io. Clicking the menu item alone does NOT push: Fusion raises a CEF 'Create New Version' confirmation and THAT is what creates the version, so this verb completes it and fails with confirm_missing rather than reporting a false pass. On a library that is not linked yet, Fusion offers Create Managed Library instead and this reports not_linked_yet - use fusion_managed_library_create. NOTE: Library Manager keeps showing the IMPORTED version number after a push, so the row gains an update-available badge instead of incrementing.",- "timeoutSeconds": 240+ "statusVerb": null,+ "summary": "Run a raw Fusion TEXT COMMAND (app.executeTextCommand) - the escape hatch to Fusion internals (Electron.* electronics commands included). Electron.* commands need an ELECTRONICS document context (schematic/board view active). Guard risky calls with expectDocument.",+ "timeoutSec": 60 }, {- "example": {},+ "example": {+ "outputPath": "C:/out/part.3mf"+ }, "input": {- "onUpdatePrompt": "optional str merge|replace|cancel (default merge)"+ "outputPath": "required str" }, "longRunning": true,- "name": "fusion_managed_library_pull",+ "name": "fusion_export_3mf", "output": {- "alerts": "[...]",- "library": "str",- "success": "bool",- "upToDate": "bool",- "updatePrompt": "obj?"+ "format": "str" },- "statusVerb": "fusion_get_app_state",- "summary": "Pull the newest version FROM library.io into the linked library in front. A pull ends in ONE of two CEF dialogs and BOTH block the add-in until answered: the force-update prompt when content differs (answered MERGE by default, since pull is an explicit request for the managed content - pass onUpdatePrompt:'replace' to overwrite local parts entirely, or 'cancel' to back out), or 'Up to Date' when already current. If neither appears, nothing was pulled and this reports failure rather than a false pass - Fusion greys Pull out on a library that was never linked.",- "timeoutSeconds": 240+ "statusVerb": null,+ "summary": "Export to 3MF mesh.",+ "timeoutSec": 300 }, { "example": {- "tab": "private"+ "outputPath": "C:/tmp/bom.csv" }, "input": {- "tab": "optional str hub|public|private (default private)"+ "grouped": "optional bool (default true)",+ "outputPath": "optional str" },- "longRunning": true,- "name": "fusion_library_manager",+ "longRunning": false,+ "name": "fusion_export_bom", "output": {- "hwnd": "int",- "libraries": "[str] (usually empty - see pitfall)",- "success": "bool",- "tab": "str",- "tableReadable": "bool"+ "componentCount": "int",+ "grouped": "bool" },- "statusVerb": "fusion_get_app_state",- "summary": "Open Fusion's Library Manager and switch to one of its three tabs. Hub Libraries = libraries saved in the Fusion hub (what our bind chain produces, and what Autodesk recommends for sharing). Private Libraries = library.io managed libraries in folders THIS account belongs to. Public Libraries = library.io's public catalogue. PITFALL: the library TABLE is CEF-drawn and exposes NOTHING to UIA, so libraries[] is normally empty and that is not a failure - screenshot the returned hwnd to read the rows.",- "timeoutSeconds": 240+ "statusVerb": null,+ "summary": "Export a Bill of Materials CSV (grouped by value+package) from the open board.",+ "timeoutSec": 60 }, { "example": {- "filePath": "C:/Users/<user>/adom-lib/nRF54L15.lbr"+ "outputPath": "C:/tmp/cpl.csv" }, "input": {- "filePath": "required str (.lbr WINDOWS path C:/...; stage from container first)",- "symbolName": "optional str",- "verify": "optional bool"+ "outputPath": "optional str",+ "side": "optional str (all|top|bottom)" }, "longRunning": false,- "name": "fusion_open_lbr",+ "name": "fusion_export_cpl", "output": {- "postOpenScreenshot": "{savedTo}",- "success": "bool"+ "bottomCount": "int",+ "topCount": "int" }, "statusVerb": null,- "summary": "Import an EAGLE .lbr library into Fusion's Electronics LIBRARY editor (Content Manager / Electronics Library workspace), not a PCB design. PITFALL: filePath MUST be a WINDOWS-local path (C:/...). The chip-fetcher/adom-lbr pipeline runs in the cloud container, and there is NO container-to-Windows push verb, so STAGE the .lbr onto Windows first (serve it on a proxied port + Invoke-WebRequest via shell_execute), then pass the C:/ path. PITFALL: an adom-lbr .lbr is 2D ONLY - symbol + footprint + a PLACEHOLDER 3D package; the real 3D chip is NOT attached (that needs a Fusion cloud URN). To attach the 3D, see the 'fusion-libraries' skill. Pass verify:true to round-trip-confirm Fusion parsed the deviceset, then check fusion_get_app_state (activeWorkspace 'Electronics Library').",- "timeoutSeconds": 50+ "summary": "Export the Component Placement List (pick-and-place) CSV.",+ "timeoutSec": 60 }, { "example": {- "filePath": "C:/Users/<user>/adom-lib/ADS8588SIPM.fusion.lbr",- "modelPath": "C:/Users/<user>/adom-lib/ADS8588SIPM.step",- "packageName": "ADS8588SIPM"+ "outputPath": "C:/out/sketch.dwg" }, "input": {- "filePath": "required str (.lbr WINDOWS path C:/...)",- "modelPath": "required str (.step WINDOWS path C:/...)",- "packageName": "optional str"+ "outputPath": "required str" }, "longRunning": true,- "name": "fusion_attach_3d_package",+ "name": "fusion_export_dwg", "output": {- "_hint": "the Save-dialog click + verify steps",- "data": "{steps}",- "savePending": "bool",- "success": "bool"+ "format": "str" },- "statusVerb": "fusion_get_app_state",- "summary": "LOW-LEVEL primitive: attaches ONE model to ONE package. Prefer fusion_create_library with parts[]. Attach a real 3D chip model to a library package, end to end: opens the .lbr (library active), runs Electron.Create3DPackage to enter the Package3DEnvironment showing the footprint, imports the STEP + auto-orients it flat on the footprint, then executes Package3DStop (FINISH). This BINDS the 3D onto the deviceset (Content Manager shows the 3D nested under the package; the Packages 'Package' column flips Placeholder->part-name). ONE desktop-side step remains: Fusion pops a modal Save dialog (an OWNED popup) - the response _hint gives the exact desktop_ui_click to confirm it. AFTER: re-grab desktop_screenshot_window and check ownedPopupCount for errors; the 3D preview LAGS a beat before showing the real chip. Both paths (filePath/modelPath) are WINDOWS paths - stage from the container first.",- "timeoutSeconds": 240- },- {- "example": {},- "input": {},- "longRunning": true,- "name": "fusion_install_fusion",- "output": {- "alreadyInstalled": "bool?",- "installing": "bool",- "mode": "globalinstall|per-user",- "statusVerb": "str",- "success": "bool"- },- "statusVerb": "fusion_get_app_state",- "summary": "Install Fusion 360 FOR the user - downloads Autodesk's official Client Downloader and streams the free trial silently, with NO shell_execute and NO ab approval gate (runs inside the trusted bridge). Elevation-aware: --globalinstall when admin, PER-USER otherwise (a non-elevated globalinstall dies silently on UAC - learned live). Returns promptly; poll fusion_readiness (reports installing:true) until installed:true, then fusion_start. Declared as detect.installVerb so ab's bridge_readiness recommends it.",- "timeoutSeconds": 90+ "statusVerb": null,+ "summary": "Export 2D DWG.",+ "timeoutSec": 120 }, { "example": {- "etch": "103",- "outputStep": "C:/tmp/ADOM-R0603-103.step",- "params": {- "A": 0.45,- "D": 1.6,- "E": 0.8,- "L": 0.3,- "L1": 0.3- },- "type": "chip"+ "outputPath": "C:/out/sketch.dxf" }, "input": {- "etch": "optional str (marking text on the chip top: MPN / '103'; multi-line with newline, e.g. MPN+variant - laid along the LONGEST face axis)",- "etchDepthMm": "optional float (marking height/depth, default 0.03)",- "etchHeightMm": "optional float (text height; default auto-fit: top-5%-band face, 10% margin, width-aware)",- "etchStyle": "optional str raised|engraved (default raised = thin WHITE positive extrude, silkscreen-style contrast; engraved = sunken cut)",- "outputStep": "optional str (WINDOWS path; exports STEP incl. the marking)",- "params": "optional dict (generator dims in MM by default, e.g. chip: {D,E,A,L,L1}; soic: {A,A1,b,D,E,E1,e,L,DPins}; omitted keys use EPG defaults)",- "type": "required str (EPG family, e.g. chip|soic|qfn|qfp|bga|sot23|dfn2|melf|ecap|crystal|header_straight - see supportedTypes in the error for the full list)",- "unitsCm": "optional bool (params already EPG-native cm)"+ "outputPath": "required str" }, "longRunning": true,- "name": "fusion_generate_package",+ "name": "fusion_export_dxf", "output": {- "bodies": "[{name,vol}]",- "epgDir": "str",- "etched": "str|null",- "manifest": "obj (settings+why+bboxes; also written as <step>.manifest.json sidecar for marking refreshes)",- "manifestPath": "str|null",- "stepPath": "str|null",- "success": "bool"+ "format": "str" },- "statusVerb": "fusion_get_app_state",- "summary": "Generate an IPC-7351-compliant parametric 3D package via Fusion's BUILT-IN ElectronicsPackageGenerator (EPG) - ~50 package families (chip/soic/qfn/qfp/bga/sot23/dfn/melf/ecap/crystal/headers/...), each a few seconds, ZERO GUI. Optionally LASER-ETCH a marking (MPN / '103') into the body top as REAL cut geometry (survives STEP), and export STEP in the same call - the royalty-free, license-clean path to a whole component library (our own generated output, not vendor models). Proven live: 0603 + '103' etch + STEP in ~2s.",- "timeoutSeconds": 120+ "statusVerb": null,+ "summary": "Export 2D DXF.",+ "timeoutSec": 120 }, { "example": {- "captureLabel": "R_4k7",- "lbrPath": "C:/tmp/newlib/R_4k7.lbr",- "modelPath": "C:/tmp/newlib/R_4k7.step"+ "outputPath": "C:/out/design.f3d" }, "input": {- "captureLabel": "optional str (capture before/after)",- "fileName": "optional str",- "folderId": "optional str (default upload folder)",- "lbrPath": "required str (.lbr WINDOWS path; its FIRST package's footprint is loaded)",- "modelPath": "required str (.step WINDOWS path)",- "orient": "optional bool (default true)",- "projectId": "optional str (default MAIN-project)"+ "outputPath": "required str" }, "longRunning": true,- "name": "fusion_make_3d_package",+ "name": "fusion_export_f3d", "output": {- "after": "str|null",- "before": "str|null",- "dims_mm": "[x,y,z]",- "f3d": "str",- "success": "bool",- "wip_urn": "str"+ "format": "str" },- "statusVerb": "fusion_get_app_state",- "summary": "Create a RENDERING component 3D package (FOOTPRINT + chip, aligned) for ONE part, fully programmatically with ZERO GUI dialogs. Opens the .lbr, runs Electron.Create3DPackage to load the package's footprint into a generator doc, imports the STEP onto it, orients it flat, saveAs an .f3d (skips the FINISH Save dialog + the two unbeatable 'Fusion360' CEF modals), uploads the f3d via APS, and returns an fs.file:vf wip_urn to hand-write into the library's <packages3d>. projectId/folderId default to the MAIN-project upload folder. Pass captureLabel to also get a BEFORE (footprint) + AFTER (chip placed) screenshot. PITFALL: a RAW STEP upload binds but renders NOTHING ('Thumbnail download failed') - this verb makes an f3d, which renders. Prefer fusion_build_library_3d for many parts.",- "timeoutSeconds": 200+ "statusVerb": null,+ "summary": "Export the active design as a Fusion archive (.f3d).",+ "timeoutSec": 300 }, { "example": {- "lbrPath": "C:/tmp/newlib/AdomBasicParts2.lbr",- "parts": [- {- "lbrPath": "C:/tmp/newlib/R_4k7.lbr",- "modelPath": "C:/tmp/newlib/R_4k7.step",- "package": "R_4k7"- }- ]+ "outputPath": "C:/out/part.fbx" }, "input": {- "capture": "optional bool (default true)",- "folderId": "optional str",- "lbrPath": "required str (combined .lbr WINDOWS path to bind + open)",- "openWhenDone": "optional bool (default true)",- "outLbrPath": "optional str (default: overwrite lbrPath)",- "parts": "required [{package, lbrPath (per-part footprint .lbr), modelPath (.step)}]",- "projectId": "optional str"+ "outputPath": "required str" }, "longRunning": true,- "name": "fusion_build_library_3d",+ "name": "fusion_export_fbx", "output": {- "boundLbr": "str",- "opened": "bool",- "parts": "[{package,success,wip_urn,dims_mm,error}]",- "partsBound": "int",- "partsTotal": "int",- "screenshots": "[{package,stage,path}]",- "success": "bool"+ "format": "str" },- "statusVerb": "fusion_get_app_state",- "summary": "Build a RENDERING multi-part 3D library in ONE call - the whole programmatic pipeline. For each part it makes the footprint+chip f3d package (no GUI dialogs, optional before/after screenshots) and collects the wip_urn, then injects ALL bindings into the combined .lbr in one pass (no hand XML surgery) and opens the finished library once. This is the verb for a basic-parts sampler / any many-part library; it replaces the per-part make_3d_package loop + manual binding. projectId/folderId default to the MAIN-project upload folder. Each part is independent, so re-run just the failed parts if Fusion resets mid-run.",- "timeoutSeconds": 1800+ "statusVerb": null,+ "summary": "Export to FBX mesh.",+ "timeoutSec": 300 }, { "example": {- "packages": [- "ESP32-S3FN8",- "ATSAMD51J20A-AUT"- ]+ "outputDir": "C:/tmp/jlc" }, "input": {- "packages": "required [<deviceset name>] (or 'package': single name)",- "settle": "optional float secs after each EDIT before capture (default 1.5; raise if a shot shows the PREVIOUS part)",- "views": "optional subset of ['symbol','footprint','3d','component'] (default ALL FOUR, in that assembly order - 3d is the dedicated full-size 3D-package render, component is the ganged view last)"+ "outputDir": "optional str (default C:/tmp/adom-gerbers)" }, "longRunning": true,- "name": "fusion_capture_library_views",+ "name": "fusion_export_gerbers", "output": {- "captured": "[{package,view,path,ok}]",- "success": "bool"+ "fileCount": "int",+ "files": "[{name,size}]",+ "zipPath": "str" },- "statusVerb": "fusion_get_app_state",- "summary": "Capture the LIBRARY-EDITOR views EEs trust: the schematic SYMBOL (full pinout), the FOOTPRINT (pads + layer stack), and the COMPONENT/device view (Content Manager: symbol + the package table with the footprint<->package Mapped check + pin/pad counts). The 3D before/after shots don't show these. Requires the .lbr OPEN in the Electronics Library workspace (fusion_open_lbr first). Runs EDIT <pkg>.sym/.pac/.dev + WINDOW FIT + a background hwnd screenshot per view; returns the PNG paths on the box (C:/tmp/conduit-screenshots - pull with desktop_pull_file). EDIT uses the DEVICESET name (e.g. ESP32-S3FN8).",- "timeoutSeconds": 300+ "statusVerb": null,+ "summary": "Run the bundled JLCPCB CAM job (auto 2/4-layer) to produce the gerber + drill ZIP.",+ "timeoutSec": 180 }, { "example": {- "fileIds": [- "urn:adsk.wipprod:dm.lineage:abc123"- ],- "projectName": "Adom"+ "outputPath": "C:/out/board.iges" }, "input": {- "fileIds": "required [<lineage urn>]",- "folderPath": "optional str (default root - where the files live)",- "projectName": "optional str (default active project)"+ "outputPath": "required str" }, "longRunning": true,- "name": "fusion_cleanup_cloud_files",+ "name": "fusion_export_iges", "output": {- "deletedCount": "int",- "failed": "[{fileId,error}]",- "failedCount": "int",- "success": "bool"+ "format": "str" },- "statusVerb": "fusion_get_app_state",- "summary": "PRECISELY delete a list of cloud files by lineage urn - SAFE cleanup of AI-created clutter. Deletes ONLY the exact fileIds given (never name-guessing), so it can't touch a teammate's file in a shared folder. Loops server-side. Get the ids from fusion_aps_browse (item .id). Use this to remove f3d files the bridge created. A big list takes minutes (relay may time out at ~60s while deletes continue server-side - re-browse to confirm).",- "timeoutSeconds": 1800+ "statusVerb": null,+ "summary": "Export to IGES.",+ "timeoutSec": 300 }, { "example": {- "script": "import adsk.fusion\n# ...build geometry..."+ "outputPath": "C:/out/part.obj" }, "input": {- "script": "required str (Python using adsk; globals: adsk, app, ui, design)"+ "outputPath": "required str" }, "longRunning": true,- "name": "fusion_run_modeling_script",+ "name": "fusion_export_obj", "output": {- "documentName": "str",- "result": "obj"+ "format": "str" }, "statusVerb": null,- "summary": "Run an adsk.fusion modeling script in the live session (sketches/extrudes/params). Free programmatic CAD.",- "timeoutSeconds": 180+ "summary": "Export to OBJ mesh.",+ "timeoutSec": 300 }, { "example": {- "includePhysicalProperties": true,- "treatAsUnit": [- "with fasteners"- ]+ "outputPath": "C:/out/board.glb" }, "input": {- "exclude": "optional [str] top-level name prefixes to skip",- "includePhysicalProperties": "optional bool (adds volume_cm3/mass_kg per line)",- "maxSeconds": "optional float (walk budget, default 20; the verb REFUSES fast with bom_walk_budget_exceeded + a fusion_export_bom redirect on electronics 3D PCBs instead of blocking Fusion for minutes - issue #659)",- "outputPath": "optional str (Windows CSV path)",- "treatAsUnit": "optional [str] name substrings counted as one unit (default ['with fasteners'])"+ "async": "bool - RECOMMENDED true: fires the whole export+optimize pipeline in the background and returns {exportId} instantly (the sync form can outlive the relay window on a cold Fusion session); poll fusion_fetch_optimized_glb {exportId}",+ "outputPath": "required str (.glb)",+ "pin": "str medium|large default medium",+ "silkscreen": "bool default true",+ "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)" },- "longRunning": false,- "name": "fusion_assembly_bom",+ "longRunning": true,+ "name": "fusion_export_optimized_glb", "output": {- "design": "str",- "partCount": "int",- "parts": "[{componentName, partNumber, description, material, quantity, bodies}]",- "success": "bool",- "totalInstances": "int"+ "glbPath": "str",+ "jobId": "str",+ "meshesAfter": "int",+ "moleculeAnchored": "bool",+ "pending": "bool",+ "silkscreenApplied": "bool",+ "sizeBytes": "int" }, "statusVerb": null,- "summary": "Structured, kit-aware MECHANICAL bill of materials for the active Design assembly (the counterpart to the electronics-only fusion_export_bom). Recurses organizational subassemblies but counts a physical part or a purchased kit/unit ONCE, so hardware modeled inside a kit (a '... with fasteners' bracket, a bearing/pulley) is NOT double-counted the way a flat allOccurrences walk counts it. Matches Fusion's Manage -> BOM quantities. Optionally writes a CSV.",- "timeoutSeconds": 120+ "summary": "Fusion board -> wiki-grade optimized GLB. Exports STEP (+ optional silkscreen) and submits it to service-step2glb molecule mode (anchor to MP pins + silk bake + gold pins + join/weld/prune + Draco): ~465KB/~31 draw calls vs a raw ~16MB/~25000 that halts the viewer. Bounded-waits `wait`s (default 90); small boards return complete, big boards return {pending, jobId} - finish with fusion_fetch_optimized_glb. Use for a component page's component.parts.model_3d. STACKUP RULE (John 2026-07-15, see pcb-stackup skill): board bbox = the FR4 body, NEVER the whole assembly (components overhang edges + tower above); each overlay goes at ITS layer's z (copper at FR4-top, silk above mask); copper traces are REAL 35um 3D bodies colored like the FR4 - recolor them + translucent mask in the RAW GLB before optimizing to make routing visible; put the measured stackup table on the board's wiki page. PUBLISH EVERYTHING: a board's wiki page repo must carry EVERY exportable design file (.f3d/.step/.usdz/EAGLE .brd+.sch/Fusion .fbrd+.fsch/gerbers/BOM/CPL/GLBs/renders), not just the 3D - the page exists to share the design.",+ "timeoutSec": 200 }, { "example": {- "accuracy": "low"+ "outputPath": "C:/out/part.sat" }, "input": {- "accuracy": "optional low|medium|high|veryhigh (default low)",- "names": "optional [str] component names to measure (default all)"+ "outputPath": "required str" },- "longRunning": false,- "name": "fusion_physical_properties",+ "longRunning": true,+ "name": "fusion_export_sat", "output": {- "count": "int",- "properties": "{name: {volume_cm3, mass_kg, density, area_cm2, center_of_mass}}",- "success": "bool"+ "format": "str" }, "statusVerb": null,- "summary": "Per-component physical properties (volume cm3, mass kg, density, area, center of mass) for the active design. Reads them directly so BOM/costing tools no longer have to run getPhysicalProperties inside a modeling script. Pass names to scope to specific components.",- "timeoutSeconds": 120+ "summary": "Export to SAT (ACIS solid).",+ "timeoutSec": 300 }, { "example": {- "theme": "dark"+ "outputPath": "C:/out/part.skp" }, "input": {- "cameraPivot": "optional bool",- "gestureNav": "optional bool",- "invertScrollZoom": "optional bool",- "lengthUnit": "optional str",- "modelingOrientation": "optional str",- "orbitScheme": "optional str",- "theme": "optional str"+ "outputPath": "required str" }, "longRunning": true,- "name": "fusion_set_preference",- "output": {- "applied": "obj",- "current": "obj"- },- "statusVerb": null,- "summary": "Set Fusion appearance/navigation preferences in the LIVE session - applies immediately, NO restart. Keys: theme (light/darkblue/darkgray/classic/device/dark; some builds only ship LightGray/DarkBlue/Device, failures reported per-key), invertScrollZoom (bool - mouse scroll/zoom direction), orbitScheme (fusion360/alias/inventor/solidworks/tinkercad/powermill), modelingOrientation (yup/zup), gestureNav (bool), cameraPivot (bool), lengthUnit (mm/cm/m/in/ft - active design). Per-key outcome in `applied`, resulting state in `current`.",- "timeoutSeconds": 30- },- {- "example": {},- "input": {},- "longRunning": false,- "name": "fusion_get_preferences",+ "name": "fusion_export_skp", "output": {- "current": "obj"+ "format": "str" }, "statusVerb": null,- "summary": "Read current Fusion appearance/navigation preferences (theme, activeTheme, invertScrollZoom, orbitScheme, modelingOrientation, gestureNav, cameraPivot, lengthUnit) as friendly values - the same keys fusion_set_preference accepts.",- "timeoutSeconds": 20+ "summary": "Export to SketchUp (.skp).",+ "timeoutSec": 300 }, { "example": {@@ -1255,148 +1075,114 @@ }, "statusVerb": null, "summary": "Export the active design to STEP (needs the 3D view).",- "timeoutSeconds": 300+ "timeoutSec": 300 }, { "example": {- "filePath": "C:/Temp/part.step"+ "outputPath": "C:/out/part.stl" }, "input": {- "filePath": "required str (WINDOWS path C:/...; stage from container first)"+ "outputPath": "required str" }, "longRunning": true,- "name": "fusion_import_step",+ "name": "fusion_export_stl", "output": {- "documentName": "str",- "success": "bool"+ "format": "str" }, "statusVerb": null,- "summary": "Import a STEP/.stp (or other native-importable) file into a NEW Fusion design via the native import API. The open-electronics hints redirect .step here. (Was implemented but missing from this catalog until the verb suite's reverse-drift check caught it, 2026-08-14.)",- "timeoutSeconds": 300+ "summary": "Export to STL mesh.",+ "timeoutSec": 300 }, { "example": {- "fileName": "BQ25792",- "projectName": "Main"+ "outputPath": "C:/out/part.usdz" }, "input": {- "fileName": "required str",- "folderPath": "optional str",- "projectName": "optional str (defaults to active project)"+ "outputPath": "required str" }, "longRunning": true,- "name": "fusion_open_cloud_file",+ "name": "fusion_export_usdz", "output": {- "documentName": "str",- "success": "bool"+ "format": "str" },- "statusVerb": "fusion_get_app_state",- "summary": "Open a CLOUD file by name (the front door to every design flow). PCB/ELECTRONICS NAV RULE: open the electronics DESIGN document (EcadDesignProductType), never a child .brd/.sch/3D directly - then switch views with fusion_show_schematic / fusion_show_2d_board / fusion_show_3d_board. LONG-RUNNING: fire it, then poll fusion_get_app_state until activeDocument matches (cloud opens on electronics designs take 1-3 min). A crash-recovery modal can block the open - fusion_dismiss_blocking_dialogs.",- "timeoutSeconds": 240+ "statusVerb": null,+ "summary": "Export to USDZ (AR / Hydrogen 3D).",+ "timeoutSec": 300 }, { "example": {- "filePath": "C:/Temp/part.f3d"+ "jobId": "abc123",+ "outputPath": "C:/out/board.glb" }, "input": {- "filePath": "required str (WINDOWS path)"- },- "longRunning": true,- "name": "fusion_open_design",- "output": {- "success": "bool"- },- "statusVerb": null,- "summary": "Open a LOCAL design file (routes through the add-in import). Prefer cloud flows (fusion_open_cloud_file) for team designs; this is for local .f3d/.step style files.",- "timeoutSeconds": 240- },- {- "example": {- "save": false- },- "input": {- "name": "optional str (default: active document)",- "save": "optional bool default false"+ "exportId": "str (async form from export_optimized_glb async:true - returns {phase, done, glbPath} instantly per poll)",+ "jobId": "str (service job form)",+ "outputPath": "str (.glb)",+ "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)" }, "longRunning": false,- "name": "fusion_close_document",+ "name": "fusion_fetch_optimized_glb", "output": {- "closed": "str",- "success": "bool"+ "glbPath": "str",+ "pending": "bool",+ "sizeBytes": "int",+ "status": "str" }, "statusVerb": null,- "summary": "Close ONE document WITHOUT the modal save dialog (save:false discards silently - the automation-safe close). Omit name to close the active document.",- "timeoutSeconds": 60+ "summary": "Finish a fusion_export_optimized_glb job: bounded-poll the optimizer by jobId and write the GLB to outputPath when complete. Re-call until status=complete (big boards tessellate a few minutes). No Fusion needed.",+ "timeoutSec": 150 }, { "example": {- "closeUntitled": true- },- "input": {- "closeUntitled": "optional bool default false - also close UNMODIFIED Untitled scratch docs",- "force": "optional bool default false - with closeUntitled, also discard MODIFIED Untitled docs",- "save": "optional bool default false"- },- "longRunning": false,- "name": "fusion_close_all_documents",- "output": {- "closed": "[str]",- "success": "bool",- "wouldPrompt": "[str]|null"+ "etch": "103",+ "outputStep": "C:/tmp/ADOM-R0603-103.step",+ "params": {+ "A": 0.45,+ "D": 1.6,+ "E": 0.8,+ "L": 0.3,+ "L1": 0.3+ },+ "type": "chip" },- "statusVerb": null,- "summary": "Close ALL open documents without save dialogs. DESTRUCTIVE to unsaved user work - confirm with the human before calling on a box you do not own. By default it SKIPS scratch 'Untitled' docs; pass closeUntitled:true to close the UNMODIFIED ones too (page issue #59: the library/bind flow left ~5 per import, 16 after three, and no caller could clear them), and force:true to also discard MODIFIED Untitled docs, which may hold real unsaved work.",- "timeoutSeconds": 60- },- {- "example": {}, "input": {- "force": "optional bool default false - also discard MODIFIED Untitled docs"+ "etch": "optional str (marking text on the chip top: MPN / '103'; multi-line with newline, e.g. MPN+variant - laid along the LONGEST face axis)",+ "etchDepthMm": "optional float (marking height/depth, default 0.03)",+ "etchHeightMm": "optional float (text height; default auto-fit: top-5%-band face, 10% margin, width-aware)",+ "etchStyle": "optional str raised|engraved (default raised = thin WHITE positive extrude, silkscreen-style contrast; engraved = sunken cut)",+ "outputStep": "optional str (WINDOWS path; exports STEP incl. the marking)",+ "params": "optional dict (generator dims in MM by default, e.g. chip: {D,E,A,L,L1}; soic: {A,A1,b,D,E,E1,e,L,DPins}; omitted keys use EPG defaults)",+ "type": "required str (EPG family, e.g. chip|soic|qfn|qfp|bga|sot23|dfn2|melf|ecap|crystal|header_straight - see supportedTypes in the error for the full list)",+ "unitsCm": "optional bool (params already EPG-native cm)" },- "longRunning": false,- "name": "fusion_reset_workspace",+ "longRunning": true,+ "name": "fusion_generate_package", "output": {- "closedByAddin": "list",- "remaining": "list",- "success": "bool",- "untitledScratchClosed": "int"+ "bodies": "[{name,vol}]",+ "epgDir": "str",+ "etched": "str|null",+ "manifest": "obj (settings+why+bboxes; also written as <step>.manifest.json sidecar for marking refreshes)",+ "manifestPath": "str|null",+ "stepPath": "str|null",+ "success": "bool" },- "statusVerb": null,- "summary": "Bring Fusion back to a clean slate between runs: closes named documents AND the Untitled scratch the library/bind flow leaves behind (page issue #59). Fusion always keeps ONE blank document alive, so a single remaining Untitled is correct rather than a failure. MODIFIED Untitled docs are left untouched unless force:true, because those can hold real unsaved work.",- "timeoutSeconds": 90+ "statusVerb": "fusion_get_app_state",+ "summary": "Generate an IPC-7351-compliant parametric 3D package via Fusion's BUILT-IN ElectronicsPackageGenerator (EPG) - ~50 package families (chip/soic/qfn/qfp/bga/sot23/dfn/melf/ecap/crystal/headers/...), each a few seconds, ZERO GUI. Optionally LASER-ETCH a marking (MPN / '103') into the body top as REAL cut geometry (survives STEP), and export STEP in the same call - the royalty-free, license-clean path to a whole component library (our own generated output, not vendor models). Proven live: 0603 + '103' etch + STEP in ~2s.",+ "timeoutSec": 120 }, { "example": {}, "input": {}, "longRunning": false,- "name": "fusion_document_info",- "output": {- "isSaved": "bool",- "name": "str",- "productType": "str",- "success": "bool"- },- "statusVerb": null,- "summary": "Metadata for the ACTIVE document: name, product type, save state, version, parent project.",- "timeoutSeconds": 30- },- {- "example": {- "command": "Electron.ImmediateRepaint /full"- },- "input": {- "command": "required str",- "expectDocument": "optional str (refuses wrong_document if the active doc does not match)"- },- "longRunning": false,- "name": "fusion_execute_text_command",+ "name": "fusion_get_app_state", "output": {- "output": "str",- "success": "bool"+ "activeDocument": "str",+ "activeWorkspace": "str",+ "isElectronics": "bool" }, "statusVerb": null,- "summary": "Run a raw Fusion TEXT COMMAND (app.executeTextCommand) - the escape hatch to Fusion internals (Electron.* electronics commands included). Electron.* commands need an ELECTRONICS document context (schematic/board view active). Guard risky calls with expectDocument.",- "timeoutSeconds": 60+ "summary": "Current app state: running, active document, workspace, isElectronics. The poll/status verb for long-running calls.",+ "timeoutSec": 20 }, { "example": {},@@ -1410,7 +1196,7 @@ }, "statusVerb": null, "summary": "Design-level summary of the active design: components, bodies, sketches, units, root name.",- "timeoutSeconds": 60+ "timeoutSec": 60 }, { "example": {},@@ -1423,204 +1209,239 @@ }, "statusVerb": null, "summary": "All user + model parameters of the active design (name, expression, value, unit).",- "timeoutSeconds": 60+ "timeoutSec": 60 }, { "example": {}, "input": {},- "longRunning": true,- "name": "fusion_list_cloud_projects",+ "longRunning": false,+ "name": "fusion_get_preferences", "output": {- "projects": "list",- "success": "bool"+ "current": "obj" }, "statusVerb": null,- "summary": "List the cloud PROJECTS visible to the signed-in Fusion session (hub walk - slow on big teams; prefer fusion_aps_search for finding files).",- "timeoutSeconds": 120+ "summary": "Read current Fusion appearance/navigation preferences (theme, activeTheme, invertScrollZoom, orbitScheme, modelingOrientation, gestureNav, cameraPivot, lengthUnit) as friendly values - the same keys fusion_set_preference accepts.",+ "timeoutSec": 20 }, { "example": {- "projectName": "Main"+ "filePath": "C:/Temp/part.step" }, "input": {- "folderPath": "optional str",- "projectName": "optional str (defaults to active project)"+ "filePath": "required str (WINDOWS path C:/...; stage from container first)" }, "longRunning": true,- "name": "fusion_list_cloud_files",+ "name": "fusion_import_step", "output": {- "files": "list",+ "documentName": "str", "success": "bool" }, "statusVerb": null,- "summary": "List files/folders in a cloud project (optionally a subfolder).",- "timeoutSeconds": 120+ "summary": "Import a STEP/.stp (or other native-importable) file into a NEW Fusion design via the native import API. The open-electronics hints redirect .step here. (Was implemented but missing from this catalog until the verb suite's reverse-drift check caught it, 2026-08-14.)",+ "timeoutSec": 300 }, { "example": {- "outputPath": "C:/out/design.f3d"+ "worldSpace": true }, "input": {- "outputPath": "required str"+ "occurrence": "optional str (substring filter on occurrence name, worldSpace only)",+ "worldSpace": "optional bool (resolve through occurrence transforms; default false = root component only)"+ },+ "longRunning": false,+ "name": "fusion_inspect_bodies",+ "output": {+ "bodies": "[{name,owner,bbox:{x,y,z},sizeMm,volumeMm3,areaMm2,faceCount,cylindricalFaceCount,appearance,material,isVisible}]",+ "bodyCount": "int",+ "documentName": "str",+ "success": "bool",+ "units": "str (mm)" },+ "statusVerb": "fusion_get_app_state",+ "summary": "GEOMETRY READ-BACK: what actually got built, in MILLIMETRES. Per body: bbox, sizeMm, volumeMm3, areaMm2, faceCount, cylindricalFaceCount, appearance, material, isVisible. Use this to VERIFY your own modelling - a body at the wrong Z (a double-applied offset, a part buried inside another) renders perfectly and is INVISIBLE in a screenshot, and this work is often matte-black parts on a dark canvas where pixels are a weak check. cylindricalFaceCount is the cheap way to confirm holes were actually cut: a symmetric cut that reaches nothing fails SILENTLY with no exception. Pass worldSpace:true to resolve bodies through their occurrence transforms (assembly placement); optionally filter with occurrence. Fusion's API is centimetres - this converts for you.",+ "timeoutSec": 130+ },+ {+ "example": {},+ "input": {}, "longRunning": true,- "name": "fusion_export_f3d",+ "name": "fusion_install_fusion", "output": {- "format": "str"+ "alreadyInstalled": "bool?",+ "installing": "bool",+ "mode": "globalinstall|per-user",+ "statusVerb": "str",+ "success": "bool" },- "statusVerb": null,- "summary": "Export the active design as a Fusion archive (.f3d).",- "timeoutSeconds": 300+ "statusVerb": "fusion_get_app_state",+ "summary": "Install Fusion 360 FOR the user - downloads Autodesk's official Client Downloader and streams the free trial silently, with NO shell_execute and NO ab approval gate (runs inside the trusted bridge). Elevation-aware: --globalinstall when admin, PER-USER otherwise (a non-elevated globalinstall dies silently on UAC - learned live). Returns promptly; poll fusion_readiness (reports installing:true) until installed:true, then fusion_start. Declared as detect.installVerb so ab's bridge_readiness recommends it.",+ "timeoutSec": 90 }, { "example": {- "outputPath": "C:/out/part.fbx"+ "libraryName": "MyLib",+ "zipPath": "C:/Temp/MyLib.zip" }, "input": {- "outputPath": "required str"+ "capture": "optional bool (default true: bind + prove; false: place only)",+ "lbrPath": "str (staged .lbr, STEP models beside it)",+ "libraryName": "optional str (default: file stem)",+ "zipPath": "str (staged bundle zip on THIS machine) - or" }, "longRunning": true,- "name": "fusion_export_fbx",+ "name": "fusion_install_library_bundle", "output": {- "format": "str"+ "fusion": "{installed, running, addinReady, status, message}",+ "installed": "bool",+ "libraryPath": "str",+ "partMetadata": "{ok, summary, warnings[]}",+ "partsToBind": "int",+ "partsTotal": "int",+ "runId": "str",+ "statusVerb": "fusion_prove_result",+ "unbound": "[{package, why}]" },- "statusVerb": null,- "summary": "Export to FBX mesh.",- "timeoutSeconds": 300+ "statusVerb": "fusion_prove_result",+ "summary": "COMPOSITE, the Fusion twin of kicad_install_library_bundle (#68): ONE call takes a staged Cloud Library bundle (zip of <Lib>.lbr + <Part>.step models, or a bare lbrPath with STEPs beside it), unpacks it safely, places it under ~/Documents/Adom Libraries/<name>/, pairs EVERY deviceset with its STEP by name (deviceset, package, normalised, prefix; a one-part library pairs its single model regardless), lints part numbers (partMetadata: MPN, manufacturer, Mouser/DigiKey/LCSC - reported, never blocking), pre-flights Fusion state (fusion{status}), and starts the same bind -> prove chain fusion_create_library runs. ASYNC BY CONTRACT: returns IMMEDIATELY with runId + statusVerb because a STEP bind is 3-15 minutes and cannot fit a relay window; poll fusion_prove_result (measured percent, four streamed proof frames per part, provedParts[], and an addin_restart_required payload the CALLER turns into the ask). PARTIAL SHIPS: a deviceset with no model is a NAMED skip in unbound[], the rest still bind; partsBound < partsTotal is the signal. Each part is minted on its own single-deviceset library (the Package3D generator loads the FIRST package of the open library, so a merged library would put every chip on part one's footprint), with the STEP placed exactly as published (orient:false, origin trusted), then all urns are bound into the merged library, and after prove the bound library is SAVED TO THE CLOUD under <project>/Adom Libraries so it appears in Fusion's Library Manager (cloudLibrary{saved, fileId, duplicatesBefore} in fusion_prove_result). Per-part scaffolding is deleted; the user is left with one library plus its per-package f3d folder. capture:false only places the files and returns. This is the one contract the wiki CLI (`library install --format fusion`) and the Export button both read; the recipe has one owner.",+ "timeoutSec": 60 }, {- "example": {- "outputPath": "C:/out/part.sat"- },+ "example": {}, "input": {- "outputPath": "required str"+ "skipCleanClose": "optional bool" },- "longRunning": true,- "name": "fusion_export_sat",+ "longRunning": false,+ "name": "fusion_kill", "output": {- "format": "str"+ "output": "str",+ "success": "bool" }, "statusVerb": null,- "summary": "Export to SAT (ACIS solid).",- "timeoutSeconds": 300+ "summary": "Force-KILL Fusion 360 (taskkill /F) - the desperate path for when fusion_stop cannot close it (stuck modal / wedged process). Closes docs cleanly first when reachable.",+ "timeoutSec": 30 }, { "example": {- "outputPath": "C:/out/part.skp"+ "tab": "private" }, "input": {- "outputPath": "required str"+ "tab": "optional str hub|public|private (default private)" }, "longRunning": true,- "name": "fusion_export_skp",+ "name": "fusion_library_manager", "output": {- "format": "str"+ "hwnd": "int",+ "libraries": "[str] (usually empty - see pitfall)",+ "success": "bool",+ "tab": "str",+ "tableReadable": "bool" },- "statusVerb": null,- "summary": "Export to SketchUp (.skp).",- "timeoutSeconds": 300+ "statusVerb": "fusion_get_app_state",+ "summary": "Open Fusion's Library Manager and switch to one of its three tabs. Hub Libraries = libraries saved in the Fusion hub (what our bind chain produces, and what Autodesk recommends for sharing). Private Libraries = library.io managed libraries in folders THIS account belongs to. Public Libraries = library.io's public catalogue. PITFALL: the library TABLE is CEF-drawn and exposes NOTHING to UIA, so libraries[] is normally empty and that is not a failure - screenshot the returned hwnd to read the rows.",+ "timeoutSec": 240 }, { "example": {- "libraryPath": "C:/Temp/lib.lbr",- "name": "ADS8588SIPM"+ "libraryPath": "C:/tmp/TJA1050.lbr" }, "input": {- "capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).",- "foreground": "optional bool - DEFAULT IS BACKGROUND (no focus steal; response carries windowEtiquette:background). true REQUIRES foregroundReason and shows it to the user as a 3s ab caption before any focus change - only when the USER asked to watch",- "foregroundReason": "required WITH foreground:true - a short user-facing sentence shown as the 3s caption (e.g. 'You asked to watch the 3D board build')",- "libraryPath": "optional str (WINDOWS path .lbr)",- "name": "str (deviceset/symbol name; optional if symbolXml carries name=)",- "symbolXml": "optional str (bare <symbol ...>...</symbol>)"+ "libraryPath": "required str (also accepts lbrPath/filePath/path)" }, "longRunning": true,- "name": "fusion_show_symbol",+ "name": "fusion_library_parts", "output": {- "screenshot": "str path",+ "devicesets": "[{name, description, variants:[{fullName, device, technology, package, attributes, missing}]}]",+ "metadataOk": "bool", "success": "bool",- "view": "str",- "wrappedLibrary": "str (when wrapped)"+ "totals": "{devicesets, variants, withMpn, withDistributor}",+ "warnings": "[]" }, "statusVerb": null,- "summary": "Open a schematic SYMBOL in Fusion's library editor - first-class, no hand-built EDIT strings. Pass name + libraryPath (.lbr, opened first; omit libraryPath if a library is already open), OR pass a BARE <symbol> XML fragment as symbolXml and the bridge generates a minimal wrapper library around it (deviceset + gate synthesized) so it shows in the Library Manager. Background-safe: never steals the foreground.",- "timeoutSeconds": 180+ "summary": "Every ORDERABLE variant in a .lbr with its part numbers - offline, no Fusion needed. For an EE this is the point of the library: two variants can share ONE symbol, ONE footprint and ONE 3D body and still be different products (an AEC-Q100 automotive part vs the baseline one), and that difference lives ENTIRELY in the metadata. EAGLE spells the orderable identity as deviceset + device + technology: <device> carries the PACKAGE variant, <technology> carries the GRADE/temperature variant, and attributes hang off <technology>. Reports per-variant MPN / MANUFACTURER / MOUSER_PN / DIGIKEY_PN / LCSC_PN (the exact names this bridge's own BOM export reads), plus warnings when variants are indistinguishable, share an MPN, or a deviceset has no description. These warnings never block the library from opening - they say whether an EE could actually order from it.",+ "timeoutSec": 15 }, { "example": {- "libraryPath": "C:/Temp/lib.lbr",- "name": "ADS8588SIPM"+ "projectName": "Main" }, "input": {- "capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).",- "foreground": "optional bool - DEFAULT IS BACKGROUND (no focus steal; response carries windowEtiquette:background). true REQUIRES foregroundReason and shows it to the user as a 3s ab caption before any focus change - only when the USER asked to watch",- "foregroundReason": "required WITH foreground:true - a short user-facing sentence shown as the 3s caption (e.g. 'You asked to watch the 3D board build')",- "libraryPath": "optional str",- "name": "str",- "packageXml": "optional str (bare <package ...>...</package>)"+ "folderPath": "optional str",+ "projectName": "optional str (defaults to active project)" }, "longRunning": true,- "name": "fusion_show_footprint",+ "name": "fusion_list_cloud_files", "output": {- "screenshot": "str path",- "success": "bool",- "view": "str",- "wrappedLibrary": "str (when wrapped)"+ "files": "list",+ "success": "bool" }, "statusVerb": null,- "summary": "Open a FOOTPRINT (package) in Fusion's library editor. Same contract as fusion_show_symbol: name + libraryPath, or a bare <package> fragment as packageXml (wrapper library generated).",- "timeoutSeconds": 180+ "summary": "List files/folders in a cloud project (optionally a subfolder).",+ "timeoutSec": 120+ },+ {+ "example": {},+ "input": {},+ "longRunning": true,+ "name": "fusion_list_cloud_projects",+ "output": {+ "projects": "list",+ "success": "bool"+ },+ "statusVerb": null,+ "summary": "List the cloud PROJECTS visible to the signed-in Fusion session (hub walk - slow on big teams; prefer fusion_aps_search for finding files).",+ "timeoutSec": 120 }, { "example": {- "name": "MYCHIP",- "stepPath": "C:/Temp/chip.step"+ "action": "frames",+ "since": 0 }, "input": {- "capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).",- "foreground": "optional bool - DEFAULT IS BACKGROUND (no focus steal; response carries windowEtiquette:background). true REQUIRES foregroundReason and shows it to the user as a 3s ab caption before any focus change - only when the USER asked to watch",- "foregroundReason": "required WITH foreground:true - a short user-facing sentence shown as the 3s caption (e.g. 'You asked to watch the 3D board build')",- "libraryPath": "optional str (.lbr)",- "name": "str",- "packageXml": "optional str (bare <package> fragment)",- "stepPath": "optional str (WINDOWS path .step to attach)"+ "action": "start|stop|status|frames",+ "intervalSec": "optional float (default 1.0)",+ "maxSec": "optional float (default 600)",+ "since": "frames only: return ticks newer than this" }, "longRunning": true,- "name": "fusion_show_3d_package",+ "name": "fusion_live_feed", "output": {- "screenshot": "str path",+ "frames": "[{tick, kind:main|popup, path, title, at}]",+ "running": "bool", "success": "bool",- "view": "str",- "wrappedLibrary": "str"+ "tick": "int" }, "statusVerb": null,- "summary": "Open the 3D CHIP view (Package3DEnvironment) for a library package - and the RELIABLE way to get a 3D model into a library: pass stepPath and it is attached AFTER the library opens (embedded <packages3d> in hand-built .lbr files is the Fusion quirk that fails; attach-after-open works). stepPath with no library/packageXml auto-generates a minimal 2-pad carrier footprint. Without stepPath it just enters the 3D view of the named package.",- "timeoutSeconds": 300+ "summary": "A ~1 fps look INTO Fusion while the bridge drives it, kept as a BOX-SIDE ring buffer (~10 min). Each tick records the Fusion main window (kind:main - the video) and every owned popup over it (kind:popup - dialogs, Browser/tool panels, the Adom banner; Fusion draws these as separate windows a main capture omits), each a path on this machine tied to its tick. Drain from the container with {action:frames, since:<tick>}, read_file each path, and publish where a human looks - tools/live_feed_drain.py does exactly that into shotlog. Proof frames are NOT here: fusion_prove_result evidence[] carries kind:proof with step labels, so the three kinds are distinguishable at the source. Exists so a human can see open tabs and dialogs during boot or a restart instead of guessing.",+ "timeoutSec": 15 }, { "example": {- "demoId": "tour-abc123",- "imagesAs": "base64",- "sinceShot": 3+ "captureLabel": "R_4k7",+ "lbrPath": "C:/tmp/newlib/R_4k7.lbr",+ "modelPath": "C:/tmp/newlib/R_4k7.step" }, "input": {- "demoId": "required str (from fusion_demo mode:tour; omitted = the single running tour)",- "imagesAs": "optional str paths|base64|both",- "sinceShot": "optional int - shots you already received; response windows to the new ones"+ "captureLabel": "optional str (capture before/after)",+ "fileName": "optional str",+ "folderId": "optional str (default upload folder)",+ "lbrPath": "required str (.lbr WINDOWS path; its FIRST package's footprint is loaded)",+ "modelPath": "required str (.step WINDOWS path)",+ "orient": "optional bool (default true)",+ "projectId": "optional str (default MAIN-project)" },- "longRunning": false,- "name": "fusion_demo_progress",+ "longRunning": true,+ "name": "fusion_make_3d_package", "output": {- "done": "bool",- "percent": "int",- "shotCount": "int",- "shots": "list",- "step": "str",- "stepLabel": "str",- "stepShot": "dict|absent"+ "after": "str|null",+ "before": "str|null",+ "dims_mm": "[x,y,z]",+ "f3d": "str",+ "success": "bool",+ "wip_urn": "str" },- "statusVerb": null,- "summary": "Poll a running fusion_demo tour: {percent, elapsedSec, estimatedSec, step, stepLabel, done, failed, shots, shotCount, stepShot}. Poll ~1-2.5s and render a REAL progress bar. LIVE EVIDENCE (issue #30): each frame MAY carry stepShot {step, title, image} - a ~640px base64 capture the BRIDGE took at that step's completion, one per step, riding the first frame after it completes (render these during the run; the page should not guess-snap). shots[] streams too: mid-run it carries paths for every completed step; pass imagesAs:base64 + sinceShot:<count you have> to receive only NEW shots fully encoded. done:true = the full gallery (base64 by default). Finished tours are DISK-SNAPSHOTTED, so the final frame stays pollable even across a bridge respawn.",- "timeoutSeconds": 15+ "statusVerb": "fusion_get_app_state",+ "summary": "Create a RENDERING component 3D package (FOOTPRINT + chip, aligned) for ONE part, fully programmatically with ZERO GUI dialogs. Opens the .lbr, runs Electron.Create3DPackage to load the package's footprint into a generator doc, imports the STEP onto it, orients it flat, saveAs an .f3d (skips the FINISH Save dialog + the two unbeatable 'Fusion360' CEF modals), uploads the f3d via APS, and returns an fs.file:vf wip_urn to hand-write into the library's <packages3d>. projectId/folderId default to the MAIN-project upload folder. Pass captureLabel to also get a BEFORE (footprint) + AFTER (chip placed) screenshot. PITFALL: a RAW STEP upload binds but renders NOTHING ('Thumbnail download failed') - this verb makes an f3d, which renders. Prefer fusion_build_library_3d for many parts.",+ "timeoutSec": 200 }, { "example": {@@ -1641,766 +1462,926 @@ }, "statusVerb": null, "summary": "Create a NEW electronics test project FROM SCRATCH around a library part - the Web Control demo's PROJECT button, and the answer to 'a customer only has a component page'. Generates a minimal test circuit server-side (the part + a generated 330R series resistor with VCC/GND stubs on a small board) as real EAGLE .sch/.brd, then imports the pair as a new electronics design. NO cloud, NO sign-in, no pre-existing content. After it, fusion_show_schematic / fusion_show_2d_board / fusion_show_3d_board all work on the result. Wiring uses the deviceset's own connects (first two pins), so 2-terminal parts get a real circuit and big ICs get a structurally-valid integration proof.",- "timeoutSeconds": 300+ "timeoutSec": 300 }, { "example": {}, "input": {- "hwnd": "optional int (a dialog; default the main window)"+ "description": "optional str",+ "name": "optional str (defaults to the library's own name)" },- "longRunning": false,- "name": "fusion_screenshot_fusion",+ "longRunning": true,+ "name": "fusion_managed_library_create", "output": {- "savedTo": "str",- "sizeKB": "int",+ "alerts": "[...]",+ "library": "str", "success": "bool" },- "statusVerb": null,- "summary": "Background PrintWindow capture of Fusion's MAIN window (largest same-title window; works occluded/bottom-of-z-order, never raises anything) or a specific dialog by hwnd. Returns savedTo (PNG/WebP on the box; pull with pull_file). (Live since early versions but undescribed until 2026-08-15 - it was invisible to callers AND to the drift check.)",- "timeoutSeconds": 60+ "statusVerb": "fusion_get_app_state",+ "summary": "Create a NEW managed library on library.io FROM the library in front (which must already be saved in a Fusion Hub). Drives MANAGE > LIBRARY.IO > Push, then completes Fusion's Create Managed Library dialog. PITFALL: a managed library created under one Autodesk account is NOT visible to another account until its managed FOLDER is shared on library.io - proven live, so do not assume hub membership shares it.",+ "timeoutSec": 240 }, {- "example": {- "libraryPath": "C:/Temp/PART.lbr",- "stepPath": "C:/Temp/PART.step"+ "example": {},+ "input": {+ "name": "optional str (exact library name from the picker; omit to list read-only)",+ "onUpdatePrompt": "optional str cancel|merge|replace (default cancel; replace OVERWRITES the local library)"+ },+ "longRunning": true,+ "name": "fusion_managed_library_link",+ "output": {+ "alerts": "[...]",+ "linkedTo": "str?",+ "listedOnly": "bool?",+ "managedLibraries": "[{name,type,version,managedFolder,description}]",+ "success": "bool",+ "updatePrompt": "obj?" },+ "statusVerb": "fusion_get_app_state",+ "summary": "Link the library IN FRONT to an existing managed library on library.io. With no name, opens Fusion's Select Managed Library picker, returns its rows as records (name/type/version/managedFolder/description) and CANCELS - a safe read-only listing of what this Autodesk account can reach. With name, selects that row and confirms. SAFETY: after linking, Fusion offers to force-update your library FROM the managed one; 'Replace Local Library' would destroy your parts, so this verb answers CANCEL by default - pass onUpdatePrompt to choose deliberately. A managed library owned by ANOTHER Autodesk account does NOT appear until its managed FOLDER is shared with this account on library.io; being in the same Fusion hub is not enough.",+ "timeoutSec": 180+ },+ {+ "example": {}, "input": {- "deviceset": "optional str (defaults to the library's first)",- "libraryPath": "required str (WINDOWS .lbr path)",- "stepPath": "required str (WINDOWS .step path)"+ "onUpdatePrompt": "optional str merge|replace|cancel (default merge)" }, "longRunning": true,- "name": "fusion_bind_library_3d",+ "name": "fusion_managed_library_pull", "output": {- "bound": "bool",- "reopened": "bool",+ "alerts": "[...]",+ "library": "str", "success": "bool",- "wip_urn": "str"+ "upToDate": "bool",+ "updatePrompt": "obj?"+ },+ "statusVerb": "fusion_get_app_state",+ "summary": "Pull the newest version FROM library.io into the linked library in front. A pull ends in ONE of two CEF dialogs and BOTH block the add-in until answered: the force-update prompt when content differs (answered MERGE by default, since pull is an explicit request for the managed content - pass onUpdatePrompt:'replace' to overwrite local parts entirely, or 'cancel' to back out), or 'Up to Date' when already current. If neither appears, nothing was pulled and this reports failure rather than a false pass - Fusion greys Pull out on a library that was never linked.",+ "timeoutSec": 240+ },+ {+ "example": {},+ "input": {+ "onConfirm": "optional str create|cancel (default create)"+ },+ "longRunning": true,+ "name": "fusion_managed_library_push",+ "output": {+ "alerts": "[...]",+ "confirmed": "obj",+ "library": "str",+ "success": "bool"+ },+ "statusVerb": "fusion_get_app_state",+ "summary": "Push a NEW VERSION of the library in front up to its linked managed library on library.io. Clicking the menu item alone does NOT push: Fusion raises a CEF 'Create New Version' confirmation and THAT is what creates the version, so this verb completes it and fails with confirm_missing rather than reporting a false pass. On a library that is not linked yet, Fusion offers Create Managed Library instead and this reports not_linked_yet - use fusion_managed_library_create. NOTE: Library Manager keeps showing the IMPORTED version number after a push, so the row gains an update-available badge instead of incrementing.",+ "timeoutSec": 240+ },+ {+ "example": {},+ "input": {},+ "longRunning": true,+ "name": "fusion_managed_library_unlink",+ "output": {+ "alerts": "[...]",+ "library": "str",+ "success": "bool"+ },+ "statusVerb": "fusion_get_app_state",+ "summary": "Unlink the library in front from its managed library on library.io. Fusion greys this out on a library that was never linked, so a silent no-op there is expected.",+ "timeoutSec": 180+ },+ {+ "example": {+ "arguments": {+ "queryType": "summary"+ },+ "tool": "fusion_mcp_read"+ },+ "input": {+ "arguments": "optional obj (per that tool's inputSchema)",+ "timeout": "optional int seconds, default 120",+ "tool": "required str (from fusion_mcp_tools)"+ },+ "longRunning": true,+ "name": "fusion_mcp_call",+ "output": {+ "result": "obj",+ "tool": "str"+ },+ "statusVerb": "fusion_mcp_status",+ "summary": "Call any tool on Autodesk's Fusion MCP server (fusion_mcp_read, fusion_mcp_update, fusion_mcp_execute, fusion_mcp_electronics_read). The bridge handles the MCP streamable-HTTP handshake for you (initialize -> MCP-Session-Id -> notifications/initialized) and re-establishes a dropped session automatically. MCP acts on the ACTIVE Fusion document, so open one first. PREFER a native fusion_* verb when one exists - they are tested here and return richer hints - and reach for MCP for Autodesk's text-to-CAD surface.",+ "timeoutSec": 150+ },+ {+ "example": {},+ "input": {},+ "longRunning": true,+ "name": "fusion_mcp_enable",+ "output": {+ "alreadyOn": "bool",+ "enabled": "bool",+ "serverInfo": "obj",+ "steps": "list"+ },+ "statusVerb": "fusion_mcp_status",+ "summary": "Turn Autodesk's Fusion MCP server ON by driving Fusion's Preferences dialog for the user. Autodesk exposes NO API for this toggle, so the UI is the only route: this opens Preferences (Commands.Start PreferencesCommand), expands General, selects API, ticks 'Fusion MCP Server (runs locally on this device)', then clicks Apply and OK. Needs Fusion running; briefly uses the foreground and announces why with an on-screen caption. NEVER ask the user to do this by hand first - do it for them, then tell them it is handled. Note the setting is DISCARDED if Fusion is killed or restarted before Apply is clicked.",+ "timeoutSec": 180+ },+ {+ "example": {},+ "input": {+ "uri": "optional str (omit to list)"+ },+ "longRunning": false,+ "name": "fusion_mcp_resources",+ "output": {+ "count": "int",+ "resources": "[str]",+ "result": "obj"+ },+ "statusVerb": "fusion_mcp_status",+ "summary": "List Autodesk MCP resources, or read one by uri. These are mostly the Electronics entity schemas (resource://mcp.electronics_schema_*) that describe what fusion_mcp_electronics_read can return.",+ "timeoutSec": 60+ },+ {+ "example": {},+ "input": {},+ "longRunning": false,+ "name": "fusion_mcp_status",+ "output": {+ "enabled": "bool",+ "serverInfo": "obj",+ "toolCount": "int",+ "tools": "[{name,description}]"+ },+ "statusVerb": "fusion_mcp_status",+ "summary": "Is Autodesk's Fusion MCP server up, and what does it expose? Autodesk + Anthropic ship a LOCAL MCP server at http://127.0.0.1:27182/mcp exposing Fusion's own text-to-CAD surface (read geometry, execute/update features, read Electronics design data). It binds LOOPBACK on the user's machine and was built for Claude Desktop running there, so a CLOUD AI cannot reach it at all - this bridge runs on that machine and proxies it for you. Returns enabled, serverInfo and the live tool list. OFF by default: if it reports enabled:false, call fusion_mcp_enable and the bridge turns it on for the user.",+ "timeoutSec": 45+ },+ {+ "example": {},+ "input": {},+ "longRunning": false,+ "name": "fusion_mcp_tools",+ "output": {+ "count": "int",+ "tools": "list"+ },+ "statusVerb": "fusion_mcp_status",+ "summary": "Full tool list from Autodesk's Fusion MCP server INCLUDING input schemas, so you can construct a valid fusion_mcp_call. These are Autodesk's tools, not this bridge's.",+ "timeoutSec": 45+ },+ {+ "example": {+ "brdPath": "C:/Users/me/adom-lib/MyBoard.brd",+ "schPath": "C:/Users/me/adom-lib/MyBoard.sch"+ },+ "input": {+ "brdPath": "optional str (defaults to sibling .brd)",+ "schPath": "required str (Windows path to .sch)"+ },+ "longRunning": false,+ "name": "fusion_new_electronics_from_eagle",+ "output": {+ "imported": "list",+ "success": "bool" }, "statusVerb": null,- "summary": "LOW-LEVEL primitive: binds ONE STEP onto one deviceset. Prefer fusion_create_library with parts[] (which handles one part or many through a single path); reach for this only when you are driving a single bind deliberately. The CORRECT library in ONE call (the Library button's real bar: the 3D chip renders in the Content Manager preview): mints the part's 3D as a cloud f3d (make_3d_package), injects the wip_urn binding into the .lbr ON DISK (packages3d + package3dinstances), and reopens the library. Needs a signed-in Fusion session - the one cloud-dependent step in the wiki-to-EE circle. Autodesk's thumbnail preview lags ~2-3 min behind a fresh mint. NOTE: distinct from the older fusion_build_library_3d (multi-part batch pipeline).",- "timeoutSeconds": 480+ "summary": "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D.",+ "timeoutSec": 200 }, { "example": {- "libraryPath": "C:/Temp/PART.lbr"+ "body": "Please complete the Autodesk 2FA prompt on winvm - everything else is done.",+ "title": "Fusion needs you" }, "input": {- "capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).",- "libraryPath": "optional str (WINDOWS .lbr)",- "name": "optional str (already-open library name)"+ "body": "required str",+ "level": "optional str info|warning|error",+ "title": "optional str" },- "longRunning": true,- "name": "fusion_show_library",+ "longRunning": false,+ "name": "fusion_notify_owner", "output": {- "screenshot": "str", "success": "bool",- "view": "str"+ "targets": "list" }, "statusVerb": null,- "summary": "Eight-surfaces contract: the LIBRARY surface. libraryPath (WINDOWS .lbr) opens it; name alone handles the already-open case (symmetric with kicad_show_library). Background; returns a screenshot path.",- "timeoutSeconds": 240+ "summary": "LAST-RESORT human escalation: toast the user's MAIN computer (fans out cross-ab to every peer on the relay, so it reaches them even when this bridge runs on an unattended VM). ONLY for true human walls - password/2FA entry, UAC elevation, a physical step. Exhaust programmatic options first (UIA background clicks, seat auto-resolve, warm-SSO sign-in). After sending, WAIT and poll fusion_readiness; do not re-toast within a few minutes.",+ "timeoutSec": 20 }, { "example": {- "query": "BQ25792"+ "urn": "urn:adsk.wipprod:fs.file:vf.xxxx?version=1" }, "input": {- "capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).",- "fileName": "alias of query",- "query": "optional str (cloud design name)",- "urn": "optional str"+ "urn": "required str" }, "longRunning": true,- "name": "fusion_show_project",+ "name": "fusion_open_by_urn", "output": {- "document": "str",- "screenshot": "str",- "success": "bool",- "view": "str"+ "output": "str",+ "success": "bool" }, "statusVerb": "fusion_get_app_state",- "summary": "Eight-surfaces contract: bring the PROJECT/design to its top level. No args = activate this session's generated test project (fusion_make_test_project). query = cloud design name (already-open match wins, else fire-and-poll open). urn = open_by_urn passthrough. Background; returns a screenshot path.",- "timeoutSeconds": 240+ "summary": "Open a cloud ELECTRONICS DESIGN by URN. PCB/ELECTRONICS NAV RULE: a board is NOT a standalone file - the schematic, 2D board and 3D board are VIEWS inside ONE electronics DESIGN. Open the DESIGN document first (fusion_open_cloud_file or fusion_open_by_urn), THEN switch to its schematic and its 2D board (fusion_show_2d_board) and 3D board (fusion_show_3d_board). A Select-Electronics-Design-File picker IS that design list of its schematic + board. An EMPTY 2D board usually means you opened a derivative or the wrong file, or it is out of sync - reopen the parent electronics DESIGN and switch to its board view; do NOT open a .brd or .sch as a standalone file.",+ "timeoutSec": 240 }, { "example": {},+ "input": {},+ "longRunning": false,+ "name": "fusion_open_by_urn_note",+ "output": {},+ "statusVerb": null,+ "summary": "(see fusion_open_by_urn)",+ "timeoutSec": 30+ },+ {+ "example": {+ "fileName": "BQ25792",+ "projectName": "Main"+ }, "input": {- "enabled": "optional bool (omit to read)"+ "fileName": "required str",+ "folderPath": "optional str",+ "projectName": "optional str (defaults to active project)" },- "longRunning": false,- "name": "fusion_set_window_sizing",+ "longRunning": true,+ "name": "fusion_open_cloud_file", "output": {- "sizeFusionWindow": "bool",+ "documentName": "str", "success": "bool" },- "statusVerb": "fusion_state",- "summary": "Read/toggle auto-sizing Fusion's WINDOW after launch (STICKY, default ON). Fusion opens at whatever geometry it last had, which after an automated launch is often a small default; the bridge grows it to ~92% of the monitor work area, centered, using SetWindowPos with NOACTIVATE (never ShowWindow/maximize) so focus is never stolen. A window the user maximized, or already sized to >=60% of the work area, is left alone. No args = read.",- "timeoutSeconds": 15+ "statusVerb": "fusion_get_app_state",+ "summary": "Open a CLOUD file by name (the front door to every design flow). PCB/ELECTRONICS NAV RULE: open the electronics DESIGN document (EcadDesignProductType), never a child .brd/.sch/3D directly - then switch views with fusion_show_schematic / fusion_show_2d_board / fusion_show_3d_board. LONG-RUNNING: fire it, then poll fusion_get_app_state until activeDocument matches (cloud opens on electronics designs take 1-3 min). A crash-recovery modal can block the open - fusion_dismiss_blocking_dialogs.",+ "timeoutSec": 240 }, {- "example": {},+ "example": {+ "filePath": "C:/Temp/part.f3d"+ }, "input": {- "enabled": "optional bool (omit to read)"+ "filePath": "required str (WINDOWS path)" },- "longRunning": false,- "name": "fusion_set_driving_badge",+ "longRunning": true,+ "name": "fusion_open_design", "output": {- "showDrivingBadge": "bool", "success": "bool" },- "statusVerb": "fusion_state",- "summary": "Read/toggle the 'Adom is driving Fusion' TASKBAR OVERLAY BADGE (STICKY per user, default ON). While the bridge drives Fusion, its Windows taskbar button wears a small Adom mark so the user knows at a glance who is in control; it sheds automatically after 5 minutes of driving-verb idle. No args = read. Turning it off clears any badge immediately.",- "timeoutSeconds": 15+ "statusVerb": null,+ "summary": "Open a LOCAL design file (routes through the add-in import). Prefer cloud flows (fusion_open_cloud_file) for team designs; this is for local .f3d/.step style files.",+ "timeoutSec": 240 }, {- "example": {},+ "example": {+ "filePath": "C:/Users/<user>/adom-lib/nRF54L15.lbr"+ }, "input": {- "enabled": "optional bool (omit to read)"+ "filePath": "required str (.lbr WINDOWS path C:/...; stage from container first)",+ "symbolName": "optional str",+ "verify": "optional bool" }, "longRunning": false,- "name": "fusion_set_seat_takeover",+ "name": "fusion_open_lbr", "output": {- "autoTakeOverSeat": "bool",+ "postOpenScreenshot": "{savedTo}", "success": "bool" },- "statusVerb": "fusion_readiness",- "summary": "Read/toggle the mid-session Autodesk seat-takeover sweep (STICKY per user, default ON). When Autodesk pops 'Active Sessions Exceeded' mid-session, the bridge clicks Continue through BOTH dialogs in the background: this machine takes the seat, the other machine's Fusion suspends RESUMABLY, the user gets one toast. No args = read current value. Gates only the passive sweep - a verb-driven fusion_start still grabs the seat (the user asked for Fusion, so the seat is implied).",- "timeoutSeconds": 15+ "statusVerb": null,+ "summary": "Import an EAGLE .lbr library into Fusion's Electronics LIBRARY editor (Content Manager / Electronics Library workspace), not a PCB design. PITFALL: filePath MUST be a WINDOWS-local path (C:/...). The chip-fetcher/adom-lbr pipeline runs in the cloud container, and there is NO container-to-Windows push verb, so STAGE the .lbr onto Windows first (serve it on a proxied port + Invoke-WebRequest via shell_execute), then pass the C:/ path. PITFALL: an adom-lbr .lbr is 2D ONLY - symbol + footprint + a PLACEHOLDER 3D package; the real 3D chip is NOT attached (that needs a Fusion cloud URN). To attach the 3D, see the 'fusion-libraries' skill. Pass verify:true to round-trip-confirm Fusion parsed the deviceset, then check fusion_get_app_state (activeWorkspace 'Electronics Library').",+ "timeoutSec": 50 }, {- "example": {},+ "example": {+ "accuracy": "low"+ }, "input": {- "imageAs": "optional str path|base64 (default path)",- "screenshot": "optional bool (default true)"+ "accuracy": "optional low|medium|high|veryhigh (default low)",+ "names": "optional [str] component names to measure (default all)" }, "longRunning": false,- "name": "fusion_state",+ "name": "fusion_physical_properties", "output": {- "activeDocument": "str",- "blockingDialogs": "list",- "focusEvents": "list - the QUIETNESS AUDIT LOG (SDK canonical): every time this bridge took the user's foreground, or refused a bare foreground:true, as {at, verb, outcome: foreground|refused, reason}. Audit how quiet the bridge actually was instead of trusting that it was.",- "mainThreadBusy": "dict|null",- "running": "bool",- "screenshot": "str path",- "screenshotBase64": "str (imageAs:base64 only)",- "success": "bool",- "timings": "dict - measured launch history on THIS machine (p50/p90 per phase, cold vs warm)"+ "count": "int",+ "properties": "{name: {volume_cm3, mass_kg, density, area_cm2, center_of_mass}}",+ "success": "bool" }, "statusVerb": null,- "summary": "The RENDER-ME verb (kicad_state parity, issues #28/#30): running + window + dialog + busy state PLUS a fresh background screenshot of whatever Fusion is showing - the one verb that makes every background fusion_show_* result visible. SERVER-SIDE ONLY by design: it answers even while the add-in main thread is busy or wedged (activeDocument is best-effort with a short budget and skipped when busy). blockingDialogs carries the CLASSIFIED modal list (cause + resolution). imageAs:base64 inlines the PNG (auto-refused over 4MB - pull the screenshot path via pull_file instead).",- "timeoutSeconds": 60+ "summary": "Per-component physical properties (volume cm3, mass kg, density, area, center of mass) for the active design. Reads them directly so BOM/costing tools no longer have to run getPhysicalProperties inside a modeling script. Pass names to scope to specific components.",+ "timeoutSec": 120 }, { "example": {- "outputPath": "C:/out/board.iges"+ "parts": [+ {+ "lbrPath": "C:/tmp/al8843.lbr",+ "modelPath": "C:/tmp/al8843.step",+ "package": "AL8843SP-13"+ }+ ] }, "input": {- "outputPath": "required str"+ "libraryPath": "optional .lbr for context",+ "parts": "optional [{package, lbrPath, modelPath}] - the parts you intend to import (drives the estimate)",+ "stepPath": "optional single .step when not passing parts[]" }, "longRunning": true,- "name": "fusion_export_iges",+ "name": "fusion_plan_library_import", "output": {- "format": "str"+ "estimatedSec": "float",+ "estimatedSecIfAlreadyOpen": "float",+ "fusion": "{installed, running, addinReady, status, message}",+ "plan": "[{stage, label, estimatedSec, confidence, needsUserConsent?}]",+ "success": "bool",+ "summary": "str" }, "statusVerb": null,- "summary": "Export to IGES.",- "timeoutSeconds": 300+ "summary": "PRE-FLIGHT for a library import: does NO work and changes nothing. Reports whether Fusion is installed, running, or needs its add-in loaded (with a plain-language message you can show the user), plus a per-stage plan with MEASURED per-machine time estimates scaled to the number of parts you are sending - a multi-part library does one STEP import per part, which is the slowest stage, so the estimate grows with the library. Call this FIRST so the wiki can render the summary and the progress bar's steps, then run the import and poll fusion_prove_result, which reports the SAME stage names.",+ "timeoutSec": 20 }, { "example": {- "outputPath": "C:/out/part.stl"+ "libraryPath": "C:/Temp/PART.lbr" }, "input": {- "outputPath": "required str"+ "libraryPath": "str (Windows .lbr path)" }, "longRunning": true,- "name": "fusion_export_stl",+ "name": "fusion_preflight_lbr", "output": {- "format": "str"+ "preflight": "{ok, checks[], errors[]}",+ "success": "bool" }, "statusVerb": null,- "summary": "Export to STL mesh.",- "timeoutSeconds": 300+ "summary": "PRE-ANALYZE a .lbr: will Fusion open it? Offline and instant - no Fusion needed, nothing launched. Runs the named checks the bridge learned from real Fusion rejections: file-readable, size-sane (truncated-transfer corpse detection), xml-parses, root-is-eagle, packages-nonempty (Fusion's silent 'No Components'), symbols-present, devicesets-present (missing ones make EDIT raise Create-new prompts), connects-resolve (dead wiring renders fine and connects nothing). fusion_open_lbr and fusion_create_library run the same gate automatically; call this directly to gate a batch BEFORE staging.",+ "timeoutSec": 10 }, { "example": {- "outputPath": "C:/out/part.3mf"+ "save": true }, "input": {- "outputPath": "required str"+ "save": "optional bool, default true" },- "longRunning": true,- "name": "fusion_export_3mf",+ "longRunning": false,+ "name": "fusion_prefs_close", "output": {- "format": "str"+ "closed": "bool",+ "saved": "bool" },- "statusVerb": null,- "summary": "Export to 3MF mesh.",- "timeoutSeconds": 300+ "statusVerb": "fusion_get_preferences",+ "summary": "Apply+OK (save:true, the default) or Cancel Fusion's Preferences dialog. Settings changed via fusion_prefs_open are NOT persisted until this is called with save:true.",+ "timeoutSec": 60 }, { "example": {- "outputPath": "C:/out/part.usdz"+ "section": "api" }, "input": {- "outputPath": "required str"+ "section": "optional str (see knownSections in the response)" }, "longRunning": true,- "name": "fusion_export_usdz",+ "name": "fusion_prefs_open", "output": {- "format": "str"+ "hwnd": "int",+ "knownSections": "[str]",+ "screenshot": "str",+ "shotId": "str",+ "steps": "list" },- "statusVerb": null,- "summary": "Export to USDZ (AR / Hydrogen 3D).",- "timeoutSeconds": 300+ "statusVerb": "fusion_get_preferences",+ "summary": "Open Fusion's Preferences dialog and optionally jump to a section, then hand back a SCREENSHOT + shotId you can click in. This is how you reach ANY Fusion preference: the Python API exposes only a thin slice (theme/orbit/units, debuggingPort, developer tools) and everything else, including the MCP server toggle, is UI-only. Sections: general, api, design, manufacture, electronics, render, drawing, material, graphics, network, preview features. Child sections under General are lazily rendered by Qt and never appear in the UIA tree, which is why this returns an image to click rather than control names. ALWAYS finish with fusion_prefs_close {save:true} - nothing is saved otherwise, and killing/restarting Fusion first discards the change.",+ "timeoutSec": 120 }, { "example": {- "outputPath": "C:/out/board.glb"+ "runId": "a3f1c2d4e5b6" }, "input": {- "async": "bool - RECOMMENDED true: fires the whole export+optimize pipeline in the background and returns {exportId} instantly (the sync form can outlive the relay window on a cold Fusion session); poll fusion_fetch_optimized_glb {exportId}",- "outputPath": "required str (.glb)",- "pin": "str medium|large default medium",- "silkscreen": "bool default true",- "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)"+ "inlineFrames": "optional bool",+ "runId": "optional str (omit for the latest run)",+ "shots": "optional int",+ "sinceShot": "optional int" }, "longRunning": true,- "name": "fusion_export_optimized_glb",+ "name": "fusion_prove_result", "output": {- "glbPath": "str",- "jobId": "str",- "meshesAfter": "int",- "moleculeAnchored": "bool",- "pending": "bool",- "silkscreenApplied": "bool",- "sizeBytes": "int"+ "etaSec": "float",+ "evidence": "[]",+ "percent": "float",+ "stage": "str",+ "verdict": "str" }, "statusVerb": null,- "summary": "Fusion board -> wiki-grade optimized GLB. Exports STEP (+ optional silkscreen) and submits it to service-step2glb molecule mode (anchor to MP pins + silk bake + gold pins + join/weld/prune + Draco): ~465KB/~31 draw calls vs a raw ~16MB/~25000 that halts the viewer. Bounded-waits `wait`s (default 90); small boards return complete, big boards return {pending, jobId} - finish with fusion_fetch_optimized_glb. Use for a component page's component.parts.model_3d. STACKUP RULE (John 2026-07-15, see pcb-stackup skill): board bbox = the FR4 body, NEVER the whole assembly (components overhang edges + tower above); each overlay goes at ITS layer's z (copper at FR4-top, silk above mask); copper traces are REAL 35um 3D bodies colored like the FR4 - recolor them + translucent mask in the RAW GLB before optimizing to make routing visible; put the measured stackup table on the board's wiki page. PUBLISH EVERYTHING: a board's wiki page repo must carry EVERY exportable design file (.f3d/.step/.usdz/EAGLE .brd+.sch/Fusion .fbrd+.fsch/gerbers/BOM/CPL/GLBs/renders), not just the 3D - the page exists to share the design.",- "timeoutSeconds": 200+ "summary": "POLL a background run started by fusion_create_library or the detached fusion_build_library_3d. Returns stage + stageLabel, plan[] with measured per-stage estimates, stagesDone, percent, etaSec, confidence, and evidence[] filling in AS EACH ARTIFACT LANDS (symbol -> footprint -> 3d -> ganged). For a 3D build it carries per-part progress (minting X (3/7)), partsDone/partsTotal and parts[] with each wip_urn. stage:done only means the chain FINISHED - the top-level verdict (proven/not_proven) is the score, and a run that captured zero frames lands stage:failed with blocked_by_dialog (carrying the dialog text) or no_evidence_captured rather than reporting done. Pass inlineFrames:true on the final fetch for base64 thumbnails a browser can render.",+ "timeoutSec": 20 }, {- "example": {- "jobId": "abc123",- "outputPath": "C:/out/board.glb"- },- "input": {- "exportId": "str (async form from export_optimized_glb async:true - returns {phase, done, glbPath} instantly per poll)",- "jobId": "str (service job form)",- "outputPath": "str (.glb)",- "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)"- },+ "example": {},+ "input": {}, "longRunning": false,- "name": "fusion_fetch_optimized_glb",+ "name": "fusion_readiness", "output": {- "glbPath": "str",- "pending": "bool",- "sizeBytes": "int",- "status": "str"+ "bridgeVersion": "str",+ "hostApp": "str",+ "installed": "bool",+ "ready": "bool",+ "running": "bool" }, "statusVerb": null,- "summary": "Finish a fusion_export_optimized_glb job: bounded-poll the optimizer by jobId and write the GLB to outputPath when complete. Re-call until status=complete (big boards tessellate a few minutes). No Fusion needed.",- "timeoutSeconds": 150+ "summary": "FAST readiness check (does NOT launch Fusion): is the Fusion host app installed + running + the bridge ready to drive? ab DETECTS Fusion (never installs it) via bridge.json 'detect', and auto-installs Python if missing (ab >=1.9.47). Call this before driving Fusion; if installed-but-not-running, fusion_start. SELF-HEALS the seat/licensing dialog (background UIA + screenshot-verify) whenever it detects one. Pairs with ab's bridge_readiness.",+ "timeoutSec": 15 }, { "example": {},- "input": {},- "longRunning": true,- "name": "fusion_board_stackup",+ "input": {+ "force": "optional bool default false - also discard MODIFIED Untitled docs"+ },+ "longRunning": false,+ "name": "fusion_reset_workspace", "output": {- "copperCount": "int",- "copperLayers": "list",- "copperThickness_mm": "list",- "dielectricBonds": "list (prepreg/core order)",- "fr4": "{x_mm,y_mm,dielectric_mm}",- "layerSetup": "str"+ "closedByAddin": "list",+ "remaining": "list",+ "success": "bool",+ "untitledScratchClosed": "int" }, "statusVerb": null,- "summary": "Read a board's PHYSICAL fabrication stackup (see the pcb-stackup skill): copper layer count + copper/dielectric thicknesses from the EAGLE design rules (layerSetup/mtCopper/mtIsolate) plus the measured FR4 extent. A PCB is Cu/prepreg/Cu/core/.../Cu - the FR4 is NOT one slab. Use to build the stackup table + real-thickness exploded 3D view, and put a stackup table on every exported board's wiki page.",- "timeoutSeconds": 200+ "summary": "Bring Fusion back to a clean slate between runs: closes named documents AND the Untitled scratch the library/bind flow leaves behind (page issue #59). Fusion always keeps ONE blank document alive, so a single remaining Untitled is correct rather than a failure. MODIFIED Untitled docs are left untouched unless force:true, because those can hold real unsaved work.",+ "timeoutSec": 90 }, { "example": {- "outputPath": "C:/out/part.obj"+ "save": true }, "input": {- "outputPath": "required str"+ "save": "optional bool (default true) - save open documents before restarting" },- "longRunning": true,- "name": "fusion_export_obj",+ "longRunning": false,+ "name": "fusion_restart_for_addin", "output": {- "format": "str"+ "addinReady": "bool",+ "steps": "[{step, ok}]",+ "success": "bool",+ "tookSec": "float" }, "statusVerb": null,- "summary": "Export to OBJ mesh.",- "timeoutSeconds": 300+ "summary": "ASYNC: returns immediately (started:true) and restarts Fusion in the background, SAVING the user's open documents first; poll fusion_readiness until ready with the new addinVersion (a restart takes 60-120s, longer than the relay budget; pass wait:true for the old blocking form). Finish add-in setup by restarting Fusion. Call this when any verb returns errorCode addin_restart_required (the add-in was installed/updated while Fusion was already open, and Fusion only loads add-ins at launch). ASK THE USER FIRST when a human is present - the failing verb's payload carries userMessage plus openDocuments/unsavedDocuments so you can show them what is open. Refuses with unsaved_named_document rather than discarding a never-saved document.",+ "timeoutSec": 300 }, { "example": {- "outputPath": "C:/out/sketch.dxf"+ "script": "import adsk.fusion\n# ...build geometry..." }, "input": {- "outputPath": "required str"+ "script": "required str (Python using adsk; globals: adsk, app, ui, design)" }, "longRunning": true,- "name": "fusion_export_dxf",+ "name": "fusion_run_modeling_script", "output": {- "format": "str"+ "documentName": "str",+ "result": "obj" }, "statusVerb": null,- "summary": "Export 2D DXF.",- "timeoutSeconds": 120+ "summary": "Run an adsk.fusion modeling script in the live session (sketches/extrudes/params). Free programmatic CAD.",+ "timeoutSec": 180 }, { "example": {- "outputPath": "C:/out/sketch.dwg"+ "name": "TJA1050",+ "projectName": "Main" }, "input": {- "outputPath": "required str"+ "description": "optional str",+ "folderPath": "optional str",+ "name": "required str",+ "projectName": "optional str" },- "longRunning": true,- "name": "fusion_export_dwg",+ "longRunning": false,+ "name": "fusion_save_to_cloud", "output": {- "format": "str"+ "success": "bool",+ "urn": "str" }, "statusVerb": null,- "summary": "Export 2D DWG.",- "timeoutSeconds": 120+ "summary": "Save the ACTIVE document to Autodesk cloud storage with an explicit name and destination. Dialog-free precisely because you supply what the Save box would ask for - use it before closing or restarting when a document has never been saved (fusion_close_all_documents refuses such a document with unsaved_named_document rather than discarding it).",+ "timeoutSec": 180 }, {- "example": {- "outputDir": "C:/tmp/jlc"- },+ "example": {}, "input": {- "outputDir": "optional str (default C:/tmp/adom-gerbers)"+ "hwnd": "optional int (a dialog; default the main window)" },- "longRunning": true,- "name": "fusion_export_gerbers",+ "longRunning": false,+ "name": "fusion_screenshot_fusion", "output": {- "fileCount": "int",- "files": "[{name,size}]",- "zipPath": "str"+ "savedTo": "str",+ "sizeKB": "int",+ "success": "bool" }, "statusVerb": null,- "summary": "Run the bundled JLCPCB CAM job (auto 2/4-layer) to produce the gerber + drill ZIP.",- "timeoutSeconds": 180+ "summary": "Background PrintWindow capture of Fusion's MAIN window (largest same-title window; works occluded/bottom-of-z-order, never raises anything) or a specific dialog by hwnd. Returns savedTo (PNG/WebP on the box; pull with pull_file). (Live since early versions but undescribed until 2026-08-15 - it was invisible to callers AND to the drift check.)",+ "timeoutSec": 60 }, { "example": {- "outputPath": "C:/tmp/bom.csv"+ "query": "Molecule" }, "input": {- "grouped": "optional bool (default true)",- "outputPath": "optional str"+ "query": "required str" }, "longRunning": false,- "name": "fusion_export_bom",+ "name": "fusion_search_cloud_files", "output": {- "componentCount": "int",- "grouped": "bool"+ "disabled": "bool" }, "statusVerb": null,- "summary": "Export a Bill of Materials CSV (grouped by value+package) from the open board.",- "timeoutSeconds": 60+ "summary": "\u26d4 DISABLED (returns an error): the in-app add-in search took 30+ min and CRASHED Fusion (WinError 10054). It is hard-blocked in the bridge. Use fusion_aps_search {query} \u2014 server-indexed, seconds, no Fusion. See the 'fusion-aps-search' skill.",+ "timeoutSec": 15 }, { "example": {- "outputPath": "C:/tmp/cpl.csv"+ "enabled": true }, "input": {- "outputPath": "optional str",- "side": "optional str (all|top|bottom)"+ "enabled": "optional bool (omit to read current value)" },- "longRunning": false,- "name": "fusion_export_cpl",+ "longRunning": true,+ "name": "fusion_set_auto_update", "output": {- "bottomCount": "int",- "topCount": "int"+ "autoUpdateFusion": "bool",+ "narrate": "str" },- "statusVerb": null,- "summary": "Export the Component Placement List (pick-and-place) CSV.",- "timeoutSeconds": 60+ "statusVerb": "fusion_readiness",+ "summary": "Turn AUTOMATIC Fusion updating on/off for this user. ON BY DEFAULT, and deliberately INVISIBLE: Fusion ships updates constantly and nags with an 'Update Now / Update Later' panel plus a '14 days until Fusion must update' countdown. Every fusion_readiness call checks for that offer and, if present, the bridge clicks it IN THE BACKGROUND (UIA, no foreground, no caption) so the download starts and the newer build comes up on the next launch. The user should never have to click it. Call with no args to read the current setting; {enabled:false} opts out and the choice is STICKY (~/.adom/fusion-bridge/prefs.json) - once a user opts out, NEVER click their update prompts again. Tell the user this is handled for them rather than asking them to update.",+ "timeoutSec": 30 }, { "example": {- "action": "apply",+ "fab": "jlcpcb", "layers": "2" }, "input": {- "action": "optional str (apply|export|show)",- "layers": "optional str (auto|2|4)"+ "action": "optional str (apply|list|export|show)",+ "druPath": "optional str local .edru/.dru to load as-is",+ "fab": "optional str vendorSlug (jlcpcb|pcbway|osh-park|seeed-fusion|eurocircuits|aisler|elecrow|advanced-circuits-4pcb|sierra-circuits|macrofab|nextpcb|allpcb|adom-instapcb)",+ "layers": "optional str (auto|2|4|6)",+ "process": "optional str (the vendor's tier: standard|advanced|proto|enig|hasl|...; default standard)" }, "longRunning": false, "name": "fusion_set_design_rules", "output": { "activeWorkspace": "str",- "description": "str"+ "description": "str",+ "pack": "dict {fab, vendor, process, layers, file, record}" }, "statusVerb": null,- "summary": "Load manufacturing-grade DRC into the open board (JLCPCB 2/4-layer profiles).",- "timeoutSeconds": 90+ "summary": "Load a FAB's design rules (DRC) into the open board. The rule packs live on the wiki page adom/pcb-design-rules (JLCPCB, PCBWay, OSH Park, Seeed, Eurocircuits, Aisler, Elecrow, Advanced Circuits, Sierra, MacroFab, NextPCB, ALLPCB and Adom InstaPCB; one .edru per vendor/process/layer count, every number citing the vendor's own capabilities page): the bridge fetches the pack, stages it on the box, and the add-in runs `drc load`. {action:'list'} answers with every (fab, process, layers) row so you can offer the user a real choice; no fab means the bundled Adom InstaPCB rules; druPath loads a local .edru. Layers auto-detects from the open board. ALWAYS confirm with fusion_alerts afterwards: a refused file is an in-canvas toast, never a dialog.",+ "timeoutSec": 90 }, { "example": {},- "input": {},- "longRunning": false,- "name": "fusion_detect_layers",- "output": {- "copperLayers": "[int]",- "layerCount": "int"+ "input": {+ "enabled": "optional bool (omit to read)" },- "statusVerb": null,- "summary": "Detect 2-layer vs 4-layer board (ULP + CAM comparison).",- "timeoutSeconds": 90- },- {- "example": {},- "input": {}, "longRunning": false,- "name": "fusion_dismiss_blocking_dialogs",+ "name": "fusion_set_driving_badge", "output": {- "dialogs": "[...]"+ "showDrivingBadge": "bool",+ "success": "bool" },- "statusVerb": null,- "summary": "Detect + classify (and optionally dismiss) blocking Fusion dialogs.",- "timeoutSeconds": 30+ "statusVerb": "fusion_state",+ "summary": "Read/toggle the 'Adom is driving Fusion' TASKBAR OVERLAY BADGE (STICKY per user, default ON). While the bridge drives Fusion, its Windows taskbar button wears a small Adom mark so the user knows at a glance who is in control; it sheds automatically after 5 minutes of driving-verb idle. No args = read. Turning it off clears any badge immediately.",+ "timeoutSec": 15 }, { "example": {- "command": "set confirm yes;RUN 'C:/tmp/jlcpcb_smta_exporter.ulp' 'C:/tmp/jlc'"+ "theme": "dark" }, "input": {- "command": "required str"+ "cameraPivot": "optional bool",+ "gestureNav": "optional bool",+ "invertScrollZoom": "optional bool",+ "lengthUnit": "optional str",+ "modelingOrientation": "optional str",+ "orbitScheme": "optional str",+ "theme": "optional str" },- "longRunning": false,- "name": "fusion_electron_run",+ "longRunning": true,+ "name": "fusion_set_preference", "output": {- "output": "str",- "success": "bool"+ "applied": "obj",+ "current": "obj" }, "statusVerb": null,- "summary": "Run an EAGLE/Electron command in the open board (the generic extension point, e.g. RUN <ulp>).",- "timeoutSeconds": 90+ "summary": "Set Fusion appearance/navigation preferences in the LIVE session - applies immediately, NO restart. Keys: theme (light/darkblue/darkgray/classic/device/dark; some builds only ship LightGray/DarkBlue/Device, failures reported per-key), invertScrollZoom (bool - mouse scroll/zoom direction), orbitScheme (fusion360/alias/inventor/solidworks/tinkercad/powermill), modelingOrientation (yup/zup), gestureNav (bool), cameraPivot (bool), lengthUnit (mm/cm/m/in/ft - active design). Per-key outcome in `applied`, resulting state in `current`.",+ "timeoutSec": 30 }, {- "example": {- "factor": 3,- "steps": 20- },+ "example": {}, "input": {- "factor": "float (default 2.0)",- "fit": "optional bool",- "frameDelayMs": "optional int (default 35)",- "steps": "optional int (default 16)"+ "enabled": "optional bool (omit to read)" },- "longRunning": true,- "name": "fusion_electron_zoom",+ "longRunning": false,+ "name": "fusion_set_seat_takeover", "output": {- "output": "str",+ "autoTakeOverSeat": "bool", "success": "bool" },- "statusVerb": null,- "summary": "Smoothly ZOOM the schematic/2D board for a recording (the whole motion is one call). factor>1 zooms in, <1 out; or fit:true. The loop runs in the add-in with a repaint per frame.",- "timeoutSeconds": 60+ "statusVerb": "fusion_readiness",+ "summary": "Read/toggle the mid-session Autodesk seat-takeover sweep (STICKY per user, default ON). When Autodesk pops 'Active Sessions Exceeded' mid-session, the bridge clicks Continue through BOTH dialogs in the background: this machine takes the seat, the other machine's Fusion suspends RESUMABLY, the user gets one toast. No args = read current value. Gates only the passive sweep - a verb-driven fusion_start still grabs the seat (the user asked for Fusion, so the seat is implied).",+ "timeoutSec": 15 }, {- "example": {- "x1": 0,- "x2": 10,- "y1": 0,- "y2": 8- },+ "example": {}, "input": {- "frameDelayMs": "optional int (default 35)",- "steps": "optional int (default 16)",- "x1": "float",- "x2": "float",- "y1": "float",- "y2": "float"+ "enabled": "optional bool (omit to read)" },- "longRunning": true,- "name": "fusion_electron_pan",+ "longRunning": false,+ "name": "fusion_set_window_sizing", "output": {- "box": "[...]",- "output": "str",+ "sizeFusionWindow": "bool", "success": "bool" },- "statusVerb": null,- "summary": "Smoothly PAN + zoom the view to frame a board-coordinate box (mm; get part positions from fusion_board_info). Animates from where the view last was.",- "timeoutSeconds": 60+ "statusVerb": "fusion_state",+ "summary": "Read/toggle auto-sizing Fusion's WINDOW after launch (STICKY, default ON). Fusion opens at whatever geometry it last had, which after an automated launch is often a small default; the bridge grows it to ~92% of the monitor work area, centered, using SetWindowPos with NOACTIVATE (never ShowWindow/maximize) so focus is never stolen. A window the user maximized, or already sized to >=60% of the work area, is left alone. No args = read.",+ "timeoutSec": 15 }, {- "example": {- "name": "U1"- },+ "example": {}, "input": {- "name": "str (refdes, e.g. R1)",- "properties": "optional bool (default true)",- "x": "optional float",- "y": "optional float"+ "capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30)." }, "longRunning": true,- "name": "fusion_electron_select",+ "name": "fusion_show_2d_board", "output": {- "deviceInfo": "str",- "output": "str",- "success": "bool"+ "activeWorkspace": "str" }, "statusVerb": null,- "summary": "SELECT a part (by reference designator, or x/y board mm) and surface its properties - the way clicking a part reveals the properties panel. Great for showing life in a recording.",- "timeoutSeconds": 30+ "summary": "Switch the OPEN electronics design to its 2D PCB Editor (board layout) view. This is a VIEW of the project, not a file to open standalone - if the board is empty, you opened a child/derivative instead of the EcadDesignProductType PROJECT; reopen the project. Drive the view with fusion_electron_run WINDOW commands.",+ "timeoutSec": 70 }, {- "example": {- "runId": "a3f1c2d4e5b6"- },+ "example": {}, "input": {- "inlineFrames": "optional bool",- "runId": "optional str (omit for the latest run)",- "shots": "optional int",- "sinceShot": "optional int"+ "capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30)." }, "longRunning": true,- "name": "fusion_prove_result",+ "name": "fusion_show_3d_board", "output": {- "etaSec": "float",- "evidence": "[]",- "percent": "float",- "stage": "str",- "verdict": "str"+ "activeWorkspace": "str",+ "screenshots": "[...]" }, "statusVerb": null,- "summary": "POLL a background run started by fusion_create_library or the detached fusion_build_library_3d. Returns stage + stageLabel, plan[] with measured per-stage estimates, stagesDone, percent, etaSec, confidence, and evidence[] filling in AS EACH ARTIFACT LANDS (symbol -> footprint -> 3d -> ganged). For a 3D build it carries per-part progress (minting X (3/7)), partsDone/partsTotal and parts[] with each wip_urn. stage:done only means the chain FINISHED - the top-level verdict (proven/not_proven) is the score, and a run that captured zero frames lands stage:failed with blocked_by_dialog (carrying the dialog text) or no_evidence_captured rather than reporting done. Pass inlineFrames:true on the final fetch for base64 thumbnails a browser can render.",- "timeoutSeconds": 20+ "summary": "Switch the OPEN electronics design to its 3D PCB view (populated board). A VIEW of the project, not a standalone file. Orbit it for demos via fusion_run_modeling_script (app.activeViewport.camera + vp.refresh()).",+ "timeoutSec": 70 }, { "example": {- "runId": "a3f1c2d4e5b6"+ "name": "MYCHIP",+ "stepPath": "C:/Temp/chip.step" }, "input": {- "runId": "optional str (omit for the latest run in flight)"- },- "longRunning": true,- "name": "fusion_cancel_run",- "output": {- "cancelRequested": "bool",- "runId": "str",- "stage": "str at the time of the request",- "statusVerb": "fusion_prove_result"+ "capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).",+ "foreground": "optional bool - DEFAULT IS BACKGROUND (no focus steal; response carries windowEtiquette:background). true REQUIRES foregroundReason and shows it to the user as a 3s ab caption before any focus change - only when the USER asked to watch",+ "foregroundReason": "required WITH foreground:true - a short user-facing sentence shown as the 3s caption (e.g. 'You asked to watch the 3D board build')",+ "libraryPath": "optional str (.lbr)",+ "name": "str",+ "packageXml": "optional str (bare <package> fragment)",+ "stepPath": "optional str (WINDOWS path .step to attach)" },- "statusVerb": "fusion_prove_result",- "summary": "STOP a library import that is in flight (fusion_install_library_bundle / fusion_create_library) - the user's escape hatch from a long run, wired to a Cancel button in web control. Cooperative, not a kill: the run finishes the part it is on (a STEP import cannot be interrupted mid-command without leaving a half-made design, up to ~60s), keeps every 3D package already minted and binds them into the library, closes its scratch documents, and leaves the library open in Fusion (not saved to the cloud). The run then reports stage:cancelled, verdict:cancelled, errorCode cancelled_by_user, cancelledAt (which stage) and how many parts were done, on the same fusion_prove_result poll the caller already makes; cancelRequested:true shows on the very next poll before the boundary is reached. Omit runId to cancel the latest run still in flight. Idempotent; a run that already ended answers alreadyFinished.",- "timeoutSeconds": 10- },- {- "example": {},- "input": {}, "longRunning": true,- "name": "fusion_addin_status",+ "name": "fusion_show_3d_package", "output": {- "addinReady": "bool",- "busy": "bool",- "port": "int",- "version": "str"+ "screenshot": "str path",+ "success": "bool",+ "view": "str",+ "wrappedLibrary": "str" }, "statusVerb": null,- "summary": "Is the AdomBridge add-in loaded and answering inside Fusion? Reports addinReady, port and version, plus busy when a long command holds the main thread. If Fusion is running but this says not ready, the add-in was installed while Fusion was open - Fusion loads add-ins only at launch - so call fusion_restart_for_addin (ask the user first when one is present).",- "timeoutSeconds": 15+ "summary": "Open the 3D CHIP view (Package3DEnvironment) for a library package - and the RELIABLE way to get a 3D model into a library: pass stepPath and it is attached AFTER the library opens (embedded <packages3d> in hand-built .lbr files is the Fusion quirk that fails; attach-after-open works). stepPath with no library/packageXml auto-generates a minimal 2-pad carrier footprint. Without stepPath it just enters the 3D view of the named package.",+ "timeoutSec": 300 }, { "example": {- "name": "TJA1050"+ "libraryPath": "C:/Temp/lib.lbr",+ "name": "ADS8588SIPM" }, "input": {- "name": "required str (document name as reported by fusion_get_app_state)"+ "capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).",+ "foreground": "optional bool - DEFAULT IS BACKGROUND (no focus steal; response carries windowEtiquette:background). true REQUIRES foregroundReason and shows it to the user as a 3s ab caption before any focus change - only when the USER asked to watch",+ "foregroundReason": "required WITH foreground:true - a short user-facing sentence shown as the 3s caption (e.g. 'You asked to watch the 3D board build')",+ "libraryPath": "optional str",+ "name": "str",+ "packageXml": "optional str (bare <package ...>...</package>)" },- "longRunning": false,- "name": "fusion_activate_document",+ "longRunning": true,+ "name": "fusion_show_footprint", "output": {- "activeDocument": "str",- "success": "bool"+ "screenshot": "str path",+ "success": "bool",+ "view": "str",+ "wrappedLibrary": "str (when wrapped)" }, "statusVerb": null,- "summary": "Make a named open document the ACTIVE one before driving it. EDIT and most modelling verbs act on whatever is active, so in a batch with several documents open this is how you stop a verb landing in the wrong file (page issue #47). Prefer passing expectDocument on the verb itself, which fails closed instead of merely re-targeting.",- "timeoutSeconds": 60+ "summary": "Open a FOOTPRINT (package) in Fusion's library editor. Same contract as fusion_show_symbol: name + libraryPath, or a bare <package> fragment as packageXml (wrapper library generated).",+ "timeoutSec": 180 }, { "example": {- "limit": 20+ "libraryPath": "C:/Temp/PART.lbr" }, "input": {- "limit": "optional int (default 50)"+ "capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).",+ "libraryPath": "optional str (WINDOWS .lbr)",+ "name": "optional str (already-open library name)" }, "longRunning": true,- "name": "fusion_action_ledger",+ "name": "fusion_show_library", "output": {- "actions": "[{at, action, detail}]"+ "screenshot": "str",+ "success": "bool",+ "view": "str" }, "statusVerb": null,- "summary": "Read the append-only record of what the bridge did in the BACKGROUND on this machine - dialogs it answered, Untitled scratch docs it swept, Fusion auto-starts, add-in restarts. Survives bridge respawns (~/.adom/fusion-bridge/actions.jsonl). This is the audit trail for why something changed on the user screen.",- "timeoutSeconds": 15+ "summary": "Eight-surfaces contract: the LIBRARY surface. libraryPath (WINDOWS .lbr) opens it; name alone handles the already-open case (symmetric with kicad_show_library). Background; returns a screenshot path.",+ "timeoutSec": 240 }, { "example": {- "name": "TJA1050",- "projectName": "Main"+ "query": "BQ25792" }, "input": {- "description": "optional str",- "folderPath": "optional str",- "name": "required str",- "projectName": "optional str"+ "capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).",+ "fileName": "alias of query",+ "query": "optional str (cloud design name)",+ "urn": "optional str" },- "longRunning": false,- "name": "fusion_save_to_cloud",+ "longRunning": true,+ "name": "fusion_show_project", "output": {+ "document": "str",+ "screenshot": "str", "success": "bool",- "urn": "str"+ "view": "str" },- "statusVerb": null,- "summary": "Save the ACTIVE document to Autodesk cloud storage with an explicit name and destination. Dialog-free precisely because you supply what the Save box would ask for - use it before closing or restarting when a document has never been saved (fusion_close_all_documents refuses such a document with unsaved_named_document rather than discarding it).",- "timeoutSeconds": 180+ "statusVerb": "fusion_get_app_state",+ "summary": "Eight-surfaces contract: bring the PROJECT/design to its top level. No args = activate this session's generated test project (fusion_make_test_project). query = cloud design name (already-open match wins, else fire-and-poll open). urn = open_by_urn passthrough. Background; returns a screenshot path.",+ "timeoutSec": 240 }, { "example": {}, "input": {- "outputPath": "optional str"+ "capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30)." }, "longRunning": true,- "name": "fusion_take_screenshot",+ "name": "fusion_show_schematic", "output": {- "path": "str",+ "activeWorkspace": "str",+ "productType": "str", "success": "bool" }, "statusVerb": null,- "summary": "Screenshot the Fusion window in the BACKGROUND (no focus steal, reads through occlusion). Use it to VERIFY your own work: geometry at the wrong Z, an empty view, or a Placeholder 3D body all render successfully and only a look at the pixels catches them.",- "timeoutSeconds": 60+ "summary": "Switch the OPEN electronics design to its SCHEMATIC view (the symmetric partner of fusion_show_2d_board / fusion_show_3d_board, via Fusion's SwitchSchDocCmd). A VIEW of the project, not a standalone file. Drive it for demos with fusion_electron_zoom / _pan / _select.",+ "timeoutSec": 70 }, {- "example": {},- "input": {},+ "example": {+ "libraryPath": "C:/Temp/lib.lbr",+ "name": "ADS8588SIPM"+ },+ "input": {+ "capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).",+ "foreground": "optional bool - DEFAULT IS BACKGROUND (no focus steal; response carries windowEtiquette:background). true REQUIRES foregroundReason and shows it to the user as a 3s ab caption before any focus change - only when the USER asked to watch",+ "foregroundReason": "required WITH foreground:true - a short user-facing sentence shown as the 3s caption (e.g. 'You asked to watch the 3D board build')",+ "libraryPath": "optional str (WINDOWS path .lbr)",+ "name": "str (deviceset/symbol name; optional if symbolXml carries name=)",+ "symbolXml": "optional str (bare <symbol ...>...</symbol>)"+ }, "longRunning": true,- "name": "fusion_verb_times",+ "name": "fusion_show_symbol", "output": {- "stages": "{name: {count, p50, p90}}"+ "screenshot": "str path",+ "success": "bool",+ "view": "str",+ "wrappedLibrary": "str (when wrapped)" }, "statusVerb": null,- "summary": "This machine MEASURED timings per verb/stage - the same store that paces every progress bar and ETA. Read it to see whether an estimate is measured (this box has run it) or typical (still the seeded literal), and to explain a slow run.",- "timeoutSeconds": 15+ "summary": "Open a schematic SYMBOL in Fusion's library editor - first-class, no hand-built EDIT strings. Pass name + libraryPath (.lbr, opened first; omit libraryPath if a library is already open), OR pass a BARE <symbol> XML fragment as symbolXml and the bridge generates a minimal wrapper library around it (deviceset + gate synthesized) so it shows in the Library Manager. Background-safe: never steals the foreground.",+ "timeoutSec": 180 }, {- "example": {- "libraryPath": "C:/tmp/TJA1050.lbr"- },+ "example": {}, "input": {- "libraryPath": "required str (also accepts lbrPath/filePath/path)"+ "auto": "optional bool (do click+capture+reopen in one on-box call - recommended)",+ "profile": "optional str (chrome:<email>/edge:<email> - the profile your Autodesk account is in; remembered)",+ "waitSec": "optional int (server-side poll budget for the fresh URL, default 32)" }, "longRunning": true,- "name": "fusion_library_parts",+ "name": "fusion_signin", "output": {- "devicesets": "[{name, description, variants:[{fullName, device, technology, package, attributes, missing}]}]",- "metadataOk": "bool",- "success": "bool",- "totals": "{devicesets, variants, withMpn, withDistributor}",- "warnings": "[]"+ "done": "bool",+ "narrate": "str",+ "notifyDelivered": "bool",+ "signinAuthUrl": "str",+ "signinOpenedVia": "str (extension|extension_free)",+ "signinProfile": "str",+ "signinProfileReason": "str",+ "signinWall": "str|null (twofactor_email_code|credentials|protocol_handoff)",+ "signinWallHint": "str|null",+ "signinWhere": "str",+ "signinWrongBrowser": "str",+ "stage": "str" },- "statusVerb": null,- "summary": "Every ORDERABLE variant in a .lbr with its part numbers - offline, no Fusion needed. For an EE this is the point of the library: two variants can share ONE symbol, ONE footprint and ONE 3D body and still be different products (an AEC-Q100 automotive part vs the baseline one), and that difference lives ENTIRELY in the metadata. EAGLE spells the orderable identity as deviceset + device + technology: <device> carries the PACKAGE variant, <technology> carries the GRADE/temperature variant, and attributes hang off <technology>. Reports per-variant MPN / MANUFACTURER / MOUSER_PN / DIGIKEY_PN / LCSC_PN (the exact names this bridge's own BOM export reads), plus warnings when variants are indistinguishable, share an MPN, or a deviceset has no description. These warnings never block the library from opening - they say whether an EE could actually order from it.",- "timeoutSeconds": 15+ "statusVerb": "fusion_signin",+ "summary": "SELF-DRIVING as of 1.9.32: launches Fusion if needed, clicks Sign In, opens the OAuth in the right profile, TYPES THE EMAIL and advances to the exact remaining wall (password/2FA/handoff), toasting the user at that step only - see signinWall + _hint in the response, and the fusion-autodesk-signin skill. Sign Fusion in through the CORRECT browser profile - the fix for Fusion's dumb OAuth that fires at the OS-DEFAULT browser and lands in the wrong Autodesk account. Fusion opens its sign-in in whatever browser is default (e.g. a personal Chrome), but a user's Autodesk seat often lives in a different profile (work). This verb reads the FULL authorize URL (client_id/PKCE/state/request_id) that Fusion wrote into that wrong browser's HISTORY and re-opens it in the RIGHT profile IN THE BACKGROUND; the URL completes back to the running Fusion via the idmgr/callback + autodesk:// handoff, so the browser no longer has to be the OS default. Pass {profile:'chrome:[email protected]'} to fix + remember your Autodesk profile; else it probes each profile for a live Autodesk session (or asks). Staged: if Fusion hasn't opened the browser yet it returns stage:'click_signin' so you click Fusion's Sign In first. BEST: pass {auto:true} to run the WHOLE chain in ONE server-side call - it clicks Fusion's Sign In, polls the browser history on-box for the fresh authorize URL, and re-opens it in the target profile, all without extra remote round-trips, so it fits inside Fusion's ~2-min OAuth request expiry even on a slow/flaky relay (the manual staged path is the fallback). Fusion's request EXPIRES in ~2 min, so a stale captured URL returns stage:'stale' - restart Fusion (fusion_stop/start) for a fresh request and retry auto PROMPTLY. See the 'fusion-multiprofile-signin' skill.",+ "timeoutSec": 120 }, { "example": {- "parts": [- {- "lbrPath": "C:/tmp/al8843.lbr",- "modelPath": "C:/tmp/al8843.step",- "package": "AL8843SP-13"- }- ]+ "code": "123456" }, "input": {- "libraryPath": "optional .lbr for context",- "parts": "optional [{package, lbrPath, modelPath}] - the parts you intend to import (drives the estimate)",- "stepPath": "optional single .step when not passing parts[]"+ "code": "required str (the 6 digits; non-digits are stripped)",+ "hwnd": "optional int (the 2FA window; auto-detected)" }, "longRunning": true,- "name": "fusion_plan_library_import",+ "name": "fusion_signin_2fa", "output": {- "estimatedSec": "float",- "estimatedSecIfAlreadyOpen": "float",- "fusion": "{installed, running, addinReady, status, message}",- "plan": "[{stage, label, estimatedSec, confidence, needsUserConsent?}]",- "success": "bool",- "summary": "str"+ "narrate": "str",+ "steps": "list",+ "submitted": "bool" },- "statusVerb": null,- "summary": "PRE-FLIGHT for a library import: does NO work and changes nothing. Reports whether Fusion is installed, running, or needs its add-in loaded (with a plain-language message you can show the user), plus a per-stage plan with MEASURED per-machine time estimates scaled to the number of parts you are sending - a multi-part library does one STEP import per part, which is the slowest stage, so the estimate grows with the library. Call this FIRST so the wiki can render the summary and the progress bar's steps, then run the import and poll fusion_prove_result, which reports the SAME stage names.",- "timeoutSeconds": 20+ "statusVerb": "fusion_readiness",+ "summary": "Submit Autodesk's emailed 6-DIGIT verification code to finish a Fusion sign-in, typed IN THE BACKGROUND (UIA SetValue + Invoke - never steals the user's foreground or moves their cursor). Exists so you can finish a login END TO END for the user instead of parking them on a 2FA screen: if `adom-google` is available in your container, read the code out of their Gmail (`adom-google gmail search 'from:autodesk verification code' --limit 1`), pull the 6 digits, and call this. If adom-google is NOT installed, tell the user that installing it would let you automate logins like this, then let them type it. NEVER ask the user for a code you could read yourself. The bridge ALSO toasts the user's desktop whenever it hits a human wall, so you do not need to ask it to.",+ "timeoutSec": 90 }, {- "example": {- "action": "frames",- "since": 0+ "example": {},+ "input": {},+ "longRunning": true,+ "name": "fusion_start",+ "output": {+ "addinReady": "bool",+ "status": "str",+ "success": "bool" },+ "statusVerb": null,+ "summary": "Launch Fusion 360 (if needed) and wait for the AdomBridge add-in to be ready.",+ "timeoutSec": 150+ },+ {+ "example": {}, "input": {- "action": "start|stop|status|frames",- "intervalSec": "optional float (default 1.0)",- "maxSec": "optional float (default 600)",- "since": "frames only: return ticks newer than this"+ "imageAs": "optional str path|base64 (default path)",+ "screenshot": "optional bool (default true)" },- "longRunning": true,- "name": "fusion_live_feed",+ "longRunning": false,+ "name": "fusion_state", "output": {- "frames": "[{tick, kind:main|popup, path, title, at}]",+ "activeDocument": "str",+ "blockingDialogs": "list",+ "focusEvents": "list - the QUIETNESS AUDIT LOG (SDK canonical): every time this bridge took the user's foreground, or refused a bare foreground:true, as {at, verb, outcome: foreground|refused, reason}. Audit how quiet the bridge actually was instead of trusting that it was.",+ "mainThreadBusy": "dict|null", "running": "bool",+ "screenshot": "str path",+ "screenshotBase64": "str (imageAs:base64 only)", "success": "bool",- "tick": "int"+ "timings": "dict - measured launch history on THIS machine (p50/p90 per phase, cold vs warm)" }, "statusVerb": null,- "summary": "A ~1 fps look INTO Fusion while the bridge drives it, kept as a BOX-SIDE ring buffer (~10 min). Each tick records the Fusion main window (kind:main - the video) and every owned popup over it (kind:popup - dialogs, Browser/tool panels, the Adom banner; Fusion draws these as separate windows a main capture omits), each a path on this machine tied to its tick. Drain from the container with {action:frames, since:<tick>}, read_file each path, and publish where a human looks - tools/live_feed_drain.py does exactly that into shotlog. Proof frames are NOT here: fusion_prove_result evidence[] carries kind:proof with step labels, so the three kinds are distinguishable at the source. Exists so a human can see open tabs and dialogs during boot or a restart instead of guessing.",- "timeoutSeconds": 15+ "summary": "The RENDER-ME verb (kicad_state parity, issues #28/#30): running + window + dialog + busy state PLUS a fresh background screenshot of whatever Fusion is showing - the one verb that makes every background fusion_show_* result visible. SERVER-SIDE ONLY by design: it answers even while the add-in main thread is busy or wedged (activeDocument is best-effort with a short budget and skipped when busy). blockingDialogs carries the CLASSIFIED modal list (cause + resolution). imageAs:base64 inlines the PNG (auto-refused over 4MB - pull the screenshot path via pull_file instead).",+ "timeoutSec": 60 }, {- "example": {- "save": true- },+ "example": {}, "input": {- "save": "optional bool (default true) - save open documents before restarting"+ "skipCleanClose": "optional bool" }, "longRunning": false,- "name": "fusion_restart_for_addin",+ "name": "fusion_stop", "output": {- "addinReady": "bool",- "steps": "[{step, ok}]",- "success": "bool",- "tookSec": "float"+ "output": "str",+ "success": "bool" }, "statusVerb": null,- "summary": "ASYNC: returns immediately (started:true) and restarts Fusion in the background, SAVING the user's open documents first; poll fusion_readiness until ready with the new addinVersion (a restart takes 60-120s, longer than the relay budget; pass wait:true for the old blocking form). Finish add-in setup by restarting Fusion. Call this when any verb returns errorCode addin_restart_required (the add-in was installed/updated while Fusion was already open, and Fusion only loads add-ins at launch). ASK THE USER FIRST when a human is present - the failing verb's payload carries userMessage plus openDocuments/unsavedDocuments so you can show them what is open. Refuses with unsaved_named_document rather than discarding a never-saved document.",- "timeoutSeconds": 300+ "summary": "Gracefully STOP Fusion 360: close docs cleanly + WM_CLOSE, then wait. NO force-kill - the clean opposite of fusion_start. If a window will not close it returns failure; use fusion_kill then.",+ "timeoutSec": 60 }, {- "example": {- "worldSpace": true- },+ "example": {}, "input": {- "occurrence": "optional str (substring filter on occurrence name, worldSpace only)",- "worldSpace": "optional bool (resolve through occurrence transforms; default false = root component only)"+ "outputPath": "optional str" },- "longRunning": false,- "name": "fusion_inspect_bodies",+ "longRunning": true,+ "name": "fusion_take_screenshot", "output": {- "bodies": "[{name,owner,bbox:{x,y,z},sizeMm,volumeMm3,areaMm2,faceCount,cylindricalFaceCount,appearance,material,isVisible}]",- "bodyCount": "int",- "documentName": "str",- "success": "bool",- "units": "str (mm)"+ "path": "str",+ "success": "bool" },- "statusVerb": "fusion_get_app_state",- "summary": "GEOMETRY READ-BACK: what actually got built, in MILLIMETRES. Per body: bbox, sizeMm, volumeMm3, areaMm2, faceCount, cylindricalFaceCount, appearance, material, isVisible. Use this to VERIFY your own modelling - a body at the wrong Z (a double-applied offset, a part buried inside another) renders perfectly and is INVISIBLE in a screenshot, and this work is often matte-black parts on a dark canvas where pixels are a weak check. cylindricalFaceCount is the cheap way to confirm holes were actually cut: a symmetric cut that reaches nothing fails SILENTLY with no exception. Pass worldSpace:true to resolve bodies through their occurrence transforms (assembly placement); optionally filter with occurrence. Fusion's API is centimetres - this converts for you.",- "timeoutSeconds": 130+ "statusVerb": null,+ "summary": "Screenshot the Fusion window in the BACKGROUND (no focus steal, reads through occlusion). Use it to VERIFY your own work: geometry at the wrong Z, an empty view, or a Placeholder 3D body all render successfully and only a look at the pixels catches them.",+ "timeoutSec": 60 }, { "example": {@@ -2423,20 +2404,53 @@ }, "statusVerb": null, "summary": "CASCADE CLEANUP, called by ab's bridge_uninstall BEFORE this bridge's cache is deleted (ab 2.0.35+). Removes the artifacts this bridge planted OUTSIDE its cache: the Fusion ADD-IN in Autodesk's AddIns directories (all three historical locations), plus this bridge's prefs (~/.adom/fusion-bridge) and APS OAuth tokens (~/.adom/fusion-aps). This matters more here than for most bridges: a stale AdomBridge add-in keeps loading on every Fusion launch and binding its port long after the bridge is gone. Fusion itself is never touched. Pass dryRun:true to see exactly what would be removed. If Fusion is RUNNING it holds the add-in files open, so removal may fail - the response says so and names the remedy.",- "timeoutSeconds": 60+ "timeoutSec": 60+ },+ {+ "example": {},+ "input": {},+ "longRunning": true,+ "name": "fusion_verb_times",+ "output": {+ "stages": "{name: {count, p50, p90}}"+ },+ "statusVerb": null,+ "summary": "This machine MEASURED timings per verb/stage - the same store that paces every progress bar and ETA. Read it to see whether an estimate is measured (this box has run it) or typical (still the seeded literal), and to explain a slow run.",+ "timeoutSec": 15 }, { "example": {}, "input": {}, "longRunning": false,- "name": "fusion_describe",+ "name": "fusion_walk_cloud_tree", "output": {- "success": "bool",- "verbs": "[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example}]"+ "disabled": "bool" }, "statusVerb": null,- "summary": "Self-describe every verb the bridge exposes (this list) for ab's Verbs tab + runner.",- "timeoutSeconds": 20+ "summary": "\u26d4 DISABLED (returns an error): the in-app add-in walk took 30+ min and CRASHED Fusion (WinError 10054). It is hard-blocked in the bridge. Use fusion_aps_search / fusion_aps_open. See the 'fusion-aps-search' skill.",+ "timeoutSec": 15+ },+ {+ "example": {+ "url": "https://library.io/"+ },+ "input": {+ "provider": "optional str google|password (default google)",+ "sessionId": "optional str (default autodesk-web)",+ "url": "required str (e.g. https://library.io/)",+ "username": "optional str (for the Google account chooser)"+ },+ "longRunning": true,+ "name": "fusion_web_signin",+ "output": {+ "errorCode": "str?",+ "steps": "[...]",+ "success": "bool",+ "url": "str"+ },+ "statusVerb": "fusion_get_app_state",+ "summary": "Sign a pup browser window into an Autodesk WEB surface (library.io, the hub portal at <hub>.autodesk360.com, Autodesk Account) via Google SSO. Fusion's DESKTOP app being signed in does NOT sign these in - they are separate web sessions. NEVER guesses a credential: a missing password or a 2FA challenge comes back as a typed stop (human_credential_required / second_factor_required / google_sso_unavailable) naming the one-time command the USER runs. The password never passes through this bridge or the AI - it is stored once with ab's credential_set and filled by pup in the background.",+ "timeoutSec": 240 } ]-}+}
dashboard/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "fusion-dashboard"-version = "1.9.351"+version = "1.9.352" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.351"+version = "1.9.352" edition = "2021" [dependencies]
describe.py+2−2@@ -255,8 +255,8 @@ _T = [ {"outputPath": "optional str", "grouped": "optional bool (default true)"}, {"componentCount": "int", "grouped": "bool"}, 60, None, False, {"outputPath": "C:/tmp/bom.csv"}), ("fusion_export_cpl", "Export the Component Placement List (pick-and-place) CSV.", {"outputPath": "optional str", "side": "optional str (all|top|bottom)"}, {"topCount": "int", "bottomCount": "int"}, 60, None, False, {"outputPath": "C:/tmp/cpl.csv"}),- ("fusion_set_design_rules", "Load manufacturing-grade DRC into the open board (JLCPCB 2/4-layer profiles).",- {"action": "optional str (apply|export|show)", "layers": "optional str (auto|2|4)"}, {"activeWorkspace": "str", "description": "str"}, 90, None, False, {"action": "apply", "layers": "2"}),+ ("fusion_set_design_rules", "Load a FAB's design rules (DRC) into the open board. The rule packs live on the wiki page adom/pcb-design-rules (JLCPCB, PCBWay, OSH Park, Seeed, Eurocircuits, Aisler, Elecrow, Advanced Circuits, Sierra, MacroFab, NextPCB, ALLPCB and Adom InstaPCB; one .edru per vendor/process/layer count, every number citing the vendor's own capabilities page): the bridge fetches the pack, stages it on the box, and the add-in runs `drc load`. {action:'list'} answers with every (fab, process, layers) row so you can offer the user a real choice; no fab means the bundled Adom InstaPCB rules; druPath loads a local .edru. Layers auto-detects from the open board. ALWAYS confirm with fusion_alerts afterwards: a refused file is an in-canvas toast, never a dialog.",+ {"fab": "optional str vendorSlug (jlcpcb|pcbway|osh-park|seeed-fusion|eurocircuits|aisler|elecrow|advanced-circuits-4pcb|sierra-circuits|macrofab|nextpcb|allpcb|adom-instapcb)", "process": "optional str (the vendor's tier: standard|advanced|proto|enig|hasl|...; default standard)", "layers": "optional str (auto|2|4|6)", "druPath": "optional str local .edru/.dru to load as-is", "action": "optional str (apply|list|export|show)"}, {"activeWorkspace": "str", "description": "str", "pack": "dict {fab, vendor, process, layers, file, record}"}, 90, None, False, {"fab": "jlcpcb", "layers": "2"}), ("fusion_detect_layers", "Detect 2-layer vs 4-layer board (ULP + CAM comparison).", {}, {"layerCount": "int", "copperLayers": "[int]"}, 90, None, False, {}), ("fusion_dismiss_blocking_dialogs", "Detect + classify (and optionally dismiss) blocking Fusion dialogs.",
install.sh+1−1@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.351"+DASH_VERSION="1.9.352" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.351",+ "version": "1.9.352", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {
page.json+11−27@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.351",+ "version": "1.9.352", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {@@ -11,6 +11,10 @@ "uninstall": "./uninstall.sh" }, "tags": [+ "ab",+ "bridge",+ "fusion",+ "desktop-app", "fusion360", "autodesk", "cad",@@ -75,7 +79,10 @@ "build a basic parts library", "add a 3d model to a component", "make a fusion 360 library with 3d",- "set up the fusion bridge"+ "set up the fusion bridge",+ "fusion dashboard",+ "fusion status page",+ "open the fusion launch page" ], "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).", "brief": "Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.",@@ -104,28 +111,5 @@ "label": "Part volume", "prompt": "How much material does each printed part in this assembly use?" }- ],- "agent_permissions": {- "allow_bash": [],- "allow": [- "adom-desktop-fusion-bridge read-only verbs (via the adom-bridge CLI): fusion_readiness, fusion_get_app_state, fusion_addin_status, fusion_check_dialogs - state queries; they change nothing.",- "adom-desktop-fusion-bridge read-only verbs: fusion_alerts (reads Fusion's own log for toasts), fusion_document_info, fusion_window_info, fusion_board_info, fusion_library_parts - inspect only.",- "adom-desktop-fusion-bridge read-only verbs: fusion_prove_result (a persisted run report), fusion_plan_library_import (report-only preflight), fusion_list_cloud_projects, fusion_describe.",- "adom-desktop-fusion-bridge evidence captures fusion_screenshot_fusion and fusion_capture_library_views read pixels and write local image files only."- ],- "soft_deny": [- "adom-desktop-fusion-bridge lifecycle verbs fusion_stop, fusion_kill, fusion_restart_for_addin close or kill the Fusion application - keep the human prompt.",- "adom-desktop-fusion-bridge fusion_install_fusion silently installs Autodesk Fusion on the user's machine.",- "adom-desktop-fusion-bridge fusion_cleanup_cloud_files deletes files from the user's Autodesk cloud hub.",- "adom-desktop-fusion-bridge managed-library verbs (fusion_managed_library_push/pull/create/link/unlink) publish to or overwrite the user's library.io libraries."- ],- "environment": [- "adom-desktop-fusion-bridge is the Fusion 360 bridge for Adom Bridge; its fusion_* verbs are invoked through the adom-bridge CLI and drive Autodesk Fusion on the user's own desktop.",- "adom-desktop-fusion-bridge desktop actions also pass Adom Bridge's own human-approval gate (caller identity, per-call reason, Activity Log) - a container approval is the first of two gates."- ]- },- "author": {- "name": "John Lauer",- "email": "[email protected]"- }-}+ ]+}
rust/Cargo.toml+2−2@@ -4,13 +4,13 @@ members = ["core", "lbr", "os-win", "os-mac", "bridge"] # One version for the whole bridge, in lockstep with BRIDGE_VERSION (tools/release.sh bumps both). [workspace.package]-version = "1.9.351"+version = "1.9.352" edition = "2021" license = "MIT" [workspace.dependencies] serde = { version = "1", features = ["derive"] }-serde_json = "1"+serde_json = { version = "1", features = ["preserve_order"] } thiserror = "1" tiny_http = "0.12" ureq = { version = "2", features = ["json"] }
rust/PORTING.md+8@@ -72,3 +72,11 @@ cargo test -p fusion-core # the frozen catalog must parse and be - **Guards run in the release**: `check-portable.sh` (core is OS-blind, no `todo!()` in a linked crate) and `check-naming.sh`, then the mingw cross-build, then the exe joins the zip at its root beside `server.py`.+- **Native verbs in the exe so far (2026-09-03, parity-checked against the Python bridge on+ the same box):** `fusion_alerts`, `fusion_action_ledger`, `fusion_verb_times`, `fusion_library_parts`, `fusion_preflight_lbr`, `fusion_aps_get` (GET, on the shared `~/.adom/fusion-aps` token store). The pattern+ for the next one: match the verb in `main.rs`, answer the same JSON shape (plus+ `shell: "rust"`), keep the Python hint text verbatim, and prove parity with a spare-port+ smoke before the release. The `.lbr` engine, bundle staging, and alert parser are ported+ with goldens; the APS client (token store + Data Management calls) is the next leaf, carrying the new permissions and member verbs.+- **Sidecar Python on a fresh box:** the exe asks ab for `runtimes {ensure:"python"}` (ab+ 2.1.14+) and waits, bounded, for `state: ready`. Older ab answers the snapshot only.
rust/bridge/Cargo.toml+2@@ -10,7 +10,9 @@ name = "adom-fusion-bridge" path = "src/main.rs" [dependencies]+chrono = { version = "0.4", default-features = false, features = ["clock"] } fusion-core = { path = "../core" }+fusion-lbr = { path = "../lbr" } serde_json.workspace = true tiny_http.workspace = true ureq.workspace = true
rust/bridge/src/main.rs+223−4@@ -67,10 +67,30 @@ impl Sidecar { // `runtimes`). The scan below is only for running without ab (a dev smoke test). if let Some(ab) = fusion_core::ab::Loopback::from_env() { use fusion_core::ab::AbClient;- if let Ok(v) = ab.call("runtimes", json!({}), &Caller::default(), 10) {- let py = &v["runtimes"]["python"];- if py["ready"].as_bool() == Some(true) {- if let Some(exe) = py["exe"].as_str() { let pb = std::path::PathBuf::from(exe); if pb.exists() { return Some(pb); } }+ // ab 2.1.14+ (adom-bridge issue #134): {ensure:"python"} kicks the same no-UAC+ // provisioning a kind:python spawn gets and returns at once; poll until ready.+ // A first-time download can take minutes, so the wait is bounded, not brief.+ let t0 = Instant::now();+ let mut asked = false;+ while t0.elapsed() < Duration::from_secs(600) {+ let args = if asked { json!({}) } else { json!({"ensure": "python"}) };+ asked = true;+ match ab.call("runtimes", args, &Caller::default(), 15) {+ Ok(v) => {+ let py = &v["runtimes"]["python"];+ if py["ready"].as_bool() == Some(true) || py["state"].as_str() == Some("ready") {+ if let Some(exe) = py["exe"].as_str() { let pb = std::path::PathBuf::from(exe); if pb.exists() { return Some(pb); } }+ break;+ }+ if py["state"].as_str().map(|s| s == "installing" || s == "downloading" || s == "provisioning").unwrap_or(false)+ || v["ensureKicked"].as_bool() == Some(true) {+ eprintln!("[fusion-bridge] waiting for ab to provision python ({} {}%)", py["phase"].as_str().unwrap_or("..."), py["pct"].as_u64().unwrap_or(0));+ std::thread::sleep(Duration::from_secs(3));+ continue;+ }+ break; // an ab without ensure (pre-2.1.14) answers the snapshot only+ }+ Err(_) => break, } } }@@ -211,6 +231,21 @@ fn main() { let parsed: Value = serde_json::from_str(&raw).unwrap_or(json!({})); let verb = parsed.get("command").and_then(|c| c.as_str()).unwrap_or("").to_string(); let reply = match verb.as_str() {+ // Native since Phase 2: Fusion's own alert toasts from its AppLogFile, the+ // same answer the Python bridge gives (parser golden-tested against it).+ "fusion_alerts" | "alerts" => {+ let since = parsed["args"]["sinceSec"].as_f64().unwrap_or(300.0).max(1.0);+ native_alerts(h.as_ref(), since)+ }+ // Native since Phase 2: the background-action ledger, a file the bridge appends.+ "fusion_action_ledger" | "action_ledger" => native_action_ledger(h.as_ref(), &parsed["args"]),+ // Native since Phase 2: the measured run-time report from the verb log.+ "fusion_verb_times" | "verb_times" => native_verb_times(h.as_ref(), parsed["args"]["limit"].as_u64().unwrap_or(40) as usize),+ // Native since Phase 2: offline library checks on the golden-tested .lbr engine.+ "fusion_library_parts" | "library_parts" => native_library_parts(&parsed["args"]),+ "fusion_preflight_lbr" | "preflight_lbr" => native_preflight_lbr(&parsed["args"]),+ // Native since Phase 2: the raw authenticated APS GET (never-charge guarded).+ "fusion_aps_get" | "aps_get" => native_aps_get(&parsed["args"]), "shell_info" => json!({"success": true, "shell": "rust", "bridgeVersion": VERSION, "contractFrozen": catalog.frozen, "_hint": "Rust plumbing shell; every catalogued verb is served, natively or via the Python sidecar."}),@@ -256,3 +291,187 @@ fn proxy(sidecar: &Arc<Mutex<Option<Sidecar>>>, raw: &str, caller: &Caller) -> V "_hint": "The Python sidecar beside adom-fusion-bridge.exe is not answering. Read ~/.adom/fusion-bridge/sidecar.log; if ab's portable python is missing (~/.adom/adom-runtimes/python-*), install any python bridge (kicad) once so ab provisions it."}), } }+++/// `fusion_alerts` served in-process. Fusion draws many errors as in-canvas CEF overlays with+/// no top-level window, so dialog enumeration cannot see them; its AppLogFile can. The log's+/// stamps are LOCAL time, so the cutoff is computed in the same convention (the Python bridge+/// compares `strptime(...).timestamp()` with `time.time()`, which is the same thing).+fn native_alerts(h: &dyn HostBoth, since_sec: f64) -> Value {+ let hint = "Fusion draws many errors as in-canvas CEF overlays with NO top-level window, so check_dialogs alone cannot see them; these come from Fusion's own AppLogFile 'New Alert message' entries. An empty list means Fusion raised no toast in the window - not that a command succeeded.";+ let Some(dir) = h.fusion_log_dir() else {+ return json!({"success": true, "alerts": [], "logFile": Value::Null, "shell": "rust",+ "diagnostics": {"why": "no AppLogFile*.log found under any base"}, "_hint": hint});+ };+ // newest AppLogFile*.log in that dir+ let mut best: Option<(std::time::SystemTime, std::path::PathBuf)> = None;+ if let Ok(rd) = std::fs::read_dir(&dir) {+ for e in rd.flatten() {+ let n = e.file_name().to_string_lossy().to_string();+ if n.starts_with("AppLogFile") && n.ends_with(".log") {+ let m = e.metadata().and_then(|m| m.modified()).unwrap_or(std::time::UNIX_EPOCH);+ if best.as_ref().map(|b| m > b.0).unwrap_or(true) { best = Some((m, e.path())); }+ }+ }+ }+ let Some((_, path)) = best else {+ return json!({"success": true, "alerts": [], "logFile": Value::Null, "shell": "rust",+ "diagnostics": {"baseFound": dir.to_string_lossy(), "why": "no AppLogFile*.log found under any base"}, "_hint": hint});+ };+ const TAIL: u64 = 400_000;+ let text = (|| -> std::io::Result<String> {+ use std::io::{Read, Seek, SeekFrom};+ let mut f = std::fs::File::open(&path)?;+ let size = f.metadata()?.len();+ let mut buf = Vec::new();+ if size > TAIL { f.seek(SeekFrom::Start(size - TAIL))?; }+ f.read_to_end(&mut buf)?;+ let mut s = String::from_utf8_lossy(&buf).to_string();+ if size > TAIL { if let Some(i) = s.find('\n') { s = s[i + 1..].to_string(); } }+ Ok(s)+ })().unwrap_or_default();+ let now_local_as_utc = chrono::Utc::now().timestamp() + chrono::Local::now().offset().local_minus_utc() as i64;+ let cutoff = now_local_as_utc - since_sec as i64;+ let alerts = fusion_core::alerts::parse_alert_lines(text.lines(), cutoff);+ json!({"success": true, "alerts": alerts, "logFile": path.to_string_lossy().replace('\\', "/"), "shell": "rust",+ "diagnostics": {"baseFound": dir.to_string_lossy()}, "_hint": hint})+}+++/// `fusion_action_ledger`: READ-ONLY view of `<state>/actions.jsonl`, every background action+/// the bridge took on the host app (update clicks, seat takeovers, dialog dismissals, doc+/// closes, WM_CLOSEs), so a caller on a relay is not blind to it. Same filters and shape as+/// the Python handler: {limit (default 50, max 500), sinceSeconds, action (substring)}.+fn native_action_ledger(h: &dyn HostBoth, args: &Value) -> Value {+ let p = h.bridge_state_dir().join("actions.jsonl");+ let hint = "Every background action the bridge takes on the host app - update clicks and deferrals, seat takeovers, dialog dismissals, doc closes, hang-gate kills, WM_CLOSEs (with window class+size) - appended live, survives respawns. Filter: {action:'wm_close', sinceSeconds:3600}.";+ if !p.is_file() {+ return json!({"success": true, "entries": [], "count": 0, "shell": "rust",+ "_hint": "Ledger empty: the bridge has taken no background actions on this box since 1.9.177 (or none since the file was last cleared)."});+ }+ let limit = args["limit"].as_u64().unwrap_or(50).clamp(1, 500) as usize;+ let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0);+ let cutoff = args["sinceSeconds"].as_f64().map(|s| now - s);+ let want = args["action"].as_str().unwrap_or("").to_lowercase();+ let text = match std::fs::read_to_string(&p) { Ok(t) => t, Err(e) => return json!({"success": false, "error": format!("ledger unreadable: {e}")}) };+ let mut out: Vec<Value> = Vec::new();+ for line in text.lines() {+ let Ok(e) = serde_json::from_str::<Value>(line) else { continue };+ if let Some(c) = cutoff { if e["epoch"].as_f64().unwrap_or(0.0) < c { continue; } }+ if !want.is_empty() && !e["action"].as_str().unwrap_or("").to_lowercase().contains(&want) { continue; }+ out.push(e);+ }+ let count = out.len();+ let tail: Vec<Value> = out.into_iter().rev().take(limit).collect::<Vec<_>>().into_iter().rev().collect();+ json!({"success": true, "entries": tail, "count": count, "shell": "rust", "_hint": hint})+}+++/// Python's `sorted(s)[int(round(q*(n-1)))]`: note `round` there is half-to-even.+fn pct(sorted: &[f64], q: f64) -> Option<f64> {+ if sorted.is_empty() { return None; }+ let n = sorted.len() as f64;+ let i = (q * (n - 1.0)).round_ties_even() as i64;+ Some(sorted[i.clamp(0, sorted.len() as i64 - 1) as usize])+}++/// `fusion_verb_times`: per-verb count / p50 / p90 / max / failures aggregated from+/// `<state>/verb-times.jsonl` (every run with its outcome), sorted by p90 descending,+/// the same numbers `timings.verb_report` produces.+fn native_verb_times(h: &dyn HostBoth, limit: usize) -> Value {+ let p = h.bridge_state_dir().join("verb-times.jsonl");+ let hint = "Aggregated from ~/.adom/fusion-bridge/verb-times.jsonl (every run, with its outcome). p90 is the useful column when hunting a slow surface; `failures` next to a healthy p50 usually means a verb that fails FAST, which is a different bug from one that is slow.";+ let mut runs: Vec<(String, Vec<f64>, u64)> = Vec::new(); // insertion order like a dict+ if let Ok(text) = std::fs::read_to_string(&p) {+ for line in text.lines() {+ let Ok(r) = serde_json::from_str::<Value>(line) else { continue };+ let verb = r["verb"].as_str().unwrap_or("?").to_string();+ let secs = r["seconds"].as_f64().unwrap_or(0.0);+ let ok = r["ok"].as_bool().unwrap_or(false);+ match runs.iter_mut().find(|(v, _, _)| *v == verb) {+ Some(e) => { e.1.push(secs); if !ok { e.2 += 1; } }+ None => runs.push((verb, vec![secs], if ok { 0 } else { 1 })),+ }+ }+ }+ let mut rows: Vec<(String, Value, f64)> = runs.into_iter().map(|(verb, mut secs, fail)| {+ secs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));+ let p90 = pct(&secs, 0.9);+ let row = json!({"count": secs.len(), "p50": pct(&secs, 0.5), "p90": p90,+ "max": secs.last().copied(), "failures": fail});+ (verb, row, p90.unwrap_or(0.0))+ }).collect();+ rows.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)); // stable, like Python's sort+ let mut verbs = serde_json::Map::new();+ for (verb, row, _) in rows.into_iter().take(limit) { verbs.insert(verb, row); }+ let count = verbs.len();+ json!({"success": true, "verbs": verbs, "count": count, "shell": "rust", "_hint": hint})+}+++fn lbr_path_arg(args: &Value, keys: &[&str]) -> String {+ keys.iter().find_map(|k| args[*k].as_str().filter(|v| !v.is_empty())).unwrap_or("").replace('\\', "/")+}++/// `fusion_library_parts`: every ORDERABLE variant in a .lbr with its part numbers, plus the+/// EE-grade warnings. Offline, no Fusion. Same shape as the Python handler.+fn native_library_parts(args: &Value) -> Value {+ let path = lbr_path_arg(args, &["libraryPath", "lbrPath", "filePath", "path"]);+ if path.is_empty() {+ return json!({"success": false, "errorCode": "no_path_argument", "error": "No library path given; the file was never examined.",+ "acceptedArgs": ["libraryPath", "lbrPath", "filePath", "path"], "shell": "rust"});+ }+ let xml = match std::fs::read_to_string(&path) {+ Ok(t) => t,+ Err(e) => return json!({"success": false, "errorCode": "library_unreadable", "error": e.to_string(), "libraryPath": path, "shell": "rust"}),+ };+ let md = match fusion_lbr::library_part_metadata(&xml) {+ Ok(m) => m,+ Err(e) => return json!({"success": false, "errorCode": "library_unreadable", "error": e.to_string(), "libraryPath": path, "shell": "rust"}),+ };+ let lint = fusion_lbr::lint_part_metadata(&xml);+ let t = &md.totals;+ let tail = if lint.ok { "No metadata warnings.".to_string() } else {+ format!("{} warning(s) in `warnings` - these do NOT stop the library opening, but an EE cannot order from it as-is.", lint.warnings.len()) };+ json!({"success": true, "libraryPath": path, "devicesets": md.devicesets, "totals": t,+ "metadataOk": lint.ok, "warnings": lint.warnings,+ "attributeNames": ["MPN", "MANUFACTURER", "MOUSER_PN", "DIGIKEY_PN", "LCSC_PN"], "shell": "rust",+ "_hint": format!("{} deviceset(s), {} orderable variant(s); {} carry an MPN and {} a distributor PN. {} The full orderable identity is deviceset + device + technology (e.g. TJA1050 + T + -AUTO), because EAGLE puts package variants on <device> and grade/temperature variants on <technology> - which is how two variants share geometry and remain different parts.",+ t.devicesets, t.variants, t.with_mpn, t.with_distributor, tail)})+}++/// `fusion_preflight_lbr`: will this .lbr open cleanly in Fusion? Offline, instant.+fn native_preflight_lbr(args: &Value) -> Value {+ let path = lbr_path_arg(args, &["libraryPath", "filePath"]);+ if path.is_empty() { return json!({"success": false, "error": "libraryPath required.", "shell": "rust"}); }+ let pf = fusion_lbr::preflight_lbr(std::path::Path::new(&path));+ let ok = pf.ok;+ json!({"success": ok, "preflight": pf, "libraryPath": path, "errorCode": if ok { Value::Null } else { json!("lbr_preflight_failed") }, "shell": "rust",+ "_hint": if ok { "Every check green - Fusion will open this library cleanly." } else {+ "Fix the named check(s) BEFORE any Fusion verb touches this file - each one maps to a real Fusion failure (blocking Error modal, silent No Components, Create-new prompt, or dead wiring). Re-stage and hash-compare if size-sane or xml-parses failed: transfer corruption is the usual cause." }})+}+++/// `fusion_aps_get`: raw authenticated GET against APS on the shared token store, with the+/// free-endpoint allowlist and the monthly cap enforced. Non-GET methods stay on the sidecar.+fn native_aps_get(args: &Value) -> Value {+ use fusion_core::aps::{aps_get, ApsError, ApsStore};+ let Some(path) = args["path"].as_str().filter(|p| !p.is_empty()) else { return json!({"success": false, "error": "Missing path.", "shell": "rust"}) };+ let method = args["method"].as_str().unwrap_or("GET").to_uppercase();+ if method != "GET" { return proxy_unavailable("fusion_aps_get with a non-GET method is still served by the sidecar"); }+ let q: Vec<(String, String)> = args["query"].as_object().map(|m| m.iter().map(|(k, v)| (k.clone(), match v { Value::String(s) => s.clone(), other => other.to_string() })).collect()).unwrap_or_default();+ let qref: Vec<(&str, &str)> = q.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();+ let store = ApsStore::default_location();+ match aps_get(&store, path, &qref) {+ Ok((status, body)) => json!({"success": (200..300).contains(&status), "output": format!("HTTP {status}"), "data": {"httpStatus": status, "body": body}, "shell": "rust"}),+ Err(ApsError::NotSignedIn) => json!({"success": false, "output": "HTTP None", "data": {"httpStatus": Value::Null, "body": {"error": "not_signed_in"}}, "shell": "rust"}),+ Err(ApsError::BlockedNonFree(p)) => json!({"success": false, "output": "HTTP 0", "data": {"httpStatus": 0, "body": {"error": "blocked_non_free_endpoint", "path": p,+ "_hint": "Refused — the bridge only calls free Data Management endpoints (/project/v1/, /data/v1/) so you are never charged."}}, "shell": "rust"}),+ Err(ApsError::UsageCap) => json!({"success": false, "output": "HTTP 0", "data": {"httpStatus": 0, "body": {"error": "usage_cap_reached", "cap": fusion_core::aps::MONTHLY_CALL_CAP}}, "shell": "rust"}),+ Err(ApsError::Http(e)) => json!({"success": false, "output": "HTTP 0", "data": {"httpStatus": 0, "body": {"error": e}}, "shell": "rust"}),+ }+}++fn proxy_unavailable(why: &str) -> Value {+ json!({"success": false, "errorCode": "not_native_yet", "error": why, "shell": "rust"})+}
rust/core/src/aps.rsadded+208@@ -0,0 +1,208 @@+//! The APS (Autodesk Platform Services) client: the bridge's ONLY cloud discovery path.+//! Never a folder walk inside Fusion (that crashed Fusion and was banned for the AI).+//!+//! Ported from `aps.py` with its two guarantees intact:+//! 1. only free Data Management / OSS endpoints are ever called (`FREE_PATH_PREFIXES`);+//! 2. a monthly hard cap on calls, fail-closed, so a user is never charged.+//! The Manufacturing Data Model API (member and permission administration) is NOT on the+//! allowlist: from 2026-08-17 it is metered past a subscription's included quota, so a verb+//! that needs it must ask for explicit opt-in rather than ride these helpers.+//!+//! Token state lives where the Python bridge keeps it (`~/.adom/fusion-aps/`), so the two+//! shells share one sign-in during the port: `config.json` {client_id}, `tokens.json`+//! {access_token, refresh_token, expires_at}, `usage.json` {month, calls}.++use serde_json::{json, Value};+use std::path::PathBuf;+use std::time::{Duration, SystemTime, UNIX_EPOCH};++pub const APS_BASE: &str = "https://developer.api.autodesk.com";+pub const TOKEN_URL: &str = "https://developer.api.autodesk.com/authentication/v2/token";+pub const FREE_PATH_PREFIXES: [&str; 4] = ["/project/v1/", "/data/v1/", "/oss/v2/", "/123D-Circuits/"];+/// Conservative; well under the Data Management free quota; fail-closed.+pub const MONTHLY_CALL_CAP: u64 = 50_000;++#[derive(Debug, thiserror::Error)]+pub enum ApsError {+ #[error("not_signed_in")]+ NotSignedIn,+ #[error("blocked_non_free_endpoint: {0}")]+ BlockedNonFree(String),+ #[error("usage_cap_reached")]+ UsageCap,+ #[error("http: {0}")]+ Http(String),+}++fn now_secs() -> u64 { SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0) }++/// `YYYY-MM` in UTC, the usage bucket (auto-resets each month).+pub fn usage_month_at(secs: u64) -> String {+ // days from civil, inverted (Howard Hinnant)+ let days = (secs / 86_400) as i64;+ let z = days + 719_468;+ let era = z.div_euclid(146_097);+ let doe = z - era * 146_097;+ let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;+ let y = yoe + era * 400;+ let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);+ let mp = (5 * doy + 2) / 153;+ let m = if mp < 10 { mp + 3 } else { mp - 9 };+ let y = if m <= 2 { y + 1 } else { y };+ format!("{y:04}-{m:02}")+}++/// Where the Python bridge keeps APS state; the Rust shell reads and writes the same files.+pub struct ApsStore { pub dir: PathBuf }++impl ApsStore {+ pub fn default_location() -> ApsStore {+ let home = std::env::var("USERPROFILE").or_else(|_| std::env::var("HOME")).unwrap_or_default();+ ApsStore { dir: PathBuf::from(home).join(".adom").join("fusion-aps") }+ }+ fn read(&self, name: &str) -> Value {+ std::fs::read_to_string(self.dir.join(name)).ok().and_then(|t| serde_json::from_str(&t).ok()).unwrap_or(json!({}))+ }+ fn write(&self, name: &str, v: &Value) {+ let _ = std::fs::create_dir_all(&self.dir);+ let _ = std::fs::write(self.dir.join(name), serde_json::to_string_pretty(v).unwrap_or_default());+ }+ pub fn client_id(&self) -> String { self.read("config.json")["client_id"].as_str().unwrap_or("").to_string() }+ pub fn tokens(&self) -> Value { self.read("tokens.json") }+ pub fn save_tokens(&self, t: &Value) { self.write("tokens.json", t) }++ /// {month, calls} for THIS month (a stale month reads as zero, like the Python side).+ pub fn usage(&self) -> (String, u64) {+ let u = self.read("usage.json");+ let month = usage_month_at(now_secs());+ if u["month"].as_str() == Some(month.as_str()) { (month, u["calls"].as_u64().unwrap_or(0)) } else { (month, 0) }+ }+ pub fn bump_usage(&self, n: u64) -> u64 {+ let (month, calls) = self.usage();+ let calls = calls + n;+ self.write("usage.json", &json!({"month": month, "calls": calls}));+ calls+ }+ pub fn cap_remaining(&self) -> u64 { MONTHLY_CALL_CAP.saturating_sub(self.usage().1) }++ /// A live access token, refreshed through the refresh token when expired. Refresh sends+ /// NO scope on purpose: a refresh may only narrow scope, so passing today's wider scopes+ /// would make Autodesk reject a token originally granted read-only.+ pub fn valid_access_token(&self) -> Option<String> {+ let mut t = self.tokens();+ if t["access_token"].as_str().map(|s| s.is_empty()).unwrap_or(true) { return None; }+ if t["expires_at"].as_u64().unwrap_or(0) <= now_secs() {+ if !self.refresh() { return None; }+ t = self.tokens();+ }+ t["access_token"].as_str().map(str::to_string)+ }+ fn refresh(&self) -> bool {+ let t = self.tokens();+ let Some(rt) = t["refresh_token"].as_str().filter(|s| !s.is_empty()).map(str::to_string) else { return false };+ let form = format!("grant_type=refresh_token&refresh_token={}&client_id={}", urlencode(&rt), urlencode(&self.client_id()));+ let resp = ureq::post(TOKEN_URL).timeout(Duration::from_secs(30))+ .set("Content-Type", "application/x-www-form-urlencoded").send_string(&form);+ let Ok(resp) = resp else { return false };+ let Ok(mut body) = resp.into_json::<Value>() else { return false };+ if body["access_token"].as_str().map(|s| !s.is_empty()).unwrap_or(false) {+ let now = now_secs();+ body["obtained_at"] = json!(now);+ body["expires_at"] = json!(now + body["expires_in"].as_u64().unwrap_or(3600) - 60);+ if body.get("refresh_token").is_none() { body["refresh_token"] = json!(rt); }+ self.save_tokens(&body);+ return true;+ }+ false+ }+}++/// Percent-encode a form value (RFC 3986 unreserved set kept).+pub fn urlencode(s: &str) -> String {+ let mut out = String::new();+ for b in s.bytes() {+ match b {+ b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => out.push(b as char),+ _ => out.push_str(&format!("%{:02X}", b)),+ }+ }+ out+}++/// The path part of an absolute-from-host path or a full URL.+pub fn only_path(path: &str) -> String {+ if let Some(rest) = path.strip_prefix("https://").or_else(|| path.strip_prefix("http://")) {+ match rest.find('/') { Some(i) => rest[i..].split('?').next().unwrap_or("/").to_string(), None => "/".into() }+ } else { path.split('?').next().unwrap_or("/").to_string() }+}++/// Guarantee 1: only ever hit free endpoints.+pub fn is_free_endpoint(path: &str) -> bool { let p = only_path(path); FREE_PATH_PREFIXES.iter().any(|x| p.starts_with(x)) }++/// Authenticated GET with both guarantees. Returns (status, body) and never raises on an+/// HTTP error status; the body is the JSON or `{"text": ...}` when not JSON.+pub fn aps_get(store: &ApsStore, path: &str, query: &[(&str, &str)]) -> Result<(u16, Value), ApsError> {+ aps_call(store, "GET", path, query, None)+}++pub fn aps_call(store: &ApsStore, method: &str, path: &str, query: &[(&str, &str)], body: Option<&Value>) -> Result<(u16, Value), ApsError> {+ let at = store.valid_access_token().ok_or(ApsError::NotSignedIn)?;+ if !is_free_endpoint(path) { return Err(ApsError::BlockedNonFree(only_path(path))); }+ if store.cap_remaining() == 0 { return Err(ApsError::UsageCap); }+ let mut url = if path.starts_with("http") { path.to_string() } else { format!("{APS_BASE}{path}") };+ if !query.is_empty() {+ let qs: Vec<String> = query.iter().map(|(k, v)| format!("{}={}", urlencode(k), urlencode(v))).collect();+ url.push(if url.contains('?') { '&' } else { '?' }); url.push_str(&qs.join("&"));+ }+ store.bump_usage(1);+ let agent = ureq::AgentBuilder::new().timeout(Duration::from_secs(120)).build();+ let req = agent.request(method, &url).set("Authorization", &format!("Bearer {at}"));+ let resp = match body {+ Some(b) => req.set("Content-Type", "application/vnd.api+json").send_string(&b.to_string()),+ None => req.call(),+ };+ let (status, text) = match resp {+ Ok(r) => { let s = r.status(); (s, r.into_string().unwrap_or_default()) }+ Err(ureq::Error::Status(s, r)) => (s, r.into_string().unwrap_or_default()),+ Err(e) => return Err(ApsError::Http(e.to_string())),+ };+ let parsed = serde_json::from_str::<Value>(&text).unwrap_or_else(|_| json!({"text": text}));+ Ok((status, parsed))+}++#[cfg(test)]+mod tests {+ use super::*;+ #[test]+ fn free_endpoint_guard_matches_python() {+ assert!(is_free_endpoint("/project/v1/hubs"));+ assert!(is_free_endpoint("https://developer.api.autodesk.com/data/v1/projects/x/folders/y/contents?page=2"));+ assert!(!is_free_endpoint("/mfg/graphql"));+ assert!(!is_free_endpoint("https://developer.api.autodesk.com/construction/admin/v1/projects"));+ }+ #[test]+ fn usage_month_is_utc_year_month() {+ assert_eq!(usage_month_at(0), "1970-01");+ assert_eq!(usage_month_at(20_699 * 86_400 + 3600), "2026-09");+ assert_eq!(usage_month_at(951_868_800), "2000-03");+ }+ #[test]+ fn usage_counter_resets_on_a_new_month_and_caps() {+ let dir = std::env::temp_dir().join(format!("fusion-aps-test-{}", std::process::id()));+ let _ = std::fs::remove_dir_all(&dir);+ let s = ApsStore { dir: dir.clone() };+ s.write("usage.json", &json!({"month": "1999-01", "calls": 49_999}));+ assert_eq!(s.usage().1, 0, "a stale month reads as zero");+ assert_eq!(s.bump_usage(1), 1);+ assert_eq!(s.cap_remaining(), MONTHLY_CALL_CAP - 1);+ s.write("usage.json", &json!({"month": usage_month_at(now_secs()), "calls": MONTHLY_CALL_CAP}));+ assert_eq!(s.cap_remaining(), 0);+ assert!(matches!(aps_get(&s, "/project/v1/hubs", &[]), Err(ApsError::NotSignedIn)), "no token: refused before any network");+ s.write("tokens.json", &json!({"access_token": "x", "expires_at": now_secs() + 3600}));+ assert!(matches!(aps_get(&s, "/mfg/graphql", &[]), Err(ApsError::BlockedNonFree(_))));+ assert!(matches!(aps_get(&s, "/project/v1/hubs", &[]), Err(ApsError::UsageCap)));+ let _ = std::fs::remove_dir_all(&dir);+ }+ #[test]+ fn urlencode_is_form_safe() { assert_eq!(urlencode("a b/c?d=e&f"), "a%20b%2Fc%3Fd%3De%26f"); assert_eq!(urlencode("abc-_.~"), "abc-_.~"); }+}
rust/core/src/lib.rs+1@@ -10,6 +10,7 @@ pub mod contract; pub mod host; pub mod alerts;+pub mod aps; pub mod ab; pub use host::{HostPaths, HostProcess};
rust/lbr/Cargo.toml+1@@ -7,6 +7,7 @@ description = "The EAGLE/Fusion .lbr engine: artifact names, 3D bindings, merges [dependencies] thiserror.workspace = true+serde_json.workspace = true serde = { workspace = true } roxmltree = "0.20" regex = "1"
rust/lbr/src/lib.rs+280@@ -573,3 +573,283 @@ pub fn place_library(lbr_path: &std::path::Path, models: &[String], dest_dir: &s } Ok((new_lbr.to_string_lossy().replace('\\', "/"), map)) }++// ───────────────────────── preflight + lints (server.py) ─────────────────────────++/// Python's `repr(float)` for the values these lints print (shortest round-trip, with a+/// trailing `.0` on integral values), so warning texts match the Python bridge exactly.+pub fn py_float(v: f64) -> String {+ if v.is_finite() && v.fract() == 0.0 && v.abs() < 1e16 { format!("{:.1}", v) } else { format!("{}", v) }+}++/// Python's `repr(str)`: single quotes unless the text has a single quote and no double.+pub fn py_repr_str(s: &str) -> String { py_repr(s) }++fn round3(v: f64) -> f64 { (v * 1000.0).round() / 1000.0 }++#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]+pub struct Check { pub check: String, pub ok: bool, pub detail: String }++#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]+pub struct ConnectCheck {+ pub ok: bool,+ #[serde(default)] pub connects: usize,+ #[serde(default, rename = "badPinRefs")] pub bad_pin_refs: Vec<(Option<String>, Option<String>)>,+ #[serde(default, rename = "badPadRefs")] pub bad_pad_refs: Vec<(Option<String>, Option<String>)>,+ #[serde(default, rename = "unconnectedPads")] pub unconnected_pads: Vec<String>,+ pub why: Option<String>,+}++/// Referential integrity of the pin→pad mapping (the fifth artifact). A connect's `pad` is a+/// SPACE-SEPARATED list (QFN thermal vias, BGA balls), so every name in it is checked.+pub fn check_connects(xml: &str) -> ConnectCheck {+ let opts = roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() };+ let Ok(doc) = roxmltree::Document::parse_with_options(xml, opts) else {+ return ConnectCheck { ok: false, why: Some("library unreadable".into()), ..Default::default() };+ };+ let el = |n: roxmltree::Node, t: &str| n.is_element() && n.tag_name().name() == t;+ let root = doc.root_element();+ let named = |t: &str| -> std::collections::BTreeSet<String> { root.descendants().filter(|n| el(*n, t)).filter_map(|n| n.attribute("name").map(str::to_string)).collect() };+ let pins = named("pin");+ let mut pads = named("smd"); pads.extend(named("pad"));+ let connects: Vec<(Option<String>, Option<String>)> = root.descendants().filter(|n| el(*n, "connect"))+ .map(|c| (c.attribute("pin").map(str::to_string), c.attribute("pad").map(str::to_string))).collect();+ let bad_pin: Vec<_> = connects.iter().filter(|c| !c.0.as_ref().map(|p| pins.contains(p)).unwrap_or(false)).cloned().collect();+ let bad_pad: Vec<_> = connects.iter().filter(|c| {+ let names: Vec<&str> = c.1.as_deref().unwrap_or("").split_whitespace().collect();+ names.is_empty() || names.iter().any(|p| !pads.contains(*p))+ }).cloned().collect();+ let connected: std::collections::BTreeSet<String> = connects.iter().flat_map(|c| c.1.as_deref().unwrap_or("").split_whitespace().map(str::to_string).collect::<Vec<_>>()).collect();+ let unconnected: Vec<String> = pads.iter().filter(|p| !connected.contains(*p)).cloned().collect();+ let ok = !connects.is_empty() && bad_pin.is_empty() && bad_pad.is_empty();+ let why = if ok { None } else if connects.is_empty() { Some("no connects at all".into()) }+ else { Some(format!("connects reference missing {}", if !bad_pin.is_empty() { "pins" } else { "pads" })) };+ ConnectCheck { ok, connects: connects.len(), bad_pin_refs: bad_pin.into_iter().take(8).collect(),+ bad_pad_refs: bad_pad.into_iter().take(8).collect(), unconnected_pads: unconnected.into_iter().take(12).collect(), why }+}++/// One EE-grade warning about a library (metadata or geometry); the same keys Python emits.+pub type Warning = serde_json::Map<String, serde_json::Value>;++fn warn(pairs: &[(&str, serde_json::Value)]) -> Warning {+ let mut m = Warning::new();+ for (k, v) in pairs { m.insert(k.to_string(), v.clone()); }+ m+}++/// Shape-aware geometry checks: stacked pins, pad copper overlap or < 0.2 mm clearance,+/// generic class names / descriptions. Same warnings as `_lint_library_geometry`.+pub fn lint_library_geometry(xml: &str) -> Vec<Warning> {+ use serde_json::json;+ let mut out = Vec::new();+ let Ok(root) = Element::parse(xml) else { return out };+ let mut probe = root.clone();+ let Some(lib) = probe.find_deep_mut("library") else { return out };+ let lib = lib.clone();+ let f = |e: &Element, k: &str| -> f64 { e.attr(k).and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.0) };+ // 1. symbol pins at identical coordinates+ if let Some(symbols) = lib.find_child("symbols") {+ for sym in symbols.children.iter().filter(|c| c.tag == "symbol") {+ let mut seen: Vec<((f64, f64), String)> = Vec::new();+ for pin in sym.children.iter().filter(|c| c.tag == "pin") {+ let key = (round3(f(pin, "x")), round3(f(pin, "y")));+ let name = pin.attr("name").unwrap_or("").to_string();+ if let Some((_, prev)) = seen.iter().find(|(k, _)| *k == key) {+ out.push(warn(&[("code", json!("symbol_pins_stacked")), ("symbol", json!(sym.attr("name"))),+ ("detail", json!(format!("pins {} and {} sit at the same point ({}, {}); one hides the other", py_repr(prev), py_repr(&name), py_float(key.0), py_float(key.1))))]));+ }+ seen.retain(|(k, _)| *k != key); seen.push((key, name));+ }+ let has = |t: &str| sym.children.iter().any(|c| c.tag == t);+ let pins = sym.children.iter().filter(|c| c.tag == "pin").count();+ if !has("wire") && !has("rectangle") && !has("circle") && !has("polygon") && pins <= 4 {+ out.push(warn(&[("code", json!("symbol_no_body")), ("symbol", json!(sym.attr("name"))),+ ("detail", json!("symbol has pins but no body graphics (the converter dropped them)"))]));+ }+ }+ }+ // 2. pad copper overlap / clearance+ let bbox = |e: &Element| -> (f64, f64, f64, f64) {+ let (x, y) = (f(e, "x"), f(e, "y"));+ let (mut dx, mut dy) = if e.tag == "smd" { (f(e, "dx"), f(e, "dy")) } else {+ let mut dia = f(e, "diameter"); if dia == 0.0 { dia = f(e, "drill") * 1.5; }+ let shape = e.attr("shape").unwrap_or("round").to_lowercase();+ if shape == "long" { (dia * 2.0, dia) } else { (dia, dia) }+ };+ let rot = e.attr("rot").unwrap_or("");+ if rot.contains("R90") || rot.contains("R270") { std::mem::swap(&mut dx, &mut dy); }+ (x - dx / 2.0, y - dy / 2.0, x + dx / 2.0, y + dy / 2.0)+ };+ if let Some(packages) = lib.find_child("packages") {+ for pkg in packages.children.iter().filter(|c| c.tag == "package") {+ let pads: Vec<&Element> = pkg.children.iter().filter(|c| c.tag == "smd").chain(pkg.children.iter().filter(|c| c.tag == "pad")).collect();+ let boxes: Vec<(String, (f64, f64, f64, f64))> = pads.iter().map(|p| (p.attr("name").unwrap_or("").to_string(), bbox(p))).collect();+ for i in 0..boxes.len() {+ for j in i + 1..boxes.len() {+ let (na, a) = &boxes[i]; let (nb, b) = &boxes[j];+ let gap_x = (a.0 - b.2).max(b.0 - a.2);+ let gap_y = (a.1 - b.3).max(b.1 - a.3);+ let gap = gap_x.max(gap_y);+ if gap < 0.0 {+ out.push(warn(&[("code", json!("pad_overlap")), ("package", json!(pkg.attr("name"))),+ ("detail", json!(format!("pads {} and {} overlap (copper intersects): a short on the board", py_repr(na), py_repr(nb))))]));+ } else if gap < 0.2 {+ out.push(warn(&[("code", json!("pad_clearance")), ("package", json!(pkg.attr("name"))),+ ("detail", json!(format!("pads {} and {} are only {:.2} mm apart", py_repr(na), py_repr(nb), gap)))]));+ }+ }+ }+ }+ }+ // 3. generic names / descriptions+ let gen_name = regex::Regex::new(r"(?i)^(L|R|C|D|LED|Q|Y|X|F|FB|J|Conn_\d+x\d+.*|Crystal(_\w+)?|Resistor|Capacitor|Inductor)$").unwrap();+ let gen_desc = regex::Regex::new(r"(?i)^(inductor|resistor|capacitor|unpolarized capacitor|polarized capacitor|light emitting diode|led|diode|generic connector.*|four pin crystal.*|crystal)$").unwrap();+ if let Some(devicesets) = lib.find_child("devicesets") {+ for ds in devicesets.children.iter().filter(|c| c.tag == "deviceset") {+ let n = ds.attr("name").unwrap_or("").to_string();+ let d = ds.find_child("description").map(|x| x.text.trim().to_string()).unwrap_or_default();+ if gen_name.is_match(&n) {+ out.push(warn(&[("code", json!("generic_deviceset_name")), ("deviceset", json!(n)),+ ("detail", json!(format!("deviceset is named {}, a class name, not a part; the MPN belongs here", py_repr(&n))))]));+ }+ if !d.is_empty() && gen_desc.is_match(&d) {+ out.push(warn(&[("code", json!("generic_description")), ("deviceset", json!(n)),+ ("detail", json!(format!("description {} says the class, not the part (value, package, maker)", py_repr(&d))))]));+ }+ }+ }+ out+}++#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]+pub struct MetadataLint {+ pub ok: bool,+ #[serde(default)] pub warnings: Vec<Warning>,+ #[serde(default, skip_serializing_if = "Option::is_none")] pub totals: Option<Totals>,+ #[serde(default, skip_serializing_if = "Option::is_none")] pub error: Option<String>,+}++/// EE-grade warnings about part metadata (never blocks opening), then geometry. `ok` means+/// "fit to ORDER from", a different question from "will open". Same as `_lint_part_metadata`.+pub fn lint_part_metadata(xml: &str) -> MetadataLint {+ use serde_json::json;+ let md = match library_part_metadata(xml) { Ok(m) => m, Err(e) => return MetadataLint { ok: false, warnings: vec![], totals: None, error: Some(e.to_string()) } };+ let mut warns = Vec::new();+ for ds in &md.devicesets {+ if ds.description.is_empty() {+ warns.push(warn(&[("code", json!("deviceset_no_description")), ("deviceset", json!(ds.name)),+ ("detail", json!("No <description>. Fusion's Content Manager shows this to whoever places the part; without it they are picking on a symbol outline alone."))]));+ }+ for v in &ds.variants {+ if !v.missing.is_empty() {+ warns.push(warn(&[("code", json!("variant_missing_part_numbers")), ("deviceset", json!(ds.name)), ("variant", json!(v.full_name)),+ ("missing", json!(v.missing)),+ ("detail", json!(format!("Missing {}. This bridge's own BOM export reads these exact attribute names, so the BOM comes out blank for this part.", v.missing.join(", "))))]));+ }+ }+ if ds.variants.len() > 1 {+ let mpns: Vec<Option<&String>> = ds.variants.iter().map(|v| v.attributes.get("MPN").filter(|s| !s.is_empty())).collect();+ let named: Vec<&String> = mpns.iter().flatten().cloned().collect();+ if named.len() < ds.variants.len() {+ warns.push(warn(&[("code", json!("variants_indistinguishable")), ("deviceset", json!(ds.name)),+ ("detail", json!(format!("{} variants but only {} carry an MPN. Variants that share a symbol, footprint and 3D body are distinguishable ONLY by metadata - an automotive grade part and a baseline one look identical here.", ds.variants.len(), named.len())))]));+ } else {+ let mut uniq = named.clone(); uniq.sort(); uniq.dedup();+ if uniq.len() < named.len() {+ let mut dups: Vec<&String> = named.iter().filter(|m| named.iter().filter(|n| n == m).count() > 1).cloned().collect();+ dups.sort(); dups.dedup();+ let list: Vec<String> = dups.into_iter().take(3).cloned().collect();+ warns.push(warn(&[("code", json!("duplicate_variant_mpn")), ("deviceset", json!(ds.name)),+ ("detail", json!(format!("Two or more variants share the same MPN ({}), so they are not actually different parts - or one MPN is wrong.", list.join(", "))))]));+ }+ }+ }+ }+ warns.extend(lint_library_geometry(xml));+ MetadataLint { ok: warns.is_empty(), warnings: warns, totals: Some(md.totals), error: None }+}++#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]+pub struct Preflight {+ pub ok: bool,+ pub checks: Vec<Check>,+ pub errors: Vec<String>,+ #[serde(default, skip_serializing_if = "Option::is_none", rename = "metadataOk")] pub metadata_ok: Option<bool>,+ #[serde(default, skip_serializing_if = "Option::is_none", rename = "metadataWarnings")] pub metadata_warnings: Option<Vec<Warning>>,+ #[serde(default, skip_serializing_if = "Option::is_none", rename = "partTotals")] pub part_totals: Option<Totals>,+ #[serde(default, skip_serializing_if = "Option::is_none", rename = "_metadataHint")] pub metadata_hint: Option<String>,+}++const EAGLE_ALIGNS: [&str; 9] = ["bottom-left", "bottom-center", "bottom-right", "center-left", "center", "center-right", "top-left", "top-center", "top-right"];++/// Will Fusion open this .lbr? Every rejection Fusion has actually produced, as a named+/// offline check: truncated file, empty packages, dead connects, absent devicesets, duplicate+/// pad names. `ok` keeps meaning "will open"; ordering fitness rides separately.+pub fn preflight_lbr(path: &std::path::Path) -> Preflight {+ let mut checks: Vec<Check> = Vec::new();+ let mut c = |name: &str, ok: bool, detail: String| -> bool { checks.push(Check { check: name.into(), ok, detail }); ok };+ let sz = match std::fs::metadata(path) {+ Ok(m) if m.is_file() => m.len(),+ Ok(_) => { c("file-readable", false, format!("[Errno 21] Is a directory: {}", py_repr(&path.to_string_lossy()))); return finish(checks, None); }+ Err(e) => { c("file-readable", false, format!("[Errno {}] {}: {}", e.raw_os_error().unwrap_or(2), if e.kind() == std::io::ErrorKind::NotFound { "No such file or directory" } else { "cannot read" }, py_repr(&path.to_string_lossy()))); return finish(checks, None); }+ };+ c("file-readable", true, format!("{sz} bytes"));+ c("size-sane", sz >= 200, format!("{sz} bytes - a real EAGLE library is never this small (truncated transfer?)"));+ let mut xml: Option<String> = None;+ if sz >= 200 {+ let text = std::fs::read_to_string(path).unwrap_or_default();+ let opts = roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() };+ match roxmltree::Document::parse_with_options(&text, opts) {+ Ok(_) => { c("xml-parses", true, String::new()); xml = Some(text); }+ Err(e) => { c("xml-parses", false, e.to_string().chars().take(160).collect()); }+ }+ }+ if let Some(text) = &xml {+ let opts = roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() };+ let doc = roxmltree::Document::parse_with_options(text, opts).unwrap();+ let root = doc.root_element();+ let el = |n: roxmltree::Node, t: &str| n.is_element() && n.tag_name().name() == t;+ c("root-is-eagle", root.tag_name().name() == "eagle", format!("root is <{}>", root.tag_name().name()));+ let count = |t: &str| root.descendants().filter(|n| el(*n, t)).count();+ let (pkgs, syms, devs) = (count("package"), count("symbol"), count("deviceset"));+ c("packages-nonempty", pkgs > 0, if pkgs > 0 { format!("{pkgs} package(s)") } else { "EMPTY <packages> - Fusion opens this to a silent 'No Components' library".into() });+ c("symbols-present", syms > 0, format!("{syms} symbol(s)"));+ let bad_align: Vec<String> = root.descendants().filter(|n| el(*n, "text")).filter_map(|t| t.attribute("align")).filter(|a| !EAGLE_ALIGNS.contains(a)).map(str::to_string).collect();+ let mut uniq = bad_align.clone(); uniq.sort(); uniq.dedup();+ c("text-align-valid", true, if bad_align.is_empty() { "all <text> align values are EAGLE values".into() } else {+ format!("{} <text> element(s) carry a non-EAGLE align value ([{}]); Fusion will list a warning for each on open. Fix upstream (adom-lbr group captions).", bad_align.len(), uniq.iter().map(|s| py_repr(s)).collect::<Vec<_>>().join(", ")) });+ c("devicesets-present", devs > 0, if devs > 0 { format!("{devs} deviceset(s)") } else { "no <deviceset> - EDIT <name>.dev will raise Fusion's Create-new prompt".into() });+ if pkgs > 0 && syms > 0 {+ let conn = check_connects(text);+ c("connects-resolve", conn.ok, if conn.ok { format!("{} connect(s), all resolving", conn.connects) } else { conn.why.clone().unwrap_or_else(|| "invalid".into()).chars().take(160).collect() });+ }+ let mut dups: Vec<String> = Vec::new();+ for pkg in root.descendants().filter(|n| el(*n, "package")) {+ let mut seen: Vec<(String, usize)> = Vec::new();+ let names = pkg.descendants().filter(|n| el(*n, "smd")).chain(pkg.descendants().filter(|n| el(*n, "pad"))).filter_map(|p| p.attribute("name").map(str::to_string));+ for nm in names { match seen.iter_mut().find(|(k, _)| *k == nm) { Some(e) => e.1 += 1, None => seen.push((nm, 1)) } }+ dups.extend(seen.into_iter().filter(|(_, n)| *n > 1).map(|(n, cnt)| format!("{}/{} x{}", pkg.attribute("name").unwrap_or(""), n, cnt)));+ }+ c("pads-unique", dups.is_empty(), if dups.is_empty() { "all pad names unique per package".into() } else {+ format!("duplicate pad names - Fusion's importer refuses the ENTIRE library over these: {}. Rename to e.g. 17, 17@2, 17@3...", dups.iter().take(6).cloned().collect::<Vec<_>>().join(", ")) });+ }+ finish(checks, xml.as_deref())+}++fn finish(checks: Vec<Check>, xml: Option<&str>) -> Preflight {+ let ok = checks.iter().all(|c| c.ok);+ let errors = checks.iter().filter(|c| !c.ok).map(|c| format!("{}: {}", c.check, c.detail)).collect();+ let mut res = Preflight { ok, checks, errors, ..Default::default() };+ if let Some(text) = xml {+ let lint = lint_part_metadata(text);+ if lint.error.is_none() {+ res.metadata_ok = Some(lint.ok);+ if !lint.ok {+ res.metadata_hint = Some(format!("The library will open, but {} part-metadata warning(s) mean an EE cannot order from it: MPN / MANUFACTURER / a distributor PN (MOUSER_PN, DIGIKEY_PN, LCSC_PN) are what this bridge's own BOM export reads, and variants that share geometry are told apart ONLY by these. See fusion_library_parts for the per-variant breakdown.", lint.warnings.len()));+ }+ res.metadata_warnings = Some(lint.warnings);+ res.part_totals = lint.totals;+ }+ }+ res+}
server.py+124−1@@ -3024,7 +3024,6 @@ ADDIN_COMMANDS = { "export_bom", "export_cpl", "export_gerbers",- "set_design_rules", "export_board_image", "detect_layers", }@@ -14608,6 +14607,130 @@ def _handle_action_ledger(args: dict) -> dict: COMMAND_HANDLERS["action_ledger"] = lambda info, args: _handle_action_ledger(args or {}) ++# ── Design rules by fab (roadmap Part 2 item 4) ─────────────────────────────────────────+_DESIGN_RULES_PAGE = "https://wiki.adom.inc/api/v1/pages/adom/pcb-design-rules/files"+++def _design_rules_index(max_age_s: int = 3600) -> dict:+ """The wiki's rules/index.json (vendor, process, layerCount -> files), cached an hour in+ the bridge state dir so a rule change is a wiki publish, never a bridge release."""+ import time as _t, urllib.request as _ur+ cache = os.path.join(os.path.expanduser("~"), ".adom", "fusion-bridge", "design-rules-index.json")+ try:+ if os.path.isfile(cache) and _t.time() - os.path.getmtime(cache) < max_age_s:+ with open(cache, "r", encoding="utf-8") as fh:+ return json.load(fh)+ except Exception:+ pass+ req = _ur.Request(_DESIGN_RULES_PAGE + "/rules/index.json", headers={"User-Agent": "adom-fusion-bridge/" + str(BRIDGE_VERSION)})+ with _ur.urlopen(req, timeout=20) as r:+ idx = json.loads(r.read().decode("utf-8"))+ try:+ os.makedirs(os.path.dirname(cache), exist_ok=True)+ with open(cache, "w", encoding="utf-8") as fh:+ json.dump(idx, fh)+ except Exception:+ pass+ return idx+++def _design_rules_pick(idx: dict, fab: str, process: str, layers: int):+ """The index row for (fab, process, layers). fab matches vendorSlug or a loose alias+ (jlcpcb, pcbway, instapcb, oshpark...); process defaults to the vendor's standard tier."""+ want = re.sub(r"[^a-z0-9]", "", (fab or "").lower())+ rows = [v for v in idx.get("vendors") or [] if re.sub(r"[^a-z0-9]", "", v.get("vendorSlug", "").lower()) == want+ or want and want in re.sub(r"[^a-z0-9]", "", v.get("vendorSlug", "").lower())]+ if not rows:+ return None, sorted({v.get("vendorSlug") for v in idx.get("vendors") or []})+ procs = sorted({v.get("process") for v in rows})+ proc = (process or "").lower() or ("standard" if "standard" in procs else procs[0])+ hit = [v for v in rows if v.get("process") == proc and int(v.get("layerCount") or 0) == int(layers)]+ if not hit:+ return None, {"processes": procs, "layerCounts": sorted({int(v.get("layerCount") or 0) for v in rows if v.get("process") == proc})}+ return hit[0], None+++def _handle_set_design_rules(fusion_info: dict, args: dict) -> dict:+ """fusion_set_design_rules {fab, process?, layers, druPath?, action}: load a FAB's design+ rules into the open board. The rule packs live on the wiki page adom/pcb-design-rules+ (one JSON record per vendor/process/layer count, generated .edru per row, every number+ citing the vendor's own capabilities page); the bridge fetches the .edru, stages it on+ the box, and the add-in runs `drc load <path>`. No fab (or fab:"instapcb" with the page+ unreachable) keeps the bundled Adom InstaPCB behaviour. action:"list" answers with the+ vendors, processes and layer counts on the page so the AI can offer a real choice.++ AI hint: the pack is per (vendor, process, layers). "jlcpcb" alone means its standard+ tier; a user on JLCPCB's advanced/HDI service needs process:"advanced" where the page+ has it. Always confirm afterwards with fusion_alerts (an empty list) - a bad .edru shows+ as an in-canvas toast, never a dialog.+ """+ a = dict(args or {})+ action = (a.get("action") or "apply").lower()+ fab = (a.get("fab") or "").strip()+ dru_path = (a.get("druPath") or "").replace("\\", "/")+ if action == "list":+ try:+ idx = _design_rules_index()+ except Exception as e:+ return {"success": False, "errorCode": "design_rules_page_unreachable", "error": str(e)[:200],+ "_hint": "The wiki page adom/pcb-design-rules did not answer; the bundled Adom InstaPCB rules still apply with no fab argument."}+ rows = [{"fab": v.get("vendorSlug"), "vendor": v.get("vendor"), "process": v.get("process"),+ "layers": v.get("layerCount"), "available": bool(v.get("filesGenerated"))} for v in idx.get("vendors") or []]+ return {"success": True, "rules": rows, "count": len(rows), "page": "https://wiki.adom.inc/adom/pcb-design-rules",+ "_hint": "Pick a row and call fusion_set_design_rules {fab, process, layers}. Layers auto-detects when omitted (2 or 4 from the open board)."}+ if action in ("show", "export") or (not fab and not dru_path):+ return _proxy_to_addin("set_design_rules", a, timeout=90) # the bundled InstaPCB path, unchanged+ if not dru_path:+ layers = str(a.get("layers") or "auto")+ if layers == "auto":+ det = _proxy_to_addin("detect_layers", {}, timeout=90) or {}+ layers = str(((det.get("data") or {}).get("layerCount")) or det.get("layerCount") or 2)+ try:+ idx = _design_rules_index()+ except Exception as e:+ return {"success": False, "errorCode": "design_rules_page_unreachable", "error": str(e)[:200],+ "_hint": "Could not fetch the rule packs from wiki.adom.inc; pass druPath to a local .edru, or omit fab for the bundled Adom InstaPCB rules."}+ row, alt = _design_rules_pick(idx, fab, a.get("process") or "", int(layers))+ if not row:+ return {"success": False, "errorCode": "design_rules_not_found", "fab": fab, "layers": int(layers),+ "available": alt, "_hint": "No pack for that fab/process/layer count; `available` lists what the page has. fusion_set_design_rules {action:'list'} shows every row."}+ if not row.get("filesGenerated"):+ return {"success": False, "errorCode": "design_rules_not_generated", "fab": fab, "process": row.get("process"), "layers": int(layers),+ "_hint": "The page has this vendor's record but no generated .edru yet (the vendor publishes no usable limits for it). Pick another process or fab."}+ rel = (row.get("files") or {}).get("fusionEdru")+ import urllib.request as _ur+ try:+ req = _ur.Request(_DESIGN_RULES_PAGE + "/" + rel, headers={"User-Agent": "adom-fusion-bridge/" + str(BRIDGE_VERSION)})+ with _ur.urlopen(req, timeout=30) as r:+ edru = r.read()+ except Exception as e:+ return {"success": False, "errorCode": "design_rules_download_failed", "error": str(e)[:200], "file": rel}+ if b"<designrules" not in edru:+ return {"success": False, "errorCode": "design_rules_invalid", "file": rel,+ "error": "The downloaded file is not an EAGLE design rules document."}+ stage = os.path.join(os.environ.get("TEMP") or os.path.expanduser("~"), "adom-fusion", "design-rules")+ os.makedirs(stage, exist_ok=True)+ dru_path = os.path.join(stage, os.path.basename(rel)).replace("\\", "/")+ with open(dru_path, "wb") as fh:+ fh.write(edru)+ a["_pack"] = {"fab": row.get("vendorSlug"), "vendor": row.get("vendor"), "process": row.get("process"),+ "layers": row.get("layerCount"), "file": rel, "record": (row.get("files") or {}).get("record")}+ a["druPath"] = dru_path+ a["action"] = "apply"+ res = _proxy_to_addin("set_design_rules", a, timeout=90) or {}+ if isinstance(res, dict):+ if a.get("_pack"):+ res.setdefault("data", {})+ if isinstance(res["data"], dict):+ res["data"]["pack"] = a["_pack"]+ res["data"]["source"] = "https://wiki.adom.inc/adom/pcb-design-rules"+ res["_hint"] = ((res.get("_hint") or "") + " Confirm with fusion_alerts {sinceSec:60}: an empty list means Fusion accepted the rules; a toast means it refused the file.").strip()+ return res+++COMMAND_HANDLERS["set_design_rules"] = _handle_set_design_rules+ def _handle_reset_workspace(args: dict) -> dict: """fusion_reset_workspace - bring Fusion back to a clean slate between runs (#59).
tools/freeze-contract.pyadded+22@@ -0,0 +1,22 @@+#!/usr/bin/env python3+"""Regenerate contracts/describe.json from describe.py: the verb catalog both the Python+bridge and the Rust exe must serve identically. Run after any describe.py change; the+release refuses to ship a stale contract (tools/release.sh)."""+import json, sys, os, datetime+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))+import describe+verbs = []+for t in describe._T:+ name, summary, inp, out, timeout, status_verb, long_running, example = (list(t) + [None] * 8)[:8]+ verbs.append({"name": name, "summary": summary, "input": inp or {}, "output": out or {},+ "timeoutSec": timeout, "statusVerb": status_verb, "longRunning": bool(long_running), "example": example or {}})+verbs.sort(key=lambda v: v["name"])+doc = {"contract": "fusion-bridge verb catalog", "frozen": datetime.date.today().isoformat(), "source": "describe.py",+ "note": "The Python bridge and the Rust bridge must both serve this catalog byte-for-byte in meaning: same names, same args, same result shapes, same timeouts. Regenerate ONLY from describe.py and diff in CI.",+ "verbs": verbs}+p = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "contracts", "describe.json")+old = json.load(open(p)) if os.path.exists(p) else {}+if old.get("verbs") == verbs:+ print("contract unchanged (%d verbs)" % len(verbs)); sys.exit(0)+json.dump(doc, open(p, "w"), indent=2, sort_keys=True); open(p, "a").write("\n")+print("contract refrozen: %d verbs (was %d)" % (len(verbs), len(old.get("verbs") or [])))
tools/promote.py+1@@ -9,6 +9,7 @@ import json, sys raw = open(sys.argv[1], encoding="utf-8").read() want, mf = sys.argv[2], sys.argv[3] m, _ = json.JSONDecoder().raw_decode(raw[raw.index("{"):])+m["released_at"] = __import__("datetime").datetime.now(__import__("datetime").timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") # the PUBLIC release moment, not the template's if m.get("version") != want: sys.exit("REFUSED: insiders tier serves %r, not %r. Promote what insiders actually ran." % (m.get("version"), want))
tools/release.sh+2−1@@ -193,6 +193,7 @@ echo " dashboard $($DASH_BIN --version 2>&1 | head -1)" step "Building Rust bridge exe" bash tools/check-portable.sh || die "check-portable failed" bash tools/check-naming.sh || die "check-naming failed"+python3 tools/freeze-contract.py || die "freeze-contract failed" # the Rust exe asserts it serves this catalog ( cd rust && cargo build --release --target x86_64-pc-windows-gnu -p adom-fusion-bridge ) || die "rust exe build failed" RUST_EXE="rust/target/x86_64-pc-windows-gnu/release/adom-fusion-bridge.exe" [ -s "$RUST_EXE" ] || die "rust exe missing after build"@@ -342,7 +343,7 @@ PUSH_FILES="server.py fusion_detect.py describe.py timings.py aps.py install_add dockbar.json install.sh" for f in handlers/*.py addin/AdomBridge/*.py addin/AdomBridge/commands/*.py \ addin/AdomBridge/*.manifest skills/*/SKILL.md tools/release.sh CLAUDE.md \- tools/promote.sh tools/promote.py tools/check-portable.sh tools/check-naming.sh \+ tools/promote.sh tools/promote.py tools/check-portable.sh tools/check-naming.sh tools/freeze-contract.py \ contracts/describe.json rust/Cargo.toml rust/PORTING.md \ rust/*/Cargo.toml rust/*/src/*.rs \ library_bundle.py README.md docs/*.md docs/img/*.svg docs/img/*.png \