← All Pull Requests

Add fusion_assembly_bom and fusion_physical_properties verbs #22

Merged opened by Oliver 2026-07-23

Adds the two verbs requested in the "BOM Forge built on this bridge" discussion: a mechanical BOM and a physical-properties reader. Both are validated live against an open assembly.

fusion_assembly_bom

Structured, kit-aware mechanical BOM for the active Design assembly (the counterpart to the electronics-only 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. Matches Fusion's Manage -> BOM quantities. Args: treatAsUnit, exclude, includePhysicalProperties, outputPath (CSV). Returns parts[] with componentName, partNumber, description, material, quantity.

Validated on a live assembly: T Nut Drop In 2020 M5 counts 14 (a flat allOccurrences walk gives 28), T Nut Eco 4040 M8 gives 18 (flat 44), BHCS M8x16 gives 20 (flat 46), fully-bundled hardware (BHCS M5x8, SHCS M8x18) correctly drops to 0.

fusion_physical_properties

Per-component volume (cm3), mass (kg), density, area, and center of mass, read directly instead of via a modeling script. Args: names (scope), accuracy.

Changes

  • addin/AdomBridge/commands/assembly_bom.py (new)
  • addin/AdomBridge/commands/physical_properties.py (new)
  • addin/AdomBridge/commands/__init__.py (register both handlers)
  • describe.py (self-describe both verbs)
  • BRIDGE_VERSION -> 1.7.10

Both handlers run on the add-in main thread (like the other commands) and py_compile clean. Closes the request in the feature-request discussion on this page.

Diff

--- a/addin/AdomBridge/commands/assembly_bom.py+++ b/addin/AdomBridge/commands/assembly_bom.py@@ -0,0 +1,141 @@+"""Structured, kit-aware mechanical assembly BOM (mirrors Fusion's Manage -> BOM).++The electronics `export_bom` only works on an open PCB board. This is its mechanical+counterpart: it walks the active Design assembly and returns a purchasing BOM that+respects the component hierarchy. It recurses through organizational subassemblies but+counts a physical part or a purchased kit/unit ONCE, so hardware modeled INSIDE a kit+(for example a "... with fasteners" bracket, or a bearing/pulley unit) is not+double-counted the way a flat allOccurrences walk would count it.+"""++import csv++import adsk.core+import adsk.fusion+++def _material_name(comp):+    try:+        if comp.material:+            return comp.material.name+    except Exception:+        pass+    try:+        if comp.bRepBodies.count > 0 and comp.bRepBodies.item(0).material:+            return comp.bRepBodies.item(0).material.name+    except Exception:+        pass+    return ""+++def _attr(comp, name):+    try:+        v = getattr(comp, name, "") or ""+        return v.strip()+    except Exception:+        return ""+++def handle_assembly_bom(app: adsk.core.Application, args: dict) -> dict:+    """Return a structured, kit-aware parts list for the active mechanical design.++    args:+      treatAsUnit  optional list of name substrings whose subassemblies are counted once+                   and NOT exploded (default: ["with fasteners"]).+      exclude      optional list of top-level component-name prefixes to skip entirely.+      includePhysicalProperties  optional bool -- add volume_cm3 / mass_kg per line.+      outputPath   optional str -- also write a CSV to this Windows path.+    """+    design = adsk.fusion.Design.cast(app.activeProduct)+    if not design:+        return {"success": False, "error": "No active Fusion Design.",+                "_hint": "Open a mechanical design in the Design workspace, then retry."}++    args = args or {}+    kit = [k.lower() for k in (args.get("treatAsUnit") or ["with fasteners", "with fastener"])]+    exclude = tuple(args.get("exclude") or [])+    include_pp = bool(args.get("includePhysicalProperties", False))++    def is_leaf(comp):+        n = comp.name.lower()+        if any(k in n for k in kit):+            return True+        return comp.bRepBodies.count > 0  # a physical part is a leaf++    parts = {}++    def walk(occs):+        for i in range(occs.count):+            occ = occs.item(i)+            comp = occ.component+            name = comp.name+            if any(name.startswith(e) for e in exclude):+                continue+            children = occ.childOccurrences+            has_children = children is not None and children.count > 0+            if is_leaf(comp):+                row = parts.get(name)+                if row is None:+                    row = {+                        "componentName": name,+                        "partNumber": _attr(comp, "partNumber"),+                        "description": _attr(comp, "description"),+                        "material": _material_name(comp),+                        "quantity": 1,+                        "bodies": comp.bRepBodies.count,+                    }+                    if include_pp:+                        try:+                            pp = comp.getPhysicalProperties(+                                adsk.fusion.CalculationAccuracy.LowCalculationAccuracy)+                            row["volume_cm3"] = round(pp.volume, 4)+                            row["mass_kg"] = round(pp.mass, 6)+                        except Exception:+                            row["volume_cm3"] = None+                            row["mass_kg"] = None+                    parts[name] = row+                else:+                    row["quantity"] += 1+            elif has_children:+                walk(children)++    walk(design.rootComponent.occurrences)+    rows = list(parts.values())++    doc_name = design.rootComponent.name+    try:+        if design.parentDocument:+            doc_name = design.parentDocument.name+    except Exception:+        pass++    out = {+        "success": True,+        "design": doc_name,+        "partCount": len(rows),+        "totalInstances": sum(r["quantity"] for r in rows),+        "treatAsUnit": kit,+        "parts": rows,+    }++    outpath = args.get("outputPath")+    if outpath:+        try:+            cols = ["Part Number", "Part Name", "Description", "Material", "Quantity"]+            if include_pp:+                cols += ["Volume cm3", "Mass kg"]+            with open(outpath, "w", newline="", encoding="utf-8") as f:+                w = csv.writer(f)+                w.writerow(cols)+                for r in rows:+                    line = [r["partNumber"], r["componentName"], r["description"],+                            r["material"], r["quantity"]]+                    if include_pp:+                        line += [r.get("volume_cm3"), r.get("mass_kg")]+                    w.writerow(line)+            out["outputPath"] = outpath+        except Exception as e:+            out["csvError"] = str(e)++    return out+--- a/addin/AdomBridge/commands/physical_properties.py+++ b/addin/AdomBridge/commands/physical_properties.py@@ -0,0 +1,65 @@+"""Per-component physical properties (volume, mass, density, area, center of mass).++Fusion exposes physical properties only through the modeling API, so before this verb a+caller had to run getPhysicalProperties inside fusion_run_modeling_script. This verb reads+them directly, per component, so BOM/costing tools can price by volume or mass without a+custom script.+"""++import adsk.core+import adsk.fusion++_ACC = {+    "low": adsk.fusion.CalculationAccuracy.LowCalculationAccuracy,+    "medium": adsk.fusion.CalculationAccuracy.MediumCalculationAccuracy,+    "high": adsk.fusion.CalculationAccuracy.HighCalculationAccuracy,+    "veryhigh": adsk.fusion.CalculationAccuracy.VeryHighCalculationAccuracy,+}+++def handle_physical_properties(app: adsk.core.Application, args: dict) -> dict:+    """Return physical properties per unique component in the active design.++    args:+      names      optional list of component names to measure (default: all).+      accuracy   optional "low" | "medium" | "high" | "veryhigh" (default: low).+    """+    design = adsk.fusion.Design.cast(app.activeProduct)+    if not design:+        return {"success": False, "error": "No active Fusion Design.",+                "_hint": "Open a design in the Design workspace, then retry."}++    args = args or {}+    names = set(args.get("names") or [])+    acc = _ACC.get((args.get("accuracy") or "low").lower(),+                   adsk.fusion.CalculationAccuracy.LowCalculationAccuracy)++    out = {}+    seen = set()+    for occ in design.rootComponent.allOccurrences:+        comp = occ.component+        if comp.name in seen:+            continue+        if names and comp.name not in names:+            continue+        seen.add(comp.name)+        try:+            pp = comp.getPhysicalProperties(acc)+            entry = {+                "volume_cm3": round(pp.volume, 4),+                "mass_kg": round(pp.mass, 6),+                "density": round(pp.density, 6),+                "area_cm2": round(pp.area, 4),+            }+            try:+                com = pp.centerOfMass+                entry["center_of_mass"] = [round(com.x, 4), round(com.y, 4), round(com.z, 4)]+            except Exception:+                pass+            out[comp.name] = entry+        except Exception as e:+            out[comp.name] = {"error": str(e)[:80]}++    return {"success": True, "count": len(out), "accuracy": (args.get("accuracy") or "low"),+            "properties": out}+--- a/addin/AdomBridge/commands/__init__.py+++ b/addin/AdomBridge/commands/__init__.py@@ -1,139 +1,144 @@-"""Command dispatch for the Adom Bridge Fusion 360 add-in."""--import adsk.core--from .app_state import handle_get_app_state-from .document_info import handle_document_info, handle_activate_document-from .export import (-    handle_export_step, handle_export_stl, handle_export_3mf,-    handle_export_f3d, handle_export_usdz, handle_export_fbx,-    handle_export_dxf, handle_export_dwg, handle_export_iges,-    handle_export_obj, handle_export_sat, handle_export_skp,-)-from .design_info import handle_get_design_info-from .electronics import (-    handle_electron_run,-    handle_execute_text_command,-    handle_export_source,-    handle_export_eagle_source,-    handle_open_electronics,-    handle_list_text_commands,-    handle_board_info,-)-from .electron_view import handle_electron_zoom, handle_electron_pan, handle_electron_select-from .open_lbr import handle_open_lbr, handle_export_lbr-from .import_file import handle_import_file-from .parameters import handle_get_parameters, handle_set_parameter-from .screenshot import handle_take_screenshot-from .close_document import handle_close_document, handle_close_all_documents-from .open_electronics_file import handle_open_schematic, handle_open_board, handle_show_3d_board, handle_show_2d_board, handle_show_schematic, handle_import_electronics-from .cloud_documents import (-    handle_save_to_cloud,-    handle_list_cloud_projects,-    handle_list_cloud_files,-    handle_delete_cloud_file,-    handle_create_cloud_folder,-    handle_open_cloud_file,-    handle_open_by_urn,-    handle_search_cloud_files,-    handle_export_cloud_file,-    handle_check_recovery,-    handle_walk_cloud_tree,-)-from .manufacturing import (-    handle_export_bom,-    handle_export_cpl,-    handle_export_gerbers,-    handle_set_design_rules,-    handle_export_board_image,-    handle_detect_layers,-)-from .silkscreen_capture import handle_take_silkscreen_screenshot-from .modeling import handle_run_modeling_script--COMMAND_HANDLERS = {-    # App state-    "get_app_state": handle_get_app_state,-    "document_info": handle_document_info,-    "activate_document": handle_activate_document,-    # 3D CAD commands-    "import_file": handle_import_file,-    "export_step": handle_export_step,-    "export_stl": handle_export_stl,-    "export_3mf": handle_export_3mf,-    "export_f3d": handle_export_f3d,-    "export_usdz": handle_export_usdz,-    "export_fbx": handle_export_fbx,-    "export_dxf": handle_export_dxf,-    "export_dwg": handle_export_dwg,-    "export_iges": handle_export_iges,-    "export_obj": handle_export_obj,-    "export_sat": handle_export_sat,-    "export_skp": handle_export_skp,-    "get_design_info": handle_get_design_info,-    "get_parameters": handle_get_parameters,-    "set_parameter": handle_set_parameter,-    "take_screenshot": handle_take_screenshot,-    # Electronics commands (EAGLE via Electron.run)-    "electron_run": handle_electron_run,-    "electron_zoom": handle_electron_zoom,-    "electron_pan": handle_electron_pan,-    "electron_select": handle_electron_select,-    "execute_text_command": handle_execute_text_command,-    "open_electronics": handle_open_electronics,-    "list_text_commands": handle_list_text_commands,-    "board_info": handle_board_info,-    "export_source": handle_export_source,-    "export_eagle_source": handle_export_eagle_source,-    # Library file commands (Document.Open / EXPORT SCRIPT)-    "open_lbr": handle_open_lbr,-    "export_lbr": handle_export_lbr,-    # Document management-    "close_document": handle_close_document,-    "close_all_documents": handle_close_all_documents,-    # Electronics file opening-    "open_schematic": handle_open_schematic,-    "open_board": handle_open_board,-    "show_3d_board": handle_show_3d_board,-    "show_2d_board": handle_show_2d_board,-    "show_schematic": handle_show_schematic,-    "import_electronics": handle_import_electronics,-    # Cloud document management-    "save_to_cloud": handle_save_to_cloud,-    "list_cloud_projects": handle_list_cloud_projects,-    "list_cloud_files": handle_list_cloud_files,-    "delete_cloud_file": handle_delete_cloud_file,-    "create_cloud_folder": handle_create_cloud_folder,-    "open_cloud_file": handle_open_cloud_file,-    "open_by_urn": handle_open_by_urn,-    "check_recovery": handle_check_recovery,-    "search_cloud_files": handle_search_cloud_files,-    "walk_cloud_tree": handle_walk_cloud_tree,-    "export_cloud_file": handle_export_cloud_file,-    # Manufacturing exports (Gerbers, BOM, CPL, DRC, images)-    "export_bom": handle_export_bom,-    "export_cpl": handle_export_cpl,-    "export_gerbers": handle_export_gerbers,-    "set_design_rules": handle_set_design_rules,-    "export_board_image": handle_export_board_image,-    "detect_layers": handle_detect_layers,-    # Silkscreen layer capture (isolated top/bottom PNG)-    "take_silkscreen_screenshot": handle_take_silkscreen_screenshot,-    # In-app parametric modeling (free; runs adsk.fusion in the live session)-    "run_modeling_script": handle_run_modeling_script,-}---def dispatch_command(app: adsk.core.Application, command: str, args: dict) -> dict:-    """Dispatch a command to the appropriate handler.--    All handlers receive the Fusion Application object and an args dict.-    """-    handler = COMMAND_HANDLERS.get(command)-    if handler is None:-        return {-            "success": False,-            "error": f"Unknown add-in command: {command}",-            "_hint": f"Valid add-in commands: {', '.join(sorted(COMMAND_HANDLERS.keys()))}.",-        }-    return handler(app, args)+"""Command dispatch for the Adom Bridge Fusion 360 add-in."""++import adsk.core++from .app_state import handle_get_app_state+from .document_info import handle_document_info, handle_activate_document+from .export import (+    handle_export_step, handle_export_stl, handle_export_3mf,+    handle_export_f3d, handle_export_usdz, handle_export_fbx,+    handle_export_dxf, handle_export_dwg, handle_export_iges,+    handle_export_obj, handle_export_sat, handle_export_skp,+)+from .design_info import handle_get_design_info+from .electronics import (+    handle_electron_run,+    handle_execute_text_command,+    handle_export_source,+    handle_export_eagle_source,+    handle_open_electronics,+    handle_list_text_commands,+    handle_board_info,+)+from .electron_view import handle_electron_zoom, handle_electron_pan, handle_electron_select+from .open_lbr import handle_open_lbr, handle_export_lbr+from .import_file import handle_import_file+from .parameters import handle_get_parameters, handle_set_parameter+from .screenshot import handle_take_screenshot+from .close_document import handle_close_document, handle_close_all_documents+from .open_electronics_file import handle_open_schematic, handle_open_board, handle_show_3d_board, handle_show_2d_board, handle_show_schematic, handle_import_electronics+from .cloud_documents import (+    handle_save_to_cloud,+    handle_list_cloud_projects,+    handle_list_cloud_files,+    handle_delete_cloud_file,+    handle_create_cloud_folder,+    handle_open_cloud_file,+    handle_open_by_urn,+    handle_search_cloud_files,+    handle_export_cloud_file,+    handle_check_recovery,+    handle_walk_cloud_tree,+)+from .manufacturing import (+    handle_export_bom,+    handle_export_cpl,+    handle_export_gerbers,+    handle_set_design_rules,+    handle_export_board_image,+    handle_detect_layers,+)+from .silkscreen_capture import handle_take_silkscreen_screenshot+from .modeling import handle_run_modeling_script+from .assembly_bom import handle_assembly_bom+from .physical_properties import handle_physical_properties++COMMAND_HANDLERS = {+    # App state+    "get_app_state": handle_get_app_state,+    "document_info": handle_document_info,+    "activate_document": handle_activate_document,+    # 3D CAD commands+    "import_file": handle_import_file,+    "export_step": handle_export_step,+    "export_stl": handle_export_stl,+    "export_3mf": handle_export_3mf,+    "export_f3d": handle_export_f3d,+    "export_usdz": handle_export_usdz,+    "export_fbx": handle_export_fbx,+    "export_dxf": handle_export_dxf,+    "export_dwg": handle_export_dwg,+    "export_iges": handle_export_iges,+    "export_obj": handle_export_obj,+    "export_sat": handle_export_sat,+    "export_skp": handle_export_skp,+    "get_design_info": handle_get_design_info,+    "assembly_bom": handle_assembly_bom,+    "physical_properties": handle_physical_properties,+    "get_parameters": handle_get_parameters,+    "set_parameter": handle_set_parameter,+    "take_screenshot": handle_take_screenshot,+    # Electronics commands (EAGLE via Electron.run)+    "electron_run": handle_electron_run,+    "electron_zoom": handle_electron_zoom,+    "electron_pan": handle_electron_pan,+    "electron_select": handle_electron_select,+    "execute_text_command": handle_execute_text_command,+    "open_electronics": handle_open_electronics,+    "list_text_commands": handle_list_text_commands,+    "board_info": handle_board_info,+    "export_source": handle_export_source,+    "export_eagle_source": handle_export_eagle_source,+    # Library file commands (Document.Open / EXPORT SCRIPT)+    "open_lbr": handle_open_lbr,+    "export_lbr": handle_export_lbr,+    # Document management+    "close_document": handle_close_document,+    "close_all_documents": handle_close_all_documents,+    # Electronics file opening+    "open_schematic": handle_open_schematic,+    "open_board": handle_open_board,+    "show_3d_board": handle_show_3d_board,+    "show_2d_board": handle_show_2d_board,+    "show_schematic": handle_show_schematic,+    "import_electronics": handle_import_electronics,+    # Cloud document management+    "save_to_cloud": handle_save_to_cloud,+    "list_cloud_projects": handle_list_cloud_projects,+    "list_cloud_files": handle_list_cloud_files,+    "delete_cloud_file": handle_delete_cloud_file,+    "create_cloud_folder": handle_create_cloud_folder,+    "open_cloud_file": handle_open_cloud_file,+    "open_by_urn": handle_open_by_urn,+    "check_recovery": handle_check_recovery,+    "search_cloud_files": handle_search_cloud_files,+    "walk_cloud_tree": handle_walk_cloud_tree,+    "export_cloud_file": handle_export_cloud_file,+    # Manufacturing exports (Gerbers, BOM, CPL, DRC, images)+    "export_bom": handle_export_bom,+    "export_cpl": handle_export_cpl,+    "export_gerbers": handle_export_gerbers,+    "set_design_rules": handle_set_design_rules,+    "export_board_image": handle_export_board_image,+    "detect_layers": handle_detect_layers,+    # Silkscreen layer capture (isolated top/bottom PNG)+    "take_silkscreen_screenshot": handle_take_silkscreen_screenshot,+    # In-app parametric modeling (free; runs adsk.fusion in the live session)+    "run_modeling_script": handle_run_modeling_script,+}+++def dispatch_command(app: adsk.core.Application, command: str, args: dict) -> dict:+    """Dispatch a command to the appropriate handler.++    All handlers receive the Fusion Application object and an args dict.+    """+    handler = COMMAND_HANDLERS.get(command)+    if handler is None:+        return {+            "success": False,+            "error": f"Unknown add-in command: {command}",+            "_hint": f"Valid add-in commands: {', '.join(sorted(COMMAND_HANDLERS.keys()))}.",+        }+    return handler(app, args)+--- a/describe.py+++ b/describe.py@@ -1,221 +1,232 @@-"""fusion_describe — self-describe the bridge's verb surface for Adom Desktop.--AD v1.9.23+ added a Verbs tab to each bridge's detail pop-out (lists verbs,-expands each for its I/O + timeout + status schema, with an inline runner). AD-does NOT hardcode a cloud-owned bridge's verbs, so the bridge must self-describe.-AD POSTs {command:"describe", args:{}} and caches the result at ~/.adom/bridge-verbs/.--Each entry: name, summary, input, output, timeoutSeconds, statusVerb, longRunning,-example. Keep this in step with the actual handlers as verbs are added.-"""--_STATUS = "fusion_get_app_state"--# Compact source table: name -> (summary, input, output, timeout, statusVerb, longRunning, example)-_T = [-    # ── Lifecycle ──────────────────────────────────────────────────────────-    ("fusion_start", "Launch Fusion 360 (if needed) and wait for the AdomBridge add-in to be ready.",-     {}, {"success": "bool", "addinReady": "bool", "status": "str"}, 150, None, True, {}),-    ("fusion_stop", "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.",-     {"skipCleanClose": "optional bool"}, {"success": "bool", "output": "str"}, 60, None, False, {}),-    ("fusion_kill", "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.",-     {"skipCleanClose": "optional bool"}, {"success": "bool", "output": "str"}, 30, None, False, {}),-    ("fusion_close_window", "Close the active Fusion document/window.",-     {"name": "optional str (default: active)"}, {"success": "bool"}, 30, None, False, {}),-    ("fusion_get_app_state", "Current app state: running, active document, workspace, isElectronics. The poll/status verb for long-running calls.",-     {}, {"activeDocument": "str", "activeWorkspace": "str", "isElectronics": "bool"}, 20, None, False, {}),-    ("fusion_readiness", "FAST readiness check (does NOT launch Fusion): is the Fusion host app installed + running + the bridge ready to drive? AD DETECTS Fusion (never installs it) via bridge.json 'detect', and auto-installs Python if missing (AD >=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 AD's bridge_readiness.",-     {}, {"installed": "bool", "running": "bool", "ready": "bool", "hostApp": "str", "bridgeVersion": "str"}, 15, None, False, {}),--    ("fusion_new_electronics_from_eagle", "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.",-     {"schPath": "required str (Windows path to .sch)", "brdPath": "optional str (defaults to sibling .brd)"},-     {"success": "bool", "imported": "list"}, 200, None, False,-     {"schPath": "C:/Users/me/adom-lib/MyBoard.sch", "brdPath": "C:/Users/me/adom-lib/MyBoard.brd"}),--    ("fusion_notify_owner", "LAST-RESORT human escalation: toast the user's MAIN computer (fans out cross-AD 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.",-     {"title": "optional str", "body": "required str", "level": "optional str info|warning|error"},-     {"success": "bool", "targets": "list"}, 20, None, False,-     {"title": "Fusion needs you", "body": "Please complete the Autodesk 2FA prompt on winvm - everything else is done."}),--    ("fusion_signin", "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.",-     {"profile": "optional str (chrome:<email>/edge:<email> - the profile your Autodesk account is in; remembered)", "auto": "optional bool (do click+capture+reopen in one on-box call - recommended)", "waitSec": "optional int (server-side poll budget for the fresh URL, default 32)"},-     {"stage": "str", "done": "bool", "signinWhere": "str", "signinProfile": "str", "signinProfileReason": "str", "signinWrongBrowser": "str", "signinAuthUrl": "str", "signinOpenedVia": "str (extension|extension_free)", "signinWall": "str|null (twofactor_email_code|credentials|protocol_handoff)", "notifyDelivered": "bool", "signinWallHint": "str|null", "narrate": "str"},-     120, "fusion_signin", True, {}),-    ("fusion_set_auto_update", "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.",-     {"enabled": "optional bool (omit to read current value)"},-     {"autoUpdateFusion": "bool", "narrate": "str"},-     30, "fusion_readiness", True, {"enabled": True}),-    ("fusion_signin_2fa", "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.",-     {"code": "required str (the 6 digits; non-digits are stripped)", "hwnd": "optional int (the 2FA window; auto-detected)"},-     {"submitted": "bool", "steps": "list", "narrate": "str"},-     90, "fusion_readiness", True, {"code": "123456"}),-    ("fusion_demo", "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.",-     {"query": "optional str (design to demo; else picked from APS search)", "stage": "optional str ('skip_aps' to proceed without cloud search)"},-     {"stage": "str", "done": "bool", "narrate": "str", "screenshots": "[{label,path}]", "steps": "[str]", "apsSampleSearch": "[{name,project}]"},-     240, "fusion_demo", True, {}),--    # ── Cloud search & files (APS) ─────────────────────────────────────────-    ("fusion_aps_status", "APS state: configured, signed in, token live, never-charge meter.",-     {}, {"configured": "bool", "signedIn": "bool", "neverCharge": "bool"}, 20, None, False, {}),-    ("fusion_aps_set_client_id", "Set the org's APS (PKCE) client id used for cloud search.",-     {"clientId": "required str"}, {"success": "bool"}, 20, None, False, {"clientId": "D5nn...EPAq"}),-    ("fusion_aps_signin", "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.",-     {"exe": "optional str (browser binary to launch+remember)", "profileDir": "optional str (e.g. 'Default','Profile 2')", "name": "optional str", "allowDefaultBrowser": "optional bool"},-     {"success": "bool", "authUrl": "str", "openedVia": "str", "needsExtensionOpen": "bool"}, 30, "fusion_aps_status", False, {}),-    ("fusion_aps_set_browser", "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.",-     {"name": "optional str", "exe": "str (browser binary) OR how:'extension'", "profileDir": "optional str", "how": "optional 'extension'|'launch'"},-     {"success": "bool", "browser": "{name,exe,profileDir}"}, 10, None, False, {"name": "Edge", "exe": "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe", "profileDir": "Default"}),-    ("fusion_aps_get_browser", "Show the remembered Autodesk sign-in browser (None until set).",-     {}, {"success": "bool", "browser": "{name,exe,profileDir}|null"}, 10, None, False, {}),-    ("fusion_aps_forget_browser", "Forget the remembered Autodesk sign-in browser (re-detect next time).",-     {}, {"success": "bool", "cleared": "bool"}, 10, None, False, {}),-    ("fusion_aps_search", "Server-indexed search across the whole team hub (seconds, not a 30-min folder walk).",-     {"query": "required str", "limit": "optional int (default 50)", "hubId": "optional str", "projectId": "optional str"},-     {"count": "int", "results": "[{name,id,projectName,lastModified}]"}, 60, None, False, {"query": "Molecule", "limit": 10}),-    ("fusion_aps_open", "Find a cloud design by name and open it in Fusion (any folder depth). Fire-and-poll.",-     {"query": "required str", "wait": "optional bool"}, {"opening": "bool", "match": "{name}", "statusVerb": "str"}, 60, _STATUS, True, {"query": "BME690 Molecule"}),-    ("fusion_open_by_urn", "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.",-     {"urn": "required str"}, {"success": "bool", "output": "str"}, 240, _STATUS, True, {"urn": "urn:adsk.wipprod:fs.file:vf.xxxx?version=1"}),-    ("fusion_aps_browse", "Browse hubs/projects/folders without launching Fusion.",-     {"hubId": "optional str", "projectId": "optional str", "folderId": "optional str"}, {"items": "[{name,type,id}]"}, 60, None, False, {}),-    ("fusion_aps_recent", "Most-recently-modified designs across the team hub, newest first.",-     {"limit": "optional int (default 25)"}, {"count": "int", "results": "[...]"}, 60, None, False, {"limit": 25}),-    ("fusion_aps_file_info", "Version history + who/when/size for a file (tip + recent versions).",-     {"query": "str  OR  projectId+urn"}, {"versionCount": "int", "tip": "{...}", "versions": "[...]"}, 40, None, False, {"query": "BME690 Molecule"}),-    ("fusion_aps_versions", "Full (uncapped) version history of a file.",-     {"query": "str  OR  projectId+urn"}, {"versionCount": "int", "versions": "[...]"}, 40, None, False, {"query": "BME690 Molecule"}),-    ("fusion_aps_download", "Download a cloud file to the local machine (no Fusion needed).",-     {"query": "str OR projectId+urn", "saveDir": "optional str", "fileName": "optional str"}, {"path": "str", "bytes": "int"}, 90, None, False, {"query": "BME690 Molecule"}),-    ("fusion_aps_upload", "Upload a local file as a NEW cloud file (free Data Management; needs data:write scope). ⛔ 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.",-     {"projectId": "required str", "folderId": "required str (NON-ROOT subfolder)", "localPath": "required str", "fileName": "optional str"},-     {"itemUrn": "str", "bytes": "int"}, 90, None, False, {"projectId": "a.YnVz...", "folderId": "urn:...", "localPath": "C:/x.f3d"}),-    ("fusion_aps_create_folder", "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.",-     {"projectId": "required str", "parentFolderId": "required str", "name": "required str"}, {"folderId": "str"}, 50, None, False, {"projectId": "a.YnVz...", "parentFolderId": "urn:...", "name": "Adom AI Workspace"}),-    ("fusion_aps_get", "Raw authenticated GET against a free APS path (dev/verify helper).",-     {"path": "required str"}, {"status": "int", "body": "obj"}, 40, None, False, {"path": "/project/v1/hubs"}),-    ("fusion_walk_cloud_tree", "⛔ 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.",-     {}, {"disabled": "bool"}, 15, None, False, {}),-    ("fusion_search_cloud_files", "⛔ 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} — server-indexed, seconds, no Fusion. See the 'fusion-aps-search' skill.",-     {"query": "required str"}, {"disabled": "bool"}, 15, None, False, {"query": "Molecule"}),--    # ── Electronics ────────────────────────────────────────────────────────-    ("fusion_show_schematic", "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.",-     {}, {"success": "bool", "activeWorkspace": "str", "productType": "str"}, 70, None, True, {}),-    ("fusion_show_2d_board", "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.",-     {}, {"activeWorkspace": "str"}, 70, None, True, {}),-    ("fusion_show_3d_board", "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()).",-     {}, {"activeWorkspace": "str", "screenshots": "[...]"}, 70, None, True, {}),-    ("fusion_open_by_urn_note", "(see fusion_open_by_urn)", {}, {}, 30, None, False, {}),-    ("fusion_board_info", "Raw board geometry XML (copper/arcs/lines) for the open board.",-     {}, {"rawXml": "str"}, 60, None, False, {}),-    ("fusion_open_lbr", "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').",-     {"filePath": "required str (.lbr WINDOWS path C:/...; stage from container first)", "symbolName": "optional str", "verify": "optional bool"},-     {"success": "bool", "postOpenScreenshot": "{savedTo}"}, 50, None, False, {"filePath": "C:/Users/<user>/adom-lib/nRF54L15.lbr"}),-    ("fusion_attach_3d_package", "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.",-     {"filePath": "required str (.lbr WINDOWS path C:/...)", "modelPath": "required str (.step WINDOWS path C:/...)", "packageName": "optional str"},-     {"success": "bool", "savePending": "bool", "data": "{steps}", "_hint": "the Save-dialog click + verify steps"}, 240, _STATUS, True, {"filePath": "C:/Users/<user>/adom-lib/ADS8588SIPM.fusion.lbr", "modelPath": "C:/Users/<user>/adom-lib/ADS8588SIPM.step", "packageName": "ADS8588SIPM"}),-    ("fusion_install_fusion", "Install Fusion 360 FOR the user - downloads Autodesk's official Client Downloader and streams the free trial silently, with NO shell_execute and NO AD 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 AD's bridge_readiness recommends it.",-     {}, {"success": "bool", "installing": "bool", "alreadyInstalled": "bool?", "mode": "globalinstall|per-user", "statusVerb": "str"}, 90, _STATUS, True, {}),-    ("fusion_generate_package", "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.",-     {"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)", "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)", "unitsCm": "optional bool (params already EPG-native cm)", "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)", "etchStyle": "optional str raised|engraved (default raised = thin WHITE positive extrude, silkscreen-style contrast; engraved = sunken cut)", "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)", "outputStep": "optional str (WINDOWS path; exports STEP incl. the marking)"},-     {"success": "bool", "bodies": "[{name,vol}]", "etched": "str|null", "stepPath": "str|null", "manifest": "obj (settings+why+bboxes; also written as <step>.manifest.json sidecar for marking refreshes)", "manifestPath": "str|null", "epgDir": "str"}, 120, _STATUS, True, {"type": "chip", "params": {"D": 1.6, "E": 0.8, "A": 0.45, "L": 0.3, "L1": 0.3}, "etch": "103", "outputStep": "C:/tmp/ADOM-R0603-103.step"}),-    ("fusion_make_3d_package", "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.",-     {"lbrPath": "required str (.lbr WINDOWS path; its FIRST package's footprint is loaded)", "modelPath": "required str (.step WINDOWS path)", "projectId": "optional str (default MAIN-project)", "folderId": "optional str (default upload folder)", "fileName": "optional str", "captureLabel": "optional str (capture before/after)", "orient": "optional bool (default true)"},-     {"success": "bool", "wip_urn": "str", "dims_mm": "[x,y,z]", "f3d": "str", "before": "str|null", "after": "str|null"}, 200, _STATUS, True, {"lbrPath": "C:/tmp/newlib/R_4k7.lbr", "modelPath": "C:/tmp/newlib/R_4k7.step", "captureLabel": "R_4k7"}),-    ("fusion_build_library_3d", "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.",-     {"lbrPath": "required str (combined .lbr WINDOWS path to bind + open)", "parts": "required [{package, lbrPath (per-part footprint .lbr), modelPath (.step)}]", "outLbrPath": "optional str (default: overwrite lbrPath)", "capture": "optional bool (default true)", "projectId": "optional str", "folderId": "optional str", "openWhenDone": "optional bool (default true)"},-     {"success": "bool", "boundLbr": "str", "partsBound": "int", "partsTotal": "int", "parts": "[{package,success,wip_urn,dims_mm,error}]", "screenshots": "[{package,stage,path}]", "opened": "bool"}, 1800, _STATUS, True, {"lbrPath": "C:/tmp/newlib/AdomBasicParts2.lbr", "parts": [{"package": "R_4k7", "lbrPath": "C:/tmp/newlib/R_4k7.lbr", "modelPath": "C:/tmp/newlib/R_4k7.step"}]}),-    ("fusion_capture_library_views", "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).",-     {"packages": "required [<deviceset name>] (or 'package': single name)", "views": "optional subset of ['component','symbol','footprint'] (default all)", "settle": "optional float secs after each EDIT before capture (default 1.5; raise if a shot shows the PREVIOUS part)"},-     {"success": "bool", "captured": "[{package,view,path,ok}]"}, 300, _STATUS, True, {"packages": ["ESP32-S3FN8", "ATSAMD51J20A-AUT"]}),-    ("fusion_cleanup_cloud_files", "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).",-     {"fileIds": "required [<lineage urn>]", "projectName": "optional str (default active project)", "folderPath": "optional str (default root - where the files live)"},-     {"success": "bool", "deletedCount": "int", "failedCount": "int", "failed": "[{fileId,error}]"}, 1800, _STATUS, True, {"fileIds": ["urn:adsk.wipprod:dm.lineage:abc123"], "projectName": "Adom"}),-    ("fusion_run_modeling_script", "Run an adsk.fusion modeling script in the live session (sketches/extrudes/params). Free programmatic CAD.",-     {"script": "required str (Python using adsk; globals: adsk, app, ui, design)"},-     {"result": "obj", "documentName": "str"}, 180, None, True, {"script": "import adsk.fusion\n# ...build geometry..."}),--    ("fusion_set_preference", "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`.",-     {"theme": "optional str", "invertScrollZoom": "optional bool", "orbitScheme": "optional str", "modelingOrientation": "optional str", "gestureNav": "optional bool", "cameraPivot": "optional bool", "lengthUnit": "optional str"},-     {"applied": "obj", "current": "obj"}, 30, None, True, {"theme": "dark"}),--    ("fusion_get_preferences", "Read current Fusion appearance/navigation preferences (theme, activeTheme, invertScrollZoom, orbitScheme, modelingOrientation, gestureNav, cameraPivot, lengthUnit) as friendly values - the same keys fusion_set_preference accepts.",-     {}, {"current": "obj"}, 20, None, False, {}),--    # ── CAD & 3D export ────────────────────────────────────────────────────-    ("fusion_export_step", "Export the active design to STEP (needs the 3D view).",-     {"outputPath": "required str"}, {"format": "str", "fileSizeKB": "int"}, 300, None, True, {"outputPath": "C:/out/board.step"}),-    ("fusion_export_iges", "Export to IGES.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/board.iges"}),-    ("fusion_export_stl", "Export to STL mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.stl"}),-    ("fusion_export_3mf", "Export to 3MF mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.3mf"}),-    ("fusion_export_usdz", "Export to USDZ (AR / Hydrogen 3D).", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.usdz"}),-    ("fusion_export_optimized_glb", "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.", {"outputPath": "required str (.glb)", "silkscreen": "bool default true", "pin": "str medium|large default medium", "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)"}, {"glbPath": "str", "jobId": "str", "pending": "bool", "meshesAfter": "int", "sizeBytes": "int", "moleculeAnchored": "bool", "silkscreenApplied": "bool"}, 200, None, True, {"outputPath": "C:/out/board.glb"}),-    ("fusion_fetch_optimized_glb", "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.", {"jobId": "required str", "outputPath": "str (.glb)", "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)"}, {"glbPath": "str", "status": "str", "pending": "bool", "sizeBytes": "int"}, 150, None, False, {"jobId": "abc123", "outputPath": "C:/out/board.glb"}),-    ("fusion_board_stackup", "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.", {}, {"layerSetup": "str", "copperLayers": "list", "copperCount": "int", "copperThickness_mm": "list", "dielectricBonds": "list (prepreg/core order)", "fr4": "{x_mm,y_mm,dielectric_mm}"}, 200, None, True, {}),-    ("fusion_export_obj", "Export to OBJ mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.obj"}),-    ("fusion_export_dxf", "Export 2D DXF.", {"outputPath": "required str"}, {"format": "str"}, 120, None, True, {"outputPath": "C:/out/sketch.dxf"}),-    ("fusion_export_dwg", "Export 2D DWG.", {"outputPath": "required str"}, {"format": "str"}, 120, None, True, {"outputPath": "C:/out/sketch.dwg"}),--    # ── Manufacturing ──────────────────────────────────────────────────────-    ("fusion_export_gerbers", "Run the bundled JLCPCB CAM job (auto 2/4-layer) to produce the gerber + drill ZIP.",-     {"outputDir": "optional str (default C:/tmp/adom-gerbers)"}, {"fileCount": "int", "files": "[{name,size}]", "zipPath": "str"}, 180, None, True, {"outputDir": "C:/tmp/jlc"}),-    ("fusion_export_bom", "Export a Bill of Materials CSV (grouped by value+package) from the open board.",-     {"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_load_design_rules", "Load a bundled .edru design-rule set into the board.",-     {"layers": "optional str (auto|2|4)"}, {"success": "bool"}, 90, None, False, {}),-    ("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.",-     {}, {"dialogs": "[...]"}, 30, None, False, {}),-    ("fusion_electron_run", "Run an EAGLE/Electron command in the open board (the generic extension point, e.g. RUN <ulp>).",-     {"command": "required str"}, {"success": "bool", "output": "str"}, 90, None, False, {"command": "set confirm yes;RUN 'C:/tmp/jlcpcb_smta_exporter.ulp' 'C:/tmp/jlc'"}),-    ("fusion_electron_zoom", "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.",-     {"factor": "float (default 2.0)", "fit": "optional bool", "steps": "optional int (default 16)", "frameDelayMs": "optional int (default 35)"},-     {"success": "bool", "output": "str"}, 60, None, True, {"factor": 3, "steps": 20}),-    ("fusion_electron_pan", "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.",-     {"x1": "float", "y1": "float", "x2": "float", "y2": "float", "steps": "optional int (default 16)", "frameDelayMs": "optional int (default 35)"},-     {"success": "bool", "output": "str", "box": "[...]"}, 60, None, True, {"x1": 0, "y1": 0, "x2": 10, "y2": 8}),-    ("fusion_electron_select", "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.",-     {"name": "str (refdes, e.g. R1)", "x": "optional float", "y": "optional float", "properties": "optional bool (default true)"},-     {"success": "bool", "output": "str", "deviceInfo": "str"}, 30, None, True, {"name": "U1"}),--    # ── Self-describe ──────────────────────────────────────────────────────-    ("fusion_describe", "Self-describe every verb the bridge exposes (this list) for AD's Verbs tab + runner.",-     {}, {"success": "bool", "verbs": "[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example}]"}, 20, None, False, {}),-]---def _verbs():-    out = []-    for name, summary, inp, outp, timeout, status, longr, ex in _T:-        if name.endswith("_note"):-            continue-        out.append({-            "name": name,-            "summary": summary,-            "input": inp,-            "output": outp,-            "timeoutSeconds": timeout,-            "statusVerb": status,-            "longRunning": longr,-            "example": ex,-        })-    return out---def handle_describe(args: dict) -> dict:-    verbs = _verbs()-    # `verbs` at the TOP LEVEL is what AD's Verbs tab reads from the raw /command-    # response. AD's relay strips non-standard top-level fields on the CLI proxy-    # path, so we ALSO nest under `data` (which the relay forwards) for CLI/debug.-    return {-        "success": True,-        "bridge": "fusion360",-        "verbCount": len(verbs),-        "verbs": verbs,-        "data": {"bridge": "fusion360", "verbCount": len(verbs), "verbs": verbs},-    }+"""fusion_describe — self-describe the bridge's verb surface for Adom Desktop.++AD v1.9.23+ added a Verbs tab to each bridge's detail pop-out (lists verbs,+expands each for its I/O + timeout + status schema, with an inline runner). AD+does NOT hardcode a cloud-owned bridge's verbs, so the bridge must self-describe.+AD POSTs {command:"describe", args:{}} and caches the result at ~/.adom/bridge-verbs/.++Each entry: name, summary, input, output, timeoutSeconds, statusVerb, longRunning,+example. Keep this in step with the actual handlers as verbs are added.+"""++_STATUS = "fusion_get_app_state"++# Compact source table: name -> (summary, input, output, timeout, statusVerb, longRunning, example)+_T = [+    # ── Lifecycle ──────────────────────────────────────────────────────────+    ("fusion_start", "Launch Fusion 360 (if needed) and wait for the AdomBridge add-in to be ready.",+     {}, {"success": "bool", "addinReady": "bool", "status": "str"}, 150, None, True, {}),+    ("fusion_stop", "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.",+     {"skipCleanClose": "optional bool"}, {"success": "bool", "output": "str"}, 60, None, False, {}),+    ("fusion_kill", "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.",+     {"skipCleanClose": "optional bool"}, {"success": "bool", "output": "str"}, 30, None, False, {}),+    ("fusion_close_window", "Close the active Fusion document/window.",+     {"name": "optional str (default: active)"}, {"success": "bool"}, 30, None, False, {}),+    ("fusion_get_app_state", "Current app state: running, active document, workspace, isElectronics. The poll/status verb for long-running calls.",+     {}, {"activeDocument": "str", "activeWorkspace": "str", "isElectronics": "bool"}, 20, None, False, {}),+    ("fusion_readiness", "FAST readiness check (does NOT launch Fusion): is the Fusion host app installed + running + the bridge ready to drive? AD DETECTS Fusion (never installs it) via bridge.json 'detect', and auto-installs Python if missing (AD >=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 AD's bridge_readiness.",+     {}, {"installed": "bool", "running": "bool", "ready": "bool", "hostApp": "str", "bridgeVersion": "str"}, 15, None, False, {}),++    ("fusion_new_electronics_from_eagle", "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.",+     {"schPath": "required str (Windows path to .sch)", "brdPath": "optional str (defaults to sibling .brd)"},+     {"success": "bool", "imported": "list"}, 200, None, False,+     {"schPath": "C:/Users/me/adom-lib/MyBoard.sch", "brdPath": "C:/Users/me/adom-lib/MyBoard.brd"}),++    ("fusion_notify_owner", "LAST-RESORT human escalation: toast the user's MAIN computer (fans out cross-AD 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.",+     {"title": "optional str", "body": "required str", "level": "optional str info|warning|error"},+     {"success": "bool", "targets": "list"}, 20, None, False,+     {"title": "Fusion needs you", "body": "Please complete the Autodesk 2FA prompt on winvm - everything else is done."}),++    ("fusion_signin", "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.",+     {"profile": "optional str (chrome:<email>/edge:<email> - the profile your Autodesk account is in; remembered)", "auto": "optional bool (do click+capture+reopen in one on-box call - recommended)", "waitSec": "optional int (server-side poll budget for the fresh URL, default 32)"},+     {"stage": "str", "done": "bool", "signinWhere": "str", "signinProfile": "str", "signinProfileReason": "str", "signinWrongBrowser": "str", "signinAuthUrl": "str", "signinOpenedVia": "str (extension|extension_free)", "signinWall": "str|null (twofactor_email_code|credentials|protocol_handoff)", "notifyDelivered": "bool", "signinWallHint": "str|null", "narrate": "str"},+     120, "fusion_signin", True, {}),+    ("fusion_set_auto_update", "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.",+     {"enabled": "optional bool (omit to read current value)"},+     {"autoUpdateFusion": "bool", "narrate": "str"},+     30, "fusion_readiness", True, {"enabled": True}),+    ("fusion_signin_2fa", "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.",+     {"code": "required str (the 6 digits; non-digits are stripped)", "hwnd": "optional int (the 2FA window; auto-detected)"},+     {"submitted": "bool", "steps": "list", "narrate": "str"},+     90, "fusion_readiness", True, {"code": "123456"}),+    ("fusion_demo", "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.",+     {"query": "optional str (design to demo; else picked from APS search)", "stage": "optional str ('skip_aps' to proceed without cloud search)"},+     {"stage": "str", "done": "bool", "narrate": "str", "screenshots": "[{label,path}]", "steps": "[str]", "apsSampleSearch": "[{name,project}]"},+     240, "fusion_demo", True, {}),++    # ── Cloud search & files (APS) ─────────────────────────────────────────+    ("fusion_aps_status", "APS state: configured, signed in, token live, never-charge meter.",+     {}, {"configured": "bool", "signedIn": "bool", "neverCharge": "bool"}, 20, None, False, {}),+    ("fusion_aps_set_client_id", "Set the org's APS (PKCE) client id used for cloud search.",+     {"clientId": "required str"}, {"success": "bool"}, 20, None, False, {"clientId": "D5nn...EPAq"}),+    ("fusion_aps_signin", "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.",+     {"exe": "optional str (browser binary to launch+remember)", "profileDir": "optional str (e.g. 'Default','Profile 2')", "name": "optional str", "allowDefaultBrowser": "optional bool"},+     {"success": "bool", "authUrl": "str", "openedVia": "str", "needsExtensionOpen": "bool"}, 30, "fusion_aps_status", False, {}),+    ("fusion_aps_set_browser", "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.",+     {"name": "optional str", "exe": "str (browser binary) OR how:'extension'", "profileDir": "optional str", "how": "optional 'extension'|'launch'"},+     {"success": "bool", "browser": "{name,exe,profileDir}"}, 10, None, False, {"name": "Edge", "exe": "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe", "profileDir": "Default"}),+    ("fusion_aps_get_browser", "Show the remembered Autodesk sign-in browser (None until set).",+     {}, {"success": "bool", "browser": "{name,exe,profileDir}|null"}, 10, None, False, {}),+    ("fusion_aps_forget_browser", "Forget the remembered Autodesk sign-in browser (re-detect next time).",+     {}, {"success": "bool", "cleared": "bool"}, 10, None, False, {}),+    ("fusion_aps_search", "Server-indexed search across the whole team hub (seconds, not a 30-min folder walk).",+     {"query": "required str", "limit": "optional int (default 50)", "hubId": "optional str", "projectId": "optional str"},+     {"count": "int", "results": "[{name,id,projectName,lastModified}]"}, 60, None, False, {"query": "Molecule", "limit": 10}),+    ("fusion_aps_open", "Find a cloud design by name and open it in Fusion (any folder depth). Fire-and-poll.",+     {"query": "required str", "wait": "optional bool"}, {"opening": "bool", "match": "{name}", "statusVerb": "str"}, 60, _STATUS, True, {"query": "BME690 Molecule"}),+    ("fusion_open_by_urn", "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.",+     {"urn": "required str"}, {"success": "bool", "output": "str"}, 240, _STATUS, True, {"urn": "urn:adsk.wipprod:fs.file:vf.xxxx?version=1"}),+    ("fusion_aps_browse", "Browse hubs/projects/folders without launching Fusion.",+     {"hubId": "optional str", "projectId": "optional str", "folderId": "optional str"}, {"items": "[{name,type,id}]"}, 60, None, False, {}),+    ("fusion_aps_recent", "Most-recently-modified designs across the team hub, newest first.",+     {"limit": "optional int (default 25)"}, {"count": "int", "results": "[...]"}, 60, None, False, {"limit": 25}),+    ("fusion_aps_file_info", "Version history + who/when/size for a file (tip + recent versions).",+     {"query": "str  OR  projectId+urn"}, {"versionCount": "int", "tip": "{...}", "versions": "[...]"}, 40, None, False, {"query": "BME690 Molecule"}),+    ("fusion_aps_versions", "Full (uncapped) version history of a file.",+     {"query": "str  OR  projectId+urn"}, {"versionCount": "int", "versions": "[...]"}, 40, None, False, {"query": "BME690 Molecule"}),+    ("fusion_aps_download", "Download a cloud file to the local machine (no Fusion needed).",+     {"query": "str OR projectId+urn", "saveDir": "optional str", "fileName": "optional str"}, {"path": "str", "bytes": "int"}, 90, None, False, {"query": "BME690 Molecule"}),+    ("fusion_aps_upload", "Upload a local file as a NEW cloud file (free Data Management; needs data:write scope). ⛔ 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.",+     {"projectId": "required str", "folderId": "required str (NON-ROOT subfolder)", "localPath": "required str", "fileName": "optional str"},+     {"itemUrn": "str", "bytes": "int"}, 90, None, False, {"projectId": "a.YnVz...", "folderId": "urn:...", "localPath": "C:/x.f3d"}),+    ("fusion_aps_create_folder", "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.",+     {"projectId": "required str", "parentFolderId": "required str", "name": "required str"}, {"folderId": "str"}, 50, None, False, {"projectId": "a.YnVz...", "parentFolderId": "urn:...", "name": "Adom AI Workspace"}),+    ("fusion_aps_get", "Raw authenticated GET against a free APS path (dev/verify helper).",+     {"path": "required str"}, {"status": "int", "body": "obj"}, 40, None, False, {"path": "/project/v1/hubs"}),+    ("fusion_walk_cloud_tree", "⛔ 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.",+     {}, {"disabled": "bool"}, 15, None, False, {}),+    ("fusion_search_cloud_files", "⛔ 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} — server-indexed, seconds, no Fusion. See the 'fusion-aps-search' skill.",+     {"query": "required str"}, {"disabled": "bool"}, 15, None, False, {"query": "Molecule"}),++    # ── Electronics ────────────────────────────────────────────────────────+    ("fusion_show_schematic", "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.",+     {}, {"success": "bool", "activeWorkspace": "str", "productType": "str"}, 70, None, True, {}),+    ("fusion_show_2d_board", "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.",+     {}, {"activeWorkspace": "str"}, 70, None, True, {}),+    ("fusion_show_3d_board", "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()).",+     {}, {"activeWorkspace": "str", "screenshots": "[...]"}, 70, None, True, {}),+    ("fusion_open_by_urn_note", "(see fusion_open_by_urn)", {}, {}, 30, None, False, {}),+    ("fusion_board_info", "Raw board geometry XML (copper/arcs/lines) for the open board.",+     {}, {"rawXml": "str"}, 60, None, False, {}),+    ("fusion_open_lbr", "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').",+     {"filePath": "required str (.lbr WINDOWS path C:/...; stage from container first)", "symbolName": "optional str", "verify": "optional bool"},+     {"success": "bool", "postOpenScreenshot": "{savedTo}"}, 50, None, False, {"filePath": "C:/Users/<user>/adom-lib/nRF54L15.lbr"}),+    ("fusion_attach_3d_package", "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.",+     {"filePath": "required str (.lbr WINDOWS path C:/...)", "modelPath": "required str (.step WINDOWS path C:/...)", "packageName": "optional str"},+     {"success": "bool", "savePending": "bool", "data": "{steps}", "_hint": "the Save-dialog click + verify steps"}, 240, _STATUS, True, {"filePath": "C:/Users/<user>/adom-lib/ADS8588SIPM.fusion.lbr", "modelPath": "C:/Users/<user>/adom-lib/ADS8588SIPM.step", "packageName": "ADS8588SIPM"}),+    ("fusion_install_fusion", "Install Fusion 360 FOR the user - downloads Autodesk's official Client Downloader and streams the free trial silently, with NO shell_execute and NO AD 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 AD's bridge_readiness recommends it.",+     {}, {"success": "bool", "installing": "bool", "alreadyInstalled": "bool?", "mode": "globalinstall|per-user", "statusVerb": "str"}, 90, _STATUS, True, {}),+    ("fusion_generate_package", "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.",+     {"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)", "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)", "unitsCm": "optional bool (params already EPG-native cm)", "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)", "etchStyle": "optional str raised|engraved (default raised = thin WHITE positive extrude, silkscreen-style contrast; engraved = sunken cut)", "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)", "outputStep": "optional str (WINDOWS path; exports STEP incl. the marking)"},+     {"success": "bool", "bodies": "[{name,vol}]", "etched": "str|null", "stepPath": "str|null", "manifest": "obj (settings+why+bboxes; also written as <step>.manifest.json sidecar for marking refreshes)", "manifestPath": "str|null", "epgDir": "str"}, 120, _STATUS, True, {"type": "chip", "params": {"D": 1.6, "E": 0.8, "A": 0.45, "L": 0.3, "L1": 0.3}, "etch": "103", "outputStep": "C:/tmp/ADOM-R0603-103.step"}),+    ("fusion_make_3d_package", "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.",+     {"lbrPath": "required str (.lbr WINDOWS path; its FIRST package's footprint is loaded)", "modelPath": "required str (.step WINDOWS path)", "projectId": "optional str (default MAIN-project)", "folderId": "optional str (default upload folder)", "fileName": "optional str", "captureLabel": "optional str (capture before/after)", "orient": "optional bool (default true)"},+     {"success": "bool", "wip_urn": "str", "dims_mm": "[x,y,z]", "f3d": "str", "before": "str|null", "after": "str|null"}, 200, _STATUS, True, {"lbrPath": "C:/tmp/newlib/R_4k7.lbr", "modelPath": "C:/tmp/newlib/R_4k7.step", "captureLabel": "R_4k7"}),+    ("fusion_build_library_3d", "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.",+     {"lbrPath": "required str (combined .lbr WINDOWS path to bind + open)", "parts": "required [{package, lbrPath (per-part footprint .lbr), modelPath (.step)}]", "outLbrPath": "optional str (default: overwrite lbrPath)", "capture": "optional bool (default true)", "projectId": "optional str", "folderId": "optional str", "openWhenDone": "optional bool (default true)"},+     {"success": "bool", "boundLbr": "str", "partsBound": "int", "partsTotal": "int", "parts": "[{package,success,wip_urn,dims_mm,error}]", "screenshots": "[{package,stage,path}]", "opened": "bool"}, 1800, _STATUS, True, {"lbrPath": "C:/tmp/newlib/AdomBasicParts2.lbr", "parts": [{"package": "R_4k7", "lbrPath": "C:/tmp/newlib/R_4k7.lbr", "modelPath": "C:/tmp/newlib/R_4k7.step"}]}),+    ("fusion_capture_library_views", "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).",+     {"packages": "required [<deviceset name>] (or 'package': single name)", "views": "optional subset of ['component','symbol','footprint'] (default all)", "settle": "optional float secs after each EDIT before capture (default 1.5; raise if a shot shows the PREVIOUS part)"},+     {"success": "bool", "captured": "[{package,view,path,ok}]"}, 300, _STATUS, True, {"packages": ["ESP32-S3FN8", "ATSAMD51J20A-AUT"]}),+    ("fusion_cleanup_cloud_files", "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).",+     {"fileIds": "required [<lineage urn>]", "projectName": "optional str (default active project)", "folderPath": "optional str (default root - where the files live)"},+     {"success": "bool", "deletedCount": "int", "failedCount": "int", "failed": "[{fileId,error}]"}, 1800, _STATUS, True, {"fileIds": ["urn:adsk.wipprod:dm.lineage:abc123"], "projectName": "Adom"}),+    ("fusion_run_modeling_script", "Run an adsk.fusion modeling script in the live session (sketches/extrudes/params). Free programmatic CAD.",+     {"script": "required str (Python using adsk; globals: adsk, app, ui, design)"},+     {"result": "obj", "documentName": "str"}, 180, None, True, {"script": "import adsk.fusion\n# ...build geometry..."}),++    ("fusion_assembly_bom", "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.",+     {"treatAsUnit": "optional [str] name substrings counted as one unit (default ['with fasteners'])", "exclude": "optional [str] top-level name prefixes to skip", "includePhysicalProperties": "optional bool (adds volume_cm3/mass_kg per line)", "outputPath": "optional str (Windows CSV path)"},+     {"success": "bool", "design": "str", "partCount": "int", "totalInstances": "int", "parts": "[{componentName, partNumber, description, material, quantity, bodies}]"},+     120, None, False, {"treatAsUnit": ["with fasteners"], "includePhysicalProperties": True}),++    ("fusion_physical_properties", "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.",+     {"names": "optional [str] component names to measure (default all)", "accuracy": "optional low|medium|high|veryhigh (default low)"},+     {"success": "bool", "count": "int", "properties": "{name: {volume_cm3, mass_kg, density, area_cm2, center_of_mass}}"},+     120, None, False, {"accuracy": "low"}),++    ("fusion_set_preference", "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`.",+     {"theme": "optional str", "invertScrollZoom": "optional bool", "orbitScheme": "optional str", "modelingOrientation": "optional str", "gestureNav": "optional bool", "cameraPivot": "optional bool", "lengthUnit": "optional str"},+     {"applied": "obj", "current": "obj"}, 30, None, True, {"theme": "dark"}),++    ("fusion_get_preferences", "Read current Fusion appearance/navigation preferences (theme, activeTheme, invertScrollZoom, orbitScheme, modelingOrientation, gestureNav, cameraPivot, lengthUnit) as friendly values - the same keys fusion_set_preference accepts.",+     {}, {"current": "obj"}, 20, None, False, {}),++    # ── CAD & 3D export ────────────────────────────────────────────────────+    ("fusion_export_step", "Export the active design to STEP (needs the 3D view).",+     {"outputPath": "required str"}, {"format": "str", "fileSizeKB": "int"}, 300, None, True, {"outputPath": "C:/out/board.step"}),+    ("fusion_export_iges", "Export to IGES.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/board.iges"}),+    ("fusion_export_stl", "Export to STL mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.stl"}),+    ("fusion_export_3mf", "Export to 3MF mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.3mf"}),+    ("fusion_export_usdz", "Export to USDZ (AR / Hydrogen 3D).", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.usdz"}),+    ("fusion_export_optimized_glb", "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.", {"outputPath": "required str (.glb)", "silkscreen": "bool default true", "pin": "str medium|large default medium", "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)"}, {"glbPath": "str", "jobId": "str", "pending": "bool", "meshesAfter": "int", "sizeBytes": "int", "moleculeAnchored": "bool", "silkscreenApplied": "bool"}, 200, None, True, {"outputPath": "C:/out/board.glb"}),+    ("fusion_fetch_optimized_glb", "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.", {"jobId": "required str", "outputPath": "str (.glb)", "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)"}, {"glbPath": "str", "status": "str", "pending": "bool", "sizeBytes": "int"}, 150, None, False, {"jobId": "abc123", "outputPath": "C:/out/board.glb"}),+    ("fusion_board_stackup", "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.", {}, {"layerSetup": "str", "copperLayers": "list", "copperCount": "int", "copperThickness_mm": "list", "dielectricBonds": "list (prepreg/core order)", "fr4": "{x_mm,y_mm,dielectric_mm}"}, 200, None, True, {}),+    ("fusion_export_obj", "Export to OBJ mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.obj"}),+    ("fusion_export_dxf", "Export 2D DXF.", {"outputPath": "required str"}, {"format": "str"}, 120, None, True, {"outputPath": "C:/out/sketch.dxf"}),+    ("fusion_export_dwg", "Export 2D DWG.", {"outputPath": "required str"}, {"format": "str"}, 120, None, True, {"outputPath": "C:/out/sketch.dwg"}),++    # ── Manufacturing ──────────────────────────────────────────────────────+    ("fusion_export_gerbers", "Run the bundled JLCPCB CAM job (auto 2/4-layer) to produce the gerber + drill ZIP.",+     {"outputDir": "optional str (default C:/tmp/adom-gerbers)"}, {"fileCount": "int", "files": "[{name,size}]", "zipPath": "str"}, 180, None, True, {"outputDir": "C:/tmp/jlc"}),+    ("fusion_export_bom", "Export a Bill of Materials CSV (grouped by value+package) from the open board.",+     {"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_load_design_rules", "Load a bundled .edru design-rule set into the board.",+     {"layers": "optional str (auto|2|4)"}, {"success": "bool"}, 90, None, False, {}),+    ("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.",+     {}, {"dialogs": "[...]"}, 30, None, False, {}),+    ("fusion_electron_run", "Run an EAGLE/Electron command in the open board (the generic extension point, e.g. RUN <ulp>).",+     {"command": "required str"}, {"success": "bool", "output": "str"}, 90, None, False, {"command": "set confirm yes;RUN 'C:/tmp/jlcpcb_smta_exporter.ulp' 'C:/tmp/jlc'"}),+    ("fusion_electron_zoom", "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.",+     {"factor": "float (default 2.0)", "fit": "optional bool", "steps": "optional int (default 16)", "frameDelayMs": "optional int (default 35)"},+     {"success": "bool", "output": "str"}, 60, None, True, {"factor": 3, "steps": 20}),+    ("fusion_electron_pan", "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.",+     {"x1": "float", "y1": "float", "x2": "float", "y2": "float", "steps": "optional int (default 16)", "frameDelayMs": "optional int (default 35)"},+     {"success": "bool", "output": "str", "box": "[...]"}, 60, None, True, {"x1": 0, "y1": 0, "x2": 10, "y2": 8}),+    ("fusion_electron_select", "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.",+     {"name": "str (refdes, e.g. R1)", "x": "optional float", "y": "optional float", "properties": "optional bool (default true)"},+     {"success": "bool", "output": "str", "deviceInfo": "str"}, 30, None, True, {"name": "U1"}),++    # ── Self-describe ──────────────────────────────────────────────────────+    ("fusion_describe", "Self-describe every verb the bridge exposes (this list) for AD's Verbs tab + runner.",+     {}, {"success": "bool", "verbs": "[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example}]"}, 20, None, False, {}),+]+++def _verbs():+    out = []+    for name, summary, inp, outp, timeout, status, longr, ex in _T:+        if name.endswith("_note"):+            continue+        out.append({+            "name": name,+            "summary": summary,+            "input": inp,+            "output": outp,+            "timeoutSeconds": timeout,+            "statusVerb": status,+            "longRunning": longr,+            "example": ex,+        })+    return out+++def handle_describe(args: dict) -> dict:+    verbs = _verbs()+    # `verbs` at the TOP LEVEL is what AD's Verbs tab reads from the raw /command+    # response. AD's relay strips non-standard top-level fields on the CLI proxy+    # path, so we ALSO nest under `data` (which the relay forwards) for CLI/debug.+    return {+        "success": True,+        "bridge": "fusion360",+        "verbCount": len(verbs),+        "verbs": verbs,+        "data": {"bridge": "fusion360", "verbCount": len(verbs), "verbs": verbs},+    }+--- a/BRIDGE_VERSION+++ b/BRIDGE_VERSION@@ -1,1 +1,1 @@-1.7.9+1.7.10

Comments

John Lauer 2026-07-23

Reviewed and merging. The code is clean: defensive try/except on every Fusion API touch, the kit-aware leaf rule (treatAsUnit name match OR bRepBodies.count > 0) is the right call, structured returns carry success/error/_hint like the rest of the bridge, and a CSV write failure lands in csvError instead of throwing away an otherwise-good BOM. Defaulting treatAsUnit to both "with fasteners" and "with fastener" is a nice touch.

The validation numbers are what sold it: 28 -> 14 on T Nut Drop In 2020 M5, 44 -> 18, 46 -> 20, and fully-bundled hardware correctly dropping to 0. That is the double-count bug being fixed, not just moved.

Two notes, neither blocking:

  1. includePhysicalProperties: true calls getPhysicalProperties per unique component on Fusion's main thread, so on a large assembly it can push toward the add-in's HTTP cap, exactly the batching problem you described in the discussion. It defaults to false, which is the right default. I am giving the verb a generous timeout in the catalog and will note the cost in the hint so callers reach for fusion_physical_properties with an explicit names scope when they only need a few parts.
  2. Your plan mentioned a Level column for the structured view; the merged version aggregates to a flat parts list with quantities. That matches Manage -> BOM counts, which is what a purchasing BOM needs, so I am taking it as-is. If BOM Forge later wants the hierarchy, open a follow-up and we can add level without breaking the current shape.

One housekeeping item on my side: your BRIDGE_VERSION bump to 1.7.10 collides with a package version I published earlier today, so I am shipping this as 1.7.11. Nothing for you to change.

Thanks for doing this properly, discussion first with a concrete plan, then an implementation validated against a real assembly. That is a better bug report than most, and it arrived as a PR.

Log in to comment.