← Commit history

v1.6.65: FIX ad_client capabilities() for current AD /commands shape (verbs in topLevel/bridges/cliRequired/desktop, no 'commands' key) - the REAL reason available() was always False and every bridge silently fell back to CLI relays. Plus discovery retries instead of caching a transient miss.

John Lauer ·73483c4415 ·3mo ago ·parent 0c2b468
4 files changed +55−24
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.64\ No newline at end of file+1.6.65\ No newline at end of file
ad_client.py+49−18
@@ -66,9 +66,8 @@ def _discover_direct_api():     ports adom-desktop.exe is LISTENING on and probe each `/commands`; the one that returns AD's verb     catalog is the direct API. Cached for the process lifetime. Windows-only + best-effort; never raises."""     global _discovered_base-    if _discovered_base is not None:-        return _discovered_base or None-    _discovered_base = ""  # default: tried and failed (so we don't re-probe every call)+    if _discovered_base:                # cache SUCCESS only; a transient miss must not stick forever+        return _discovered_base     try:         import subprocess as _sp, re as _re         cnw = getattr(_sp, "CREATE_NO_WINDOW", 0)@@ -144,29 +143,61 @@ def available() -> bool:     return capabilities() is not None  -def capabilities():-    """Set of AD command names from `GET /commands` (cached). None if unreachable.+def _names_from_list(items):+    """Collect verb names from a list of strings and/or {name|command|verb} dicts."""+    out = set()+    for it in items or []:+        if isinstance(it, str):+            out.add(it)+        elif isinstance(it, dict):+            n = it.get("name") or it.get("command") or it.get("verb")+            if n:+                out.add(n)+    return out -    Shape-tolerant: accepts {"commands":[...]}, {"commands":[{"name":..}]}, or a-    bare list of names/objects.-    """++def _parse_command_names(data):+    """Extract the full verb-name set from a /commands response, tolerant of BOTH the legacy+    `{"commands":[...]}` shape AND the current AD (1.9.x) shape, where verbs are split across:+      topLevel: ["notify_user", "targets", ...]            (flat name list)+      cliRequired: ["pull_file", "shell_execute", ...]     (flat name list)+      bridges: [{name, verbs:[...]}, ...]                  (per-bridge verb lists)+      desktop: {commands:["screenshot_window", ...]}       (SHORT names; real verb = "desktop_"+name)+    Without this, capabilities() returned None on every current AD, so available() was always False+    and the bridge silently fell back to slow CLI relays (root-caused live, John 2026-07-14)."""+    names = set()+    if isinstance(data, list):+        return _names_from_list(data)+    if not isinstance(data, dict):+        return names+    if isinstance(data.get("commands"), list):          # legacy shape+        names |= _names_from_list(data["commands"])+    for k in ("topLevel", "cliRequired"):               # current: flat name lists+        if isinstance(data.get(k), list):+            names |= _names_from_list(data[k])+    for b in (data.get("bridges") or []):               # current: per-bridge verb lists+        if isinstance(b, dict):+            names |= _names_from_list(b.get("verbs"))+    dk = data.get("desktop")                            # current: desktop short names -> desktop_*+    if isinstance(dk, dict):+        for c in (dk.get("commands") or []):+            if isinstance(c, str):+                names.add(c)+                names.add("desktop_" + c)+    return names+++def capabilities():+    """Set of AD command names from `GET /commands` (cached). None if unreachable/unparseable."""     global _caps_cache     if _caps_cache is not None:         return _caps_cache     data = _http("GET", "/commands")     if data is None:         return None-    items = data.get("commands") if isinstance(data, dict) else data-    if not isinstance(items, list):+    names = _parse_command_names(data)+    if not names:         return None-    names = set()-    for it in items:-        if isinstance(it, str):-            names.add(it)-        elif isinstance(it, dict):-            n = it.get("name") or it.get("command") or it.get("verb")-            if n:-                names.add(n)     _caps_cache = names     return names 
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.6.64",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.64/adom-bridge-fusion-v1.6.64.zip",-  "sha256": "98761caabf5b6a40e68b9cfd11b61ab08ed741a22d148505b45c07f4a711c539",-  "size": 359076,+  "version": "1.6.65",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.65/adom-bridge-fusion-v1.6.65.zip",+  "sha256": "c8b457a911913e54afa2679cddc4e321129fa3aec02bbaffb6aab1bc5876f445",+  "size": 359576,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.64",+  "version": "1.6.65",   "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/adom-desktop-fusion-bridge",   "author": "Adom Inc.",