← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.245\ No newline at end of file+1.9.246\ No newline at end of file
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.9.245",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.245/adom-bridge-fusion-v1.9.245.zip",-  "sha256": "8dc2672822ad4c7a504009d0fba2e79b06b04d263fd1bb8fd158f4d0731f00b1",-  "size": 541165,+  "version": "1.9.246",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.246/adom-bridge-fusion-v1.9.246.zip",+  "sha256": "f2d56a288f2fab8f4ab73bfcfcd131113b75a9025f9271a0e7ebe5db3a12c1f9",+  "size": 546509,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.245",+  "version": "1.9.246",   "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.",   "homepage": "https://wiki.adom.inc/adom/fusion-bridge",   "author": "Adom Inc.",
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.9.245"+version = "1.9.246" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.245"+version = "1.9.246" edition = "2021"  [dependencies]
describe.py+1
@@ -15,6 +15,7 @@ _STATUS = "fusion_get_app_state" _T = [     # ── Lifecycle ──────────────────────────────────────────────────────────     ("fusion_preflight_lbr", "PRE-ANALYZE a .lbr: will Fusion open it? Offline and instant - no Fusion needed, nothing launched. Runs the named checks the bridge learned from real Fusion rejections: file-readable, size-sane (truncated-transfer corpse detection), xml-parses, root-is-eagle, packages-nonempty (Fusion's silent 'No Components'), symbols-present, devicesets-present (missing ones make EDIT raise Create-new prompts), connects-resolve (dead wiring renders fine and connects nothing). fusion_open_lbr and fusion_create_library run the same gate automatically; call this directly to gate a batch BEFORE staging.", {"libraryPath": "str (Windows .lbr path)"}, {"success": "bool", "preflight": "{ok, checks[], errors[]}"}, 10, None, True, {"libraryPath": "C:/Temp/PART.lbr"}),+    ("fusion_install_library_bundle", "COMPOSITE, the Fusion twin of kicad_install_library_bundle (#68): ONE call takes a staged Cloud Library bundle (zip of <Lib>.lbr + <Part>.step models, or a bare lbrPath with STEPs beside it), unpacks it safely, places it under ~/Documents/Adom Libraries/<name>/, pairs EVERY deviceset with its STEP by name (deviceset, package, normalised, prefix; a one-part library pairs its single model regardless), lints part numbers (partMetadata: MPN, manufacturer, Mouser/DigiKey/LCSC - reported, never blocking), pre-flights Fusion state (fusion{status}), and starts the same bind -> prove chain fusion_create_library runs. ASYNC BY CONTRACT: returns IMMEDIATELY with runId + statusVerb because a STEP bind is 3-15 minutes and cannot fit a relay window; poll fusion_prove_result (measured percent, four streamed proof frames per part, provedParts[], and an addin_restart_required payload the CALLER turns into the ask). PARTIAL SHIPS: a deviceset with no model is a NAMED skip in unbound[], the rest still bind; partsBound < partsTotal is the signal. capture:false only places the files and returns. This is the one contract the wiki CLI (`library install --format fusion`) and the Export button both read; the recipe has one owner.", {"zipPath": "str (staged bundle zip on THIS machine) - or", "lbrPath": "str (staged .lbr, STEP models beside it)", "libraryName": "optional str (default: file stem)", "capture": "optional bool (default true: bind + prove; false: place only)"}, {"installed": "bool", "libraryPath": "str", "partsTotal": "int", "partsToBind": "int", "unbound": "[{package, why}]", "partMetadata": "{ok, summary, warnings[]}", "fusion": "{installed, running, addinReady, status, message}", "runId": "str", "statusVerb": "fusion_prove_result"}, 60, "fusion_prove_result", True, {"zipPath": "C:/Temp/MyLib.zip", "libraryName": "MyLib"}),     ("fusion_create_library", "ONE-CALL library creation for web-control (one call = one consent). EVERY LIBRARY IS MULTI-PART - a single-part library is simply parts:[one entry], and there is no separate single-part verb to choose. Pass parts:[{package, lbrPath, modelPath}] for any number of parts; the older {libraryPath, stepPath} shape still works and is normalised into a one-element parts list, so both run the identical code path and the ETA scales with the part count either way. TWO entry shapes: A) {libraryPath, stepPath} - a ready .lbr plus the 3D; B) {kicadSymbolPath, kicadFootprintPath, stepPath, pinMap?} - KiCad sources; needs adom-lbr, which is Linux-container-only by platform rule, so on Windows/Mac boxes shape B refuses IMMEDIATELY (adom_lbr_unavailable, before any staging or Fusion launch - fusion_readiness reports kicadIngest up front). Convert at AUTHORING time in a container and use shape A. pinMap = explicit {pinName: padName} for when pin numbers do not match pad names (BGA-style). Chains convert -> bind 3D -> prove (four evidence frames + verdict) in the BACKGROUND and returns immediately; poll fusion_prove_result (stage narrates; top-level `verdict` proven/not_proven is the score - `stage:done` only means the chain finished). EVERY PART IS PROVED, not just the first: a 10-component library streams 40 evidence frames, FOUR per component (symbol, footprint, 3d, ganged) in assembly order, each tagged with `part` so a front end can group them as they arrive, and `provedParts[]` reports per-part pass/fail. The library is proven only if EVERY part is - one good part does not vouch for nine unchecked ones. On the FINAL fetch pass {inlineFrames:true} to get each evidence frame as a base64 thumbnailDataUrl a browser can render (plain polls return box paths only). The chain closes its library document on success AND failure - batches need no between-part sweeping. stepPath is required for a PROVEN library - no real 3D body without it.", {"parts": "optional [{package, lbrPath, modelPath}] - THE canonical shape, any count", "libraryPath": "optional str (shape A, or output path for B)", "kicadSymbolPath": "optional str", "kicadFootprintPath": "optional str", "stepPath": "optional str (required for proven)", "pinMap": "optional {pinName: padName}", "name": "optional str"}, {"started": "bool", "statusVerb": "str"}, 30, "fusion_prove_result", True, {"libraryPath": "C:/Temp/PART.lbr", "stepPath": "C:/Temp/PART.step"}),     ("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, {}),
install.sh+1−1
@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.245"+DASH_VERSION="1.9.246" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.245",+  "version": "1.9.246",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {
page.json+11−8
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.245",+  "version": "1.9.246",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {@@ -11,6 +11,10 @@     "uninstall": "./uninstall.sh"   },   "tags": [+    "ab",+    "bridge",+    "fusion",+    "desktop-app",     "fusion360",     "autodesk",     "cad",@@ -75,7 +79,10 @@     "build a basic parts library",     "add a 3d model to a component",     "make a fusion 360 library with 3d",-    "set up the fusion bridge"+    "set up the fusion bridge",+    "fusion dashboard",+    "fusion status page",+    "open the fusion launch page"   ],   "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).",   "brief": "Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.",@@ -104,9 +111,5 @@       "label": "Part volume",       "prompt": "How much material does each printed part in this assembly use?"     }-  ],-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"-  }-}\ No newline at end of file+  ]+}
server.py+123
@@ -11,6 +11,7 @@ Usage:  import json import os+import shutil import threading import re import time as _t2@@ -12392,6 +12393,128 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict:   COMMAND_HANDLERS["create_library"] = _handle_create_library+++def _handle_install_library_bundle(fusion_info: dict, args: dict) -> dict:+    """COMPOSITE (#68, Colby's routing call): Cloud Library bundle -> installed, bound, proven+    library in ONE call, the Fusion twin of kicad_install_library_bundle. The wiki CLI's+    `library install --format fusion` and the Export button both read this one contract, so+    the import recipe has exactly one owner (this bridge) instead of living in webcontrol.js.++    Shape (settled on #68, 2026-08-28):+      in : {zipPath | lbrPath (staged on THIS machine), libraryName?, capture? (default true)}+      out: {installed, libraryPath, partsTotal, partsToBind, unbound[], partMetadata,+            fusion{...}, runId, statusVerb:"fusion_prove_result"}  - returned IMMEDIATELY.+    ASYNC by contract: a STEP bind is 3-15 minutes and cannot fit the 135s web-control+    envelope, so this returns the runId and the caller polls fusion_prove_result (measured+    progress, streamed proof frames, per-part verdicts, addin_restart_required bubbled up).+    PARTIAL SHIPS: a deviceset with no model in the bundle is a NAMED skip in `unbound[]`,+    never a silent one, and the rest still bind; partsBound < partsTotal is the signal.+    """+    import tempfile as _tf+    import library_bundle as _lb+    zip_p = (args.get("zipPath") or "").replace("\\", "/")+    lbr_in = (args.get("lbrPath") or args.get("libraryPath") or "").replace("\\", "/")+    if not zip_p and not lbr_in:+        return {"success": False, "errorCode": "no_input",+                "error": "Give me zipPath (a staged Cloud Library bundle) or lbrPath (a staged .lbr "+                         "with its STEP models beside it).",+                "_hint": "Stage with write_file / write_file_chunk first; the path must exist on "+                         "THIS machine. Then poll fusion_prove_result with the runId."}+    src = zip_p or lbr_in+    if not os.path.exists(src):+        return {"success": False, "errorCode": "not_found", "error": "Not on this machine: " + src,+                "_hint": "Stage it with write_file / write_file_chunk first, then pass that path."}+    lib_name = (args.get("libraryName") or "").strip()+    work = None+    try:+        if zip_p:+            if os.path.getsize(zip_p) > _lb.MAX_ZIP_BYTES:+                return {"success": False, "errorCode": "too_large",+                        "error": "Bundle zip is larger than the 256 MB cap"}+            work = _tf.mkdtemp(prefix="adom-lib-bundle-")+            try:+                n = _lb.safe_extract(zip_p, work)+            except Exception as e:+                return {"success": False, "errorCode": "bad_bundle",+                        "error": "Could not unpack the bundle: %s" % str(e)[:160]}+            if not n:+                return {"success": False, "errorCode": "bad_bundle", "error": "The bundle zip is empty"}+            lbr_found, models, all_lbrs = _lb.find_bundle_files(work)+            if not lbr_found:+                return {"success": False, "errorCode": "no_lbr",+                        "error": "No .lbr in the bundle",+                        "_hint": "Expected an Adom Cloud Library bundle for Fusion: <Lib>.lbr plus "+                                 "<Part>.step models (beside it or under 3dmodels/)."}+            lib_name = lib_name or os.path.splitext(os.path.basename(zip_p))[0]+        else:+            lbr_found = lbr_in+            _d = os.path.dirname(lbr_in) or "."+            _, models, all_lbrs = _lb.find_bundle_files(_d)+            lib_name = lib_name or os.path.splitext(os.path.basename(lbr_in))[0]+        if not _lb.library_name_ok(lib_name):+            return {"success": False, "errorCode": "bad_name",+                    "error": "Library name not usable: %r" % lib_name,+                    "_hint": "Letters, digits, space, dot, dash, underscore; up to 64 chars."}+        # The same offline gate every import runs: a corpse .lbr fails HERE, not after a launch.+        pf = _preflight_lbr(lbr_found) if "_preflight_lbr" in globals() else {"ok": True}+        if isinstance(pf, dict) and pf.get("ok") is False:+            return {"success": False, "errorCode": "lbr_preflight_failed",+                    "error": "The .lbr will not open in Fusion: " + "; ".join(pf.get("errors") or [])[:300],+                    "preflight": pf}+        meta = _library_part_metadata(lbr_found)+        pairing = _lb.pair_parts(lbr_found, models, meta)+        # Stable install folder: the library must outlive the temp dir the chain reads from.+        home = os.path.expanduser("~")+        dest = os.path.join(home, "Documents", "Adom Libraries", lib_name)+        placed = _lb.place_library(lbr_found, models, dest)+        lbr_path = placed["lbrPath"]+        parts = [{"package": p["package"], "lbrPath": lbr_path,+                  "modelPath": placed["models"].get(p["modelPath"], p["modelPath"]).replace("\\", "/")}+                 for p in pairing["parts"]]+        report = _part_metadata_report(lbr_path)+        plan = _handle_plan_library_import(fusion_info, {"parts": parts})+        out = {"success": True, "installed": True, "libraryName": lib_name, "libraryPath": lbr_path,+               "installDir": dest.replace("\\", "/"),+               "partsTotal": len(meta.get("devicesets") or []),+               "partsToBind": len(parts),+               "parts": [{"package": p["package"], "modelPath": p["modelPath"]} for p in parts],+               "unbound": pairing["unbound"], "modelsUnused": [os.path.basename(m) for m in pairing["modelsUnused"]],+               "partMetadata": report, "fusion": plan.get("fusion"), "plan": plan.get("plan"),+               "estimatedSec": plan.get("estimatedSec"),+               "otherLbrsInBundle": [os.path.basename(x) for x in all_lbrs[1:]]}+        if args.get("capture") is False:+            out.update(started=False, statusVerb=None,+                       _hint="capture:false - the library is placed at libraryPath and nothing was "+                             "opened in Fusion. Call fusion_create_library {parts} later to bind and prove.")+            return out+        if not parts:+            out.update(started=False, errorCode="nothing_to_bind",+                       _hint="Installed, but no deviceset could be paired with a STEP, so there is "+                             "nothing to bind or prove. unbound[] names each one. Add models named "+                             "like the deviceset or its package and call again.")+            return out+        chain = _handle_create_library(fusion_info, {"libraryPath": lbr_path, "parts": parts,+                                                     "name": lib_name})+        out.update({k: chain.get(k) for k in ("started", "attached", "runId", "statusVerb", "stage")})+        if not chain.get("success"):+            out.update(success=False, error=chain.get("error"), errorCode=chain.get("errorCode"))+        out["_hint"] = ("Installed to libraryPath. The bind+prove chain is running in the background "+                        "(async by contract - a STEP bind cannot fit a relay window). Poll "+                        "fusion_prove_result {runId} every ~15s: stage narrates, percent is measured, "+                        "evidence[] streams four proof frames per part, provedParts[] is per-part, and "+                        "an addin_restart_required payload bubbles up if Fusion must restart (the "+                        "caller owns that ask). partsBound < partsTotal means named skips in unbound[].")+        return out+    finally:+        try:+            if work:+                shutil.rmtree(work, ignore_errors=True)+        except Exception:+            pass+++COMMAND_HANDLERS["install_library_bundle"] = _handle_install_library_bundle COMMAND_HANDLERS["demo_control"] = lambda info, args: _handle_demo_control(args)  
skills/fusion-multipart-libraries/SKILL.md+4
@@ -14,6 +14,10 @@ real chip, flat on its footprint, built entirely this way.  ## ⚡ FASTEST PATH - one call: `fusion_build_library_3d` (v1.6.0+) +> **From a Cloud Library bundle (zip of .lbr + STEPs):** `fusion_install_library_bundle+> {zipPath}` derives `parts[]` itself by pairing every deviceset with a STEP by name, then+> runs this exact chain. Unpaired devicesets are named in `unbound[]`; the rest still ship.+ For a whole many-part library, you no longer loop `make_3d_package` + hand-write bindings. ONE verb does the entire pipeline - per-part footprint+chip f3d packages, **all** bindings injected in one pass, the finished library opened once, plus a BEFORE/AFTER screenshot per part:
skills/fusion-web-control/SKILL.md+28
@@ -41,6 +41,34 @@ fusion_prove_result {runId}            -> poll: stage, percent, etaSec, evidence `fusion_prove_result` is the single poll surface for **both** the create chain and the detached 3D build. No second poll verb to learn. +## The one-call bundle lane: `fusion_install_library_bundle` (#68)++The Fusion twin of `kicad_install_library_bundle`, so `adom-wiki library install --format+fusion` and the page's Export button read ONE contract and the recipe has one owner (this+bridge, not webcontrol.js). Stage the bundle, make one call, poll one verb:++```+write_file / write_file_chunk        -> C:/Temp/MyLib.zip   (<Lib>.lbr + <Part>.step models)+fusion_install_library_bundle {zipPath:"C:/Temp/MyLib.zip", libraryName?:"MyLib"}+   -> {installed:true, libraryPath, partsTotal, partsToBind, unbound[], partMetadata,+       fusion{status,message}, plan[], estimatedSec, runId, statusVerb:"fusion_prove_result"}+fusion_prove_result {runId}          -> poll every ~15s until stage:done; read verdict+```++- **Async by contract.** It returns immediately. A STEP bind is 3-15 minutes and cannot fit+  the 135s web-control envelope; there is no blocking mode on purpose (one contract shape).+- **Partial ships.** A deviceset with no model in the bundle is a NAMED skip in `unbound[]`+  (with `why`), the rest still bind and prove. `partsBound < partsTotal` is the signal.+- **Pairing is by name**: deviceset name, then package name, then a normalised (alnum,+  case-folded) match, then a prefix match. A one-deviceset, one-model bundle pairs regardless+  of names. `modelsUnused[]` lists STEPs nothing claimed.+- **Placed somewhere stable**: `~/Documents/Adom Libraries/<libraryName>/`, never a temp dir.+- **The rest is the existing chain**: preflight gate, part-number lint (`partMetadata`,+  reported not blocking), the `fusion{status}` pre-flight, the bind -> prove stages, the four+  proof frames per part, the banner, and the `addin_restart_required` payload the CALLER+  turns into the two-button ask. `capture:false` only places the files.+- `lbrPath` instead of `zipPath` takes a bare staged `.lbr` with its STEPs beside it.+ ## 1. Always pre-flight before you commit a user to anything  `fusion_plan_library_import {parts: [...]}` **does no work and changes nothing.** It answers