← Commit history

v0.1.7: altium_install_library_bundle web-control verb

John Lauer ·d96cd60bbf ·1mo ago ·parent 791fda6
6 files changed +1624−6
CHANGELOG.md+3
@@ -1,5 +1,8 @@ # Changelog +## 0.1.7 — 2026-08-31+- altium_install_library_bundle: the no-AI web-control lane (kicad-parity envelope, zip-slip hardened, restart contract, proof frame). Handoff to Colby in docs/HANDOFF-COLBY.md + acceptance test docs/webcontrol-altium-test.html; ab allowlist request filed as adom/adom-bridge#964.+ ## 0.1.6 — 2026-08-30 - Full skill slate (10): added altium-install (the install recipe, finally packed), altium-libraries, altium-boards, altium-outputs, altium-bridge-dev, altium-bridge-publish — matching the kicad-bridge fleet depth. 
adom-bridge-altium-manifest.json+4−4
@@ -1,9 +1,9 @@ {   "name": "altium",-  "version": "0.1.6",-  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.6/adom-bridge-altium-v0.1.6.zip",-  "sha256": "693f08b288cd78f87a4db324f827c0b81ce6b6b1b32a522b7e701e05a465a0ef",-  "size": 92792,+  "version": "0.1.7",+  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.7/adom-bridge-altium-v0.1.7.zip",+  "sha256": "8beb4ad6b6538216058ec3e656a8d07c5dac0a532f6240dfd75c4bc18977bda5",+  "size": 94744,   "verbPrefixes": [     "altium_"   ],
adom-bridge-altium-v0.1.7.zipadded
⋯ 1 unchanged line ⋯
bridge.json+5−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.1.6",+  "version": "0.1.7",   "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.",   "homepage": "https://wiki.adom.inc/adom/altium-bridge",   "docs": "https://wiki.adom.inc/adom/altium-bridge",@@ -168,6 +168,10 @@     {       "name": "altium_import_part",       "description": "One-call part import: footprint spec + STEP + symbol spec + models + vendor params"+    },+    {+      "name": "altium_install_library_bundle",+      "description": "Web-control no-AI lane: install a staged Altium library bundle zip (.SchLib/.PcbLib/.IntLib) in one call, with a proof frame"     }   ],   "statusVerb": "altium_status",
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "altium-bridge",-  "version": "0.1.6",+  "version": "0.1.7",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the Altium Designer bridge (altium_* verbs). The bridge runtime itself is the release zip; Adom Bridge loads that.",   "tags": [
src/server.pyadded+1611
@@ -0,0 +1,1611 @@+#!/usr/bin/env python3+"""altium-bridge server — the `altium_*` verb namespace for Adom Bridge.++Spawned by ab (`spawn.kind: python`, `--port <n>` appended, ADOM_BIND_HOST=127.0.0.1).+Every verb returns a dict with `success`, a stable `errorCode` on failure, and a+`_hint` the calling AI reads verbatim — that output IS the documentation.++Lanes (see the altium-bridge-plan skill):+  L2 agent   handlers/agent.py   resident DelphiScript loop (file drop) — eval, run_process, state, open+  L6 CLI     X2.EXE -R…          bootstrap + one-shot RunScript/RunScriptText when the agent is NOT resident+  L4/L5 UI   handlers/dialogs.py dialogs by PID; ab UIA/screenshots via ad_client callbacks+"""+from __future__ import annotations++import json+import os+import subprocess+import sys+import time+import traceback+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer+from pathlib import Path++sys.path.insert(0, str(Path(__file__).resolve().parent))++import caller_identity  # noqa: E402+from handlers import ad_client, agent, altium_detect, dialogs, progress, rcs_catalog  # noqa: E402++BRIDGE_VERSION = "0.1.0"+try:+    BRIDGE_VERSION = json.loads((Path(__file__).parent / "bridge.json").read_text())["version"]+except Exception:+    pass++_installs: list[dict] = []+_boot_at = time.time()+++def _install() -> dict | None:+    global _installs+    if not _installs:+        _installs = altium_detect.detect_installs()+    return _installs[0] if _installs else None+++def _not_installed() -> dict:+    return {"success": False, "errorCode": "host_app_not_installed", "hostApp": altium_detect.HOST_APP,+            "error": "Altium Designer is not installed on this machine.",+            "_hint": "Altium is a licensed host app (no silent installer). Offer to run the official "+                     "AltiumDesignerSetup.exe for the user — the altium-install skill has the full drive."}+++# ---- read-only ----------------------------------------------------------------++def h_readiness(args):+    inst = _install()+    lic = altium_detect.licence_state()+    ag = agent.status()+    ready = bool(inst) and lic.get("running") and bool(lic.get("licensed")) and ag.get("alive")+    out = {"success": True, "hostApp": altium_detect.HOST_APP, "installed": bool(inst), "install": inst,+           "running": lic.get("running"), "licensed": lic.get("licensed"), "licenceState": lic.get("state"),+           "adVersion": lic.get("adVersion"), "agentAlive": ag.get("alive"), "ready": ready,+           "bridgeVersion": BRIDGE_VERSION}+    if not inst:+        out["_hint"] = "Not installed — see altium_launch's hint to install."+    elif not lic.get("running"):+        out["_hint"] = "Altium is installed but not running: altium_launch (cold start ~60s, measured)."+    elif not lic.get("licensed"):+        out["_hint"] = (f"Altium is running but the title says '{lic.get('state')}' — scripting is refused "+                        "without a licence. altium_license '{}' drives the trial/On-Demand flow.")+    elif not ag.get("alive"):+        out["_hint"] = "Licensed and running; the agent loop is not resident — altium_agent_start (≈8s), then drive."+    else:+        out["_hint"] = "Ready. altium_state for the picture, altium_eval / altium_run_command to act."+    return out+++def h_status(args):+    r = h_readiness(args)+    r["uptimeSec"] = int(time.time() - _boot_at)+    r["windows"] = altium_detect.altium_windows()+    r["dialogs"] = dialogs.scan()+    return r+++def h_diagnostics(args):+    env = {k: os.environ.get(k) for k in ("LOCALAPPDATA", "APPDATA", "USERPROFILE", "ProgramFiles", "ADOM_BIND_HOST", "ADOM_DIRECT_API_URL")}+    return {"success": True, "bridgeVersion": BRIDGE_VERSION, "python": sys.version, "env": env,+            "installs": altium_detect.detect_installs(), "pids": altium_detect.running_pids(),+            "windows": altium_detect.altium_windows(), "agent": agent.status(),+            "cacheDir": str(agent.cache_dir()), "abCallbackAvailable": ad_client.available()}+++def h_window_info(args):+    wins = altium_detect.altium_windows()+    dl = dialogs.scan()+    out = {"success": True, "windows": wins, "dialogs": dl, "hasModalDialogs": any(d["ownedByMainInstance"] for d in dl)}+    if out["hasModalDialogs"]:+        out["_hint"] = ("A dialog is open on Altium's main window: the agent loop and every -R request are "+                        "frozen behind it. Read dialogs[].body + category, then altium_dismiss_dialogs "+                        "{hwnd, button} with the work-preserving choice.")+    return out+++def _attach_dialog_shots(dl: list) -> None:+    """Altium's TMessageForm paints its text (no Static child, not in UIA) — the+    only way to READ a dialog is to look at it. Attach a base64 PNG per dialog."""+    if not ad_client.available():+        return+    for d in dl:+        r = caller_identity.ad_callback("desktop_screenshot_base64", {"hwnd": d["hwnd"]})+        if isinstance(r, dict) and (r.get("image") or r.get("base64")):+            d["shot"] = r.get("image") or r.get("base64")+++def h_check_dialogs(args):+    dl = dialogs.scan()+    if args.get("screenshots", True):+        _attach_dialog_shots(dl)+    return {"success": True, "dialogsDetected": len(dl), "dialogs": dl,+            "_hint": "ANALYZE before acting: category/safeAction are suggestions; a save prompt is never auto-answered."+            if dl else "No Altium dialogs open."}+++def h_dismiss_dialogs(args):+    hwnd = args.get("hwnd")+    button = args.get("button")+    only_safe = bool(args.get("safeOnly", False))+    dl = dialogs.scan()+    done, skipped = [], []+    for d in dl:+        if hwnd and d["hwnd"] != int(hwnd):+            continue+        b = button or d.get("safeAction")+        if not b:+            skipped.append({"hwnd": d["hwnd"], "title": d["title"], "why": "no safe default — pass button explicitly"})+            continue+        if only_safe and not d.get("safeAction"):+            skipped.append({"hwnd": d["hwnd"], "title": d["title"], "why": "not safe"})+            continue+        r = dialogs.press(d["hwnd"], b)+        r.update({"title": d["title"], "category": d["category"], "body": d["body"][:200]})+        done.append(r)+    time.sleep(0.4)+    remaining = dialogs.scan()+    return {"success": True, "dismissed": done, "skipped": skipped, "remaining": remaining,+            "_hint": "Verify by altium_window_info; if a dialog persists, ab's desktop_ui_click {hwnd,name:'OK'} is the UIA fallback."}+++def h_list_commands(args):+    inst = _install()+    if not inst:+        return _not_installed()+    q = args.get("query", "")+    r = rcs_catalog.search(inst["system_dir"], q, args.get("server"), int(args.get("limit", 50)))+    r["success"] = True+    r["_hint"] = ("Run one with altium_run_command {command:'PCB:Zoom', params:'Action=All'} (goes through "+                  "the agent). Params are Name=Value|Name=Value.")+    return r+++# ---- lifecycle ------------------------------------------------------------------++def h_launch(args):+    inst = _install()+    if not inst:+        return _not_installed()+    lic = altium_detect.licence_state()+    if lic.get("running"):+        return {"success": True, "alreadyRunning": True, "licence": lic, "windowEtiquette": "background",+                "_hint": "Already running — nothing launched."}+    t0 = time.time()+    progress.begin("cold_start", "cold", "launching Altium Designer")+    try:+        subprocess.Popen([inst["exe"]], creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))+    except Exception as e:+        progress.end("cold_start")+        return {"success": False, "error": f"launch failed: {e}"}+    wait = float(args.get("waitSec", 180))+    while time.time() - t0 < wait:+        lic = altium_detect.licence_state()+        if lic.get("running"):+            progress.finish("cold_start", t0, "cold", True)+            return {"success": True, "startedInSec": round(time.time() - t0, 1), "licence": lic,+                    "windowEtiquette": "foreground",+                    "_hint": "Altium's own launch foregrounds itself (the one legitimate foreground). "+                             "Next: altium_readiness; if unlicensed, altium_license."}+        time.sleep(1.0)+    progress.finish("cold_start", t0, "cold", False)+    return {"success": False, "errorCode": "launch_timeout", "error": f"no Altium main window after {wait}s"}+++def h_close(args):+    lic = altium_detect.licence_state()+    if not lic.get("running"):+        return {"success": True, "wasRunning": False}+    if agent.is_alive():+        agent.stop()+    # polite first: WM_CLOSE on the main window (prompts for unsaved docs → dialogs), then taskkill+    import ctypes+    ctypes.windll.user32.PostMessageW(lic["hwnd"], 0x0010, 0, 0)+    t0 = time.time()+    while time.time() - t0 < float(args.get("waitSec", 20)):+        if not altium_detect.running_pids():+            return {"success": True, "closedInSec": round(time.time() - t0, 1)}+        dl = dialogs.scan(main_pid_only=True)+        if dl:+            return {"success": False, "errorCode": "close_blocked_by_dialog", "dialogs": dl,+                    "_hint": "Altium asked something on close (save?). Read dialogs[] and answer via altium_dismiss_dialogs {hwnd,button}."}+        time.sleep(0.5)+    if args.get("force"):+        subprocess.run(["taskkill", "/F", "/IM", altium_detect.EXE_NAME], capture_output=True)+        return {"success": True, "forced": True}+    return {"success": False, "errorCode": "close_timeout", "_hint": "pass force:true to taskkill"}+++# ---- agent / REPL ---------------------------------------------------------------++def _need_licensed():+    lic = altium_detect.licence_state()+    if not lic.get("running"):+        return {"success": False, "errorCode": "altium_not_running", "_hint": "altium_launch first."}+    if lic.get("licensed") is False:+        return {"success": False, "errorCode": "not_licensed", "licenceState": lic.get("state"),+                "_hint": "Altium refuses scripts without a licence. altium_license '{}'."}+    dl = dialogs.scan(main_pid_only=True)+    if dl:+        _attach_dialog_shots(dl)+        return {"success": False, "errorCode": "blocked_by_dialog", "dialogs": dl,+                "_hint": "A modal on the main window freezes scripting. Read dialogs[].shot (base64 PNG — the text is painted, not exposed), altium_dismiss_dialogs {hwnd, button}, retry. If it is a script error, altium_agent_recover."}+    return None+++def h_agent_start(args):+    g = _need_licensed()+    if g:+        return g+    t0 = time.time()+    r = agent.start(float(args.get("waitSec", 45)))+    progress.record("agent_start", time.time() - t0, "warm")+    if r.get("success"):+        r["_hint"] = "Agent resident. altium_eval / altium_run_command / altium_state now go through it (~ms). Do NOT use altium_run_script while it is up."+    return r+++def h_agent_stop(args):+    return agent.stop()+++def h_agent_status(args):+    s = agent.status()+    s["success"] = True+    return s+++def h_agent_recover(args):+    try:+        agent.call("tx_clear", {}, timeout=30)+    except Exception:+        pass+    return agent.recover(args.get("reason", "requested"))+++def h_eval(args):+    g = _need_licensed()+    if g:+        return g+    text = args.get("text") or args.get("code")+    if not text:+        return {"success": False, "error": "text is required (DelphiScript STATEMENTS: Begin ... End;)"}+    if not agent.is_alive():+        # not resident → the -R RunScriptText lane is available and simplest+        r = agent.run_script_text_lane(text, float(args.get("timeout", 30)))+        r["lane"] = "cli_run_script_text"+        r.setdefault("_hint", "Ran via X2 -RScriptingSystem:RunScriptText (statements only). Errors surface as an 'Error'/'X2' modal — altium_check_dialogs.")+        return r+    r = agent.call("eval", {}, payload=text, timeout=float(args.get("timeout", 30)))+    r["lane"] = "agent"+    return r+++def h_run_command(args):+    g = _need_licensed()+    if g:+        return g+    cmd = args.get("command") or args.get("process")+    if not cmd or ":" not in cmd:+        return {"success": False, "error": "command must be Server:Command, e.g. PCB:Zoom", "_hint": "altium_list_commands {query:'zoom'}"}+    params = args.get("params") or ""+    if isinstance(params, dict):+        params = "|".join(f"{k}={v}" for k, v in params.items())+    return agent.call("run_process", {"process": cmd, "params": params}, timeout=float(args.get("timeout", 60)))+++def h_run_script(args):+    """One-shot: run ProcName in a .PrjScr through the -R lane (agent must NOT be resident)."""+    g = _need_licensed()+    if g:+        return g+    prj = args.get("prjscr")+    proc = args.get("proc") or "Main"+    if not prj:+        return {"success": False, "error": "prjscr (absolute path to a .PrjScr on this machine) is required"}+    return agent.run_script_lane(proc, prj, float(args.get("timeout", 60)))+++def h_state(args):+    """Screenshot ARRAY of every Altium window + logical state from the agent."""+    wins = altium_detect.altium_windows()+    shots = []+    if args.get("screenshots", True) and ad_client.available():+        for w in wins:+            if w["kind"] in ("main", "dialog", "script_error_modal"):+                r = caller_identity.ad_callback("desktop_screenshot_base64", {"hwnd": w["hwnd"]})+                if isinstance(r, dict) and (r.get("image") or r.get("base64")):+                    shots.append({"title": w["title"], "hwnd": w["hwnd"], "kind": w["kind"],+                                  "image": r.get("image") or r.get("base64"), "width": r.get("width"), "height": r.get("height")})+    logical = None+    if agent.is_alive():+        logical = agent.call("state", {}, timeout=15, autostart=False)+    lic = altium_detect.licence_state()+    return {"success": True, "licence": lic, "windows": wins, "dialogs": dialogs.scan(), "shots": shots,+            "logical": logical, "focusEvents": [], "agentAlive": agent.is_alive(),+            "drivingBadge": {"active": __import__("handlers.taskbar", fromlist=["x"]).badge_active(), "lastError": __import__("handlers.taskbar", fromlist=["x"]).last_error()},+            "_hint": "shots[] are base64 PNGs of every Altium window (render inline). logical.data.documents lists open docs (agent required)."}+++def h_open_document(args):+    g = _need_licensed()+    if g:+        return g+    path = args.get("filePath") or args.get("path")+    if not path:+        return {"success": False, "error": "filePath is required (absolute Windows path)"}+    r = agent.call("open", {"path": path}, timeout=float(args.get("timeout", 60)))+    r["windowEtiquette"] = "background"+    return r+++def h_progress(args):+    return {"success": True, "active": progress.live(mine_only=False), "history": progress.history()}+++def h_uninstall(args):+    removed = []+    if agent.is_alive():+        agent.stop()+    import shutil+    d = agent.cache_dir()+    if d.exists() and not args.get("dryRun"):+        shutil.rmtree(d, ignore_errors=True)+        removed.append(str(d))+    return {"success": True, "removed": removed, "dryRun": bool(args.get("dryRun")),+            "note": "Altium Designer itself and the user's designs are never touched."}+++# ---- the eight surfaces (phase 3) ---------------------------------------------------++EXAMPLES_DIR = r"C:\Users\Public\Documents\Altium\AD26\Examples"+DEMO_PROJECT = EXAMPLES_DIR + r"\Bluetooth Sentinel\Bluetooth_Sentinel.PrjPcb"+DEMO_PCB = EXAMPLES_DIR + r"\Bluetooth Sentinel\Bluetooth_Sentinel.PcbDoc"+DEMO_SCH = EXAMPLES_DIR + r"\Bluetooth Sentinel\Bluetooth_Sentinel.SchDoc"+++def _shot_of_main() -> dict | None:+    mw = altium_detect.main_window()+    if not mw or not ad_client.available():+        return None+    r = caller_identity.ad_callback("desktop_screenshot_base64", {"hwnd": mw["hwnd"]})+    if isinstance(r, dict) and (r.get("image") or r.get("base64")):+        return {"title": mw["title"], "hwnd": mw["hwnd"], "image": r.get("image") or r.get("base64"),+                "width": r.get("width"), "height": r.get("height")}+    return None+++def _open_then(path: str, commands: list[tuple[str, str]], timeout: float, label: str) -> dict:+    """Open a document through the agent, run follow-up processes, return the frame."""+    g = _need_licensed()+    if g:+        return g+    t0 = time.time()+    steps = []+    r = agent.call("open", {"path": path}, timeout=timeout)+    steps.append({"open": path, "result": r.get("data"), "ms": r.get("roundtripMs")})+    if not r.get("success"):+        r["steps"] = steps+        return r+    for cmd, params in commands:+        rr = agent.call("run_process", {"process": cmd, "params": params}, timeout=timeout)+        steps.append({"process": cmd, "params": params, "ok": rr.get("success"), "ms": rr.get("roundtripMs")})+    time.sleep(0.6)+    dl = dialogs.scan(main_pid_only=True)+    shot = _shot_of_main()+    mw = altium_detect.main_window() or {}+    ok = not dl+    return {"success": ok, "surface": label, "path": path, "steps": steps,+            "title": mw.get("title"), "document": mw.get("document"),+            "dialogsDetected": len(dl), "dialogs": dl, "shot": shot,+            "windowEtiquette": "background", "elapsedSec": round(time.time() - t0, 1),+            "_hint": ("A dialog came up — read dialogs[] before doing anything else." if dl else+                      f"{label} is on screen (title: {mw.get('title')}). shot.image is the frame; altium_state for every window.")}+++def h_show_project(args):+    return _open_then(args.get("filePath") or DEMO_PROJECT, [], float(args.get("timeout", 60)), "project")+++def h_show_schematic(args):+    return _open_then(args.get("filePath") or DEMO_SCH, [("Sch:Zoom", "Action=All")], float(args.get("timeout", 60)), "schematic")+++def h_show_2d_board(args):+    return _open_then(args.get("filePath") or DEMO_PCB,+                      [("PCB:SwitchTo2D", ""), ("PCB:Zoom", "Action=All")], float(args.get("timeout", 120)), "2d_board")+++def h_show_3d_board(args):+    return _open_then(args.get("filePath") or DEMO_PCB,+                      [("PCB:SwitchTo3D", ""), ("PCB:Zoom", "Action=All")], float(args.get("timeout", 120)), "3d_board")+++DEMO_SCHLIB = EXAMPLES_DIR + r"\SpiritLevel-SL1\Libraries\SpiritLevel_2E.SchLib"+DEMO_PCBLIB = EXAMPLES_DIR + r"\SpiritLevel-SL1\Libraries\Spirit_Level_Project.PcbLib"+++def _lib_surface(args, kind: str, default_path: str, label: str, after: list[tuple[str, str]]):+    g = _need_licensed()+    if g:+        return g+    t0 = time.time()+    path = args.get("libraryPath") or args.get("filePath") or default_path+    name = args.get("symbolName") or args.get("footprintName") or args.get("name") or ""+    steps = []+    r = agent.call("open", {"path": path}, timeout=float(args.get("timeout", 60)))+    steps.append({"open": path, "result": r.get("data"), "ms": r.get("roundtripMs")})+    if not r.get("success"):+        r["steps"] = steps+        return r+    sel = agent.call("lib_select", {"kind": kind, "name": name}, timeout=float(args.get("timeout", 60)))+    steps.append({"lib_select": name or "(first)", "result": sel.get("data"), "ms": sel.get("roundtripMs")})+    if not sel.get("success"):+        lst = agent.call("lib_list", {"kind": kind, "limit": 50}, timeout=30)+        sel["steps"] = steps+        sel["available"] = (lst.get("data") or {}).get("names")+        sel["_hint"] = "Component not found in that library — pick one of available[] (altium_list_library shows all)."+        return sel+    for cmd, params in after:+        rr = agent.call("run_process", {"process": cmd, "params": params}, timeout=60)+        steps.append({"process": cmd, "params": params, "ok": rr.get("success"), "ms": rr.get("roundtripMs")})+    time.sleep(0.6)+    dl = dialogs.scan(main_pid_only=True)+    _attach_dialog_shots(dl)+    shot = _shot_of_main()+    mw = altium_detect.main_window() or {}+    return {"success": not dl, "surface": label, "path": path, "selected": (sel.get("data") or {}).get("selected"),+            "steps": steps, "title": mw.get("title"), "dialogsDetected": len(dl), "dialogs": dl, "shot": shot,+            "windowEtiquette": "background", "elapsedSec": round(time.time() - t0, 1),+            "_hint": f"{label}: {(sel.get('data') or {}).get('selected')} in {mw.get('document')}. shot.image is the frame."}+++def h_show_library(args):+    kind = "pcb" if str(args.get("libraryPath") or args.get("filePath") or "").lower().endswith("pcblib") else "sch"+    path = args.get("libraryPath") or args.get("filePath") or DEMO_SCHLIB+    r = _lib_surface({**args, "libraryPath": path, "name": ""}, kind, path, "library", [])+    if r.get("success"):+        lst = agent.call("lib_list", {"kind": kind, "limit": 200}, timeout=30)+        r["components"] = (lst.get("data") or {}).get("names")+        r["componentCount"] = (lst.get("data") or {}).get("count")+    return r+++def h_list_library(args):+    kind = "pcb" if str(args.get("libraryPath") or args.get("filePath") or "").lower().endswith("pcblib") else "sch"+    path = args.get("libraryPath") or args.get("filePath") or DEMO_SCHLIB+    g = _need_licensed()+    if g:+        return g+    r = agent.call("open", {"path": path}, timeout=60)+    if not r.get("success"):+        return r+    lst = agent.call("lib_list", {"kind": kind, "limit": int(args.get("limit", 500))}, timeout=60)+    lst["path"] = path+    lst["kind"] = kind+    return lst+++def h_show_symbol(args):+    return _lib_surface(args, "sch", DEMO_SCHLIB, "symbol", [])+++DEMO_FOOTPRINT = "SO-G3"     # a real 3-pin SOT in the SpiritLevel PcbLib; the first entry ("NT") is a 2-pad test point+++def h_show_footprint(args):+    a = dict(args)+    a.setdefault("footprintName", DEMO_FOOTPRINT if not (args.get("libraryPath") or args.get("filePath")) else "")+    return _lib_surface(a, "pcb", DEMO_PCBLIB, "footprint", [("PCB:SwitchTo2D", ""), ("PCB:Zoom", "Action=All")])+++def h_show_3d_chip(args):+    a = dict(args)+    a.setdefault("footprintName", DEMO_FOOTPRINT if not (args.get("libraryPath") or args.get("filePath")) else "")+    return _lib_surface(a, "pcb", DEMO_PCBLIB, "3d_chip", [("PCB:SwitchTo3D", ""), ("PCB:Zoom", "Action=All")])+++_DEMO_BEATS = [+    ("symbol", "show_symbol", "A schematic symbol from the SpiritLevel example library, in the Symbol editor."),+    ("footprint", "show_footprint", "Its land pattern in the footprint editor: pads, silkscreen, courtyard."),+    ("3d_chip", "show_3d_chip", "The same footprint rendered in 3D."),+    ("schematic", "show_schematic", "The Bluetooth Sentinel top-level schematic — six sheets wired by ports."),+    ("2d_board", "show_2d_board", "The layout in 2D: a two-board rigid-flex, four copper layers."),+    ("3d_board", "show_3d_board", "The populated board in 3D."),+]+++def h_demo(args):+    """Same shape as kicad_demo/fusion_demo: one beat per call, `done` when finished."""+    stage = int(args.get("stage", 0))+    if stage >= len(_DEMO_BEATS):+        return {"success": True, "done": True, "stage": stage, "narrate": "That is the whole tour: symbol, footprint, 3D part, schematic, 2D and 3D board — all driven through the resident agent while Altium stayed in the background.", "_hint": "Tour complete."}+    label, verb, narrate = _DEMO_BEATS[stage]+    r = COMMAND_HANDLERS[verb]({"timeout": 120})+    shot = r.get("shot")+    return {"success": bool(r.get("success")), "done": False, "stage": stage, "label": label, "narrate": narrate,+            "screenshots": [{"label": label, "image": shot.get("image")}] if shot else [],+            "steps": r.get("steps"), "dialogs": r.get("dialogs"), "failedSteps": [] if r.get("success") else [label],+            "nextCall": {"verb": "altium_demo", "args": {"stage": stage + 1}},+            "_hint": f"Beat {stage + 1}/{len(_DEMO_BEATS)} ({label}). Narrate, show the screenshot, then call altium_demo {{stage:{stage + 1}}}."}+++# ---- outputs (phase 4) ---------------------------------------------------------++DEMO_OUTJOB_FAB = EXAMPLES_DIR + r"\Bluetooth Sentinel\Fabrication.OutJob"+DEMO_OUTJOB_ASM = EXAMPLES_DIR + r"\Bluetooth Sentinel\Assembly.OutJob"+++def _ensure_project(args, default=DEMO_PROJECT):+    path = args.get("projectPath") or args.get("filePath") or default+    r = agent.call("open", {"path": path}, timeout=float(args.get("timeout", 60)))+    return path, r+++def h_compile(args):+    g = _need_licensed()+    if g:+        return g+    t0 = time.time()+    path, r = _ensure_project(args)+    if not r.get("success"):+        return r+    c = agent.call("compile", {"objectKind": args.get("objectKind", "Project"), "limit": int(args.get("limit", 200))},+                   timeout=float(args.get("timeout", 120)))+    if not c.get("success"):+        return c+    data = c.get("data") or {}+    msgs = data.get("messages") or []+    by_class = {}+    for m in msgs:+        by_class[m.get("class")] = by_class.get(m.get("class"), 0) + 1+    return {"success": True, "project": path, "messageCount": data.get("messageCount"), "byClass": by_class,+            "messages": msgs, "elapsedSec": round(time.time() - t0, 1), "windowEtiquette": "background",+            "_hint": "messages[] is Altium's Messages panel after Compile (ERC + connectivity). Zero messages = clean."}+++h_run_erc = h_compile+++def h_run_drc(args):+    """PCB:DesignRuleCheck opens a MODAL — the agent blocks inside RunProcess until it+    closes. The bridge drives the dialog's 'Run Design Rule Check' via ab UIA in the+    background, then reads the violations off the board through the agent."""+    g = _need_licensed()+    if g:+        return g+    t0 = time.time()+    path = args.get("filePath") or DEMO_PCB+    r = agent.call("open", {"path": path}, timeout=60)+    if not r.get("success"):+        return r+    # fire the process WITHOUT waiting for the agent (the modal freezes the loop)+    try:+        agent.call("run_process", {"process": "PCB:DesignRuleCheck", "params": ""}, timeout=6)+    except Exception:+        pass+    dlg = None+    for _ in range(30):+        wins = [w for w in altium_detect.altium_windows() if w["kind"] != "main" and "rule" in (w["title"] or "").lower()]+        if wins:+            dlg = wins[0]+            break+        time.sleep(0.5)+    steps = [{"dialog": dlg["title"] if dlg else None, "hwnd": dlg["hwnd"] if dlg else None}]+    if not dlg:+        return {"success": False, "errorCode": "drc_dialog_not_found", "steps": steps,+                "_hint": "PCB:DesignRuleCheck did not open its dialog — is a PCB the focused document? altium_window_info."}+    click = caller_identity.ad_callback("desktop_ui_click", {"hwnd": dlg["hwnd"], "contains": "Run Design Rule Check", "role": "button"})+    steps.append({"uiClick": "Run Design Rule Check", "result": click})+    # wait for the agent to come back (the modal closed) and DRC to finish+    wait = float(args.get("timeout", 180))+    t1 = time.time()+    while time.time() - t1 < wait:+        if agent.is_responsive():+            break+        time.sleep(0.5)+    # the DRC report (HTML) opens as a new document and takes focus — put the board back first+    agent.call("open", {"path": path}, timeout=60)+    v = agent.call("violations", {"limit": int(args.get("limit", 200))}, timeout=60)+    data = v.get("data") or {}+    steps.append({"violations": {"success": v.get("success"), "error": data.get("error") or v.get("error"), "ms": v.get("roundtripMs")}})+    dl = dialogs.scan(main_pid_only=True)+    _attach_dialog_shots(dl)+    return {"success": bool(v.get("success")) and not dl, "board": path, "violationCount": data.get("violationCount"),+            "violations": data.get("violations"), "steps": steps, "dialogs": dl,+            "elapsedSec": round(time.time() - t0, 1), "windowEtiquette": "background",+            "_hint": "violations[] are the DRC markers on the board (rule, description, location). A DRC report document may also have opened — altium_state shows it."}+++def h_run_outjob(args):+    g = _need_licensed()+    if g:+        return g+    t0 = time.time()+    outjob = args.get("outjobPath") or args.get("filePath") or DEMO_OUTJOB_FAB+    medium = args.get("medium") or "Folder Structure"+    proj = args.get("projectPath") or (DEMO_PROJECT if outjob.startswith(EXAMPLES_DIR) else None)+    if proj:+        r = agent.call("open", {"path": proj}, timeout=60)+        if not r.get("success"):+            return r+    o = agent.call("outjob", {"path": outjob, "medium": medium, "objectKind": args.get("objectKind", "OutputBatch")},+                   timeout=float(args.get("timeout", 300)))+    dl = dialogs.scan(main_pid_only=True)+    _attach_dialog_shots(dl)+    out_dir = None+    try:+        from pathlib import Path as _P+        base = _P(proj or outjob)+        pdir = base.parent / ("Project Outputs for " + base.stem)+        out_dir = str(pdir) if pdir.exists() else None+        files = sorted(str(p.relative_to(pdir)) for p in pdir.rglob("*") if p.is_file())[:60] if out_dir else []+    except Exception:+        files = []+    return {"success": bool(o.get("success")) and not dl, "outjob": outjob, "medium": medium, "agent": o.get("data"),+            "outputDir": out_dir, "files": files, "dialogs": dl, "elapsedSec": round(time.time() - t0, 1),+            "windowEtiquette": "background",+            "_hint": "files[] is what the OutJob wrote under 'Project Outputs for <project>'. A dialog means a medium needed a decision (printer/PDF path) — read dialogs[].shot."}+++def h_export_gerber(args):+    a = dict(args)+    a.setdefault("outjobPath", DEMO_OUTJOB_FAB)+    a.setdefault("medium", "Folder Structure")+    r = h_run_outjob(a)+    r["surface"] = "gerber"+    return r+++def h_export_bom(args):+    a = dict(args)+    a.setdefault("outjobPath", DEMO_OUTJOB_ASM)+    a.setdefault("medium", "Folder Structure")+    r = h_run_outjob(a)+    r["surface"] = "bom"+    return r+++def h_create_footprint(args):+    g = _need_licensed()+    if g:+        return g+    for k in ("name", "libraryPath"):+        if not args.get(k):+            return {"success": False, "error": f"{k} is required", "_hint": "pads: 'des,x_mil,y_mil,w_mil,h_mil,rect|round,hole_mil;...'"}+    r = agent.call("create_footprint", {"name": args["name"], "libraryPath": args["libraryPath"],+                                         "description": args.get("description", ""), "pads": args.get("pads", "")},+                   timeout=float(args.get("timeout", 90)))+    r["shot"] = _shot_of_main() if r.get("success") else None+    r["windowEtiquette"] = "background"+    r.setdefault("_hint", "Footprint registered in the (new or opened) PcbLib and made current; the library is NOT saved yet — altium_save {filePath} or altium_run_command Client:Save... shot.image shows it.")+    return r+++def h_create_symbol(args):+    g = _need_licensed()+    if g:+        return g+    for k in ("name", "libraryPath"):+        if not args.get(k):+            return {"success": False, "error": f"{k} is required", "_hint": "pins: 'des,name,left|right|top|bottom,offset_mil;...'"}+    r = agent.call("create_symbol", {"name": args["name"], "libraryPath": args["libraryPath"],+                                      "description": args.get("description", ""), "designator": args.get("designator", "U"),+                                      "body_w_mil": args.get("bodyWidthMil", 800), "body_h_mil": args.get("bodyHeightMil", 600),+                                      "pins": args.get("pins", "")}, timeout=float(args.get("timeout", 90)))+    r["shot"] = _shot_of_main() if r.get("success") else None+    r["windowEtiquette"] = "background"+    r.setdefault("_hint", "Symbol added to the SchLib and made current (library unsaved). shot.image shows it.")+    return r+++def h_save(args):+    """Save the focused document (or the one at filePath) — WorkspaceManager:SaveObject."""+    g = _need_licensed()+    if g:+        return g+    path = args.get("filePath")+    if path:+        agent.call("open", {"path": path}, timeout=60)+    agent.call("tx_clear", {}, timeout=30)   # pre-empts "a command is currently active" (save-a-copy Warning)+    r = agent.call("run_process", {"process": "WorkspaceManager:SaveObject", "params": "ObjectKind=FocusedDocument"}, timeout=60)+    r["_hint"] = "Saved the focused document. A 'Save As' dialog means the document never had a path — altium_check_dialogs."+    return r+++# ---- library management (ported from barrett-land/adom-desktop-altium-bridge) -------++def _lib_call(args, cmd, params, open_path=None, timeout=90):+    g = _need_licensed()+    if g:+        return g+    if open_path:+        r = agent.call("open", {"path": open_path}, timeout=60)+        if not r.get("success"):+            return r+    return agent.call(cmd, params, timeout=float(args.get("timeout", timeout)))+++def h_install_library(args):+    if not args.get("libraryPath"):+        return {"success": False, "error": "libraryPath is required (.IntLib/.SchLib/.PcbLib/.DbLib)"}+    r = _lib_call(args, "install_library", {"path": args["libraryPath"]})+    r.setdefault("_hint", "Registered in Altium's Installed Libraries (IntegratedLibraryManager.InstallLibrary).")+    return r+++def h_compile_intlib(args):+    if not args.get("libpkgPath"):+        return {"success": False, "error": "libpkgPath (.LibPkg) is required; output = destination dir"}+    return _lib_call(args, "compile_intlib", {"path": args["libpkgPath"], "output": args.get("output", "")}, timeout=300)+++def h_extract_intlib(args):+    if not args.get("intlibPath"):+        return {"success": False, "error": "intlibPath (.IntLib) is required"}+    r = _lib_call(args, "extract_intlib", {"path": args["intlibPath"]}, timeout=300)+    r.setdefault("_hint", "sources[] are the editable .SchLib/.PcbLib Altium extracted next to the IntLib (now open in the workspace).")+    return r+++def h_set_params(args):+    if not args.get("libraryPath"):+        return {"success": False, "error": "libraryPath (.SchLib) is required", "_hint": "params:'LCSC Part #=C123;Manufacturer=TI' footprint:'SOT-23' comment:'10k'"}+    params = args.get("params", "")+    if isinstance(params, dict):+        params = ";".join(f"{k}={v}" for k, v in params.items())+    r = _lib_call(args, "set_params", {"name": args.get("symbolName", ""), "params": params,+                                       "footprint": args.get("footprint", ""), "comment": args.get("comment", "")},+                  open_path=args["libraryPath"])+    r.setdefault("_hint", "Parameters set on the symbol (unsaved) — altium_save {filePath: libraryPath}. LCSC Part # / Manufacturer Part Number are the JLC/BOM keys.")+    return r+++def h_add_body(args):+    if not args.get("libraryPath"):+        return {"success": False, "error": "libraryPath (.PcbLib) is required", "_hint": "step:'C:/…/part.step' for a real model, else bwMil/bhMil/hMil/soMil for an extruded box"}+    r = _lib_call(args, "add_body", {"name": args.get("footprintName", ""), "step": args.get("step", ""),+                                     "bw_mil": args.get("bwMil", 100), "bh_mil": args.get("bhMil", 100),+                                     "h_mil": args.get("hMil", 40), "so_mil": args.get("soMil", 0)},+                  open_path=args["libraryPath"])+    if r.get("success"):+        # canvas-added bodies land 50000 mil from the pads in the stored frame; realign+        n = agent.call("normalize_bodies", {"name": args.get("footprintName", "")}, timeout=90)+        r["normalized"] = (n.get("data") or {}).get("shifts")+    r["shot"] = _shot_of_main() if r.get("success") else None+    return r+++# ---- the AdomBridge .NET extension (Barrett's) — parameterized processes ---------------++EXT_DIR = Path(__file__).resolve().parent / "extension"+EXT_REQ_DIR = r"C:\Users\Public\AdomAltium"   # SPACE-FREE: Altium's -R parser splits on whitespace+++def _extension_installed() -> bool:+    try:+        import glob as _g+        return bool(_g.glob(r"C:\ProgramData\Altium\Altium Designer*\Extensions\AdomBridge\AdomBridge.dll"))+    except Exception:+        return False+++def h_install_extension(args):+    installer = EXT_DIR / "Install-AdomBridge.ps1"+    if not installer.exists():+        return {"success": False, "error": f"installer missing at {installer}"}+    if altium_detect.running_pids() and not args.get("remove"):+        return {"success": False, "errorCode": "altium_running",+                "error": "Altium must be CLOSED to install/update the extension (it holds AdomBridge.dll open).",+                "_hint": "altium_close, then altium_install_extension, then altium_launch. Put extension/REMOVE-AdomBridge.cmd on the Desktop first — a failed extension load stops Altium starting."}+    cmd = ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(installer), "-Source", str(EXT_DIR / "AdomBridge")]+    if args.get("remove"):+        cmd.append("-Remove")+    p = subprocess.run(cmd, capture_output=True, text=True, timeout=180, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))+    ok = p.returncode == 0 and "denied" not in (p.stderr or "").lower()+    return {"success": ok, "removed": bool(args.get("remove")), "output": (p.stdout or "")[-1200:], "stderr": (p.stderr or "")[-400:],+            "_hint": "Start Altium — extensions load at startup. Then altium_export_step / altium_import_kicad."}+++EXT_STATUS_NOTE = ("MEASURED 2026-08-29 on AD 26.9.1: the AdomBridge server never loads into the RUNNING instance — "+                   "`X2 -RAdomBridge:*` helpers wait on Altium's startup mutex ~65-90 s and then host the server "+                   "themselves, and RunProcess('AdomBridge:Ping') from inside the agent is silently ignored (unknown "+                   "process). It also coincided with a 'Could not start Vault Explorer' warning at startup, so it was "+                   "unregistered again. Keep the verbs; they fail loudly until the extension is rebuilt/verified for AD 26.")+++def _ext_dispatch(process: str, req_lines: list[str], result_file: str, timeout: float) -> dict:+    """X2.EXE -RAdomBridge:<Process>(Req=<file>) with the parameters in a request file at a+    space-free path. The extension writes its result file; only the filesystem is proof."""+    if not _extension_installed():+        return {"success": False, "errorCode": "extension_not_installed",+                "_hint": "altium_install_extension (Altium closed), restart Altium, retry."}+    exe = agent._x2_exe()+    os.makedirs(EXT_REQ_DIR, exist_ok=True)+    req = os.path.join(EXT_REQ_DIR, f"{process.lower()}_req.txt")+    res = os.path.join(EXT_REQ_DIR, result_file)+    try:+        os.remove(res)+    except FileNotFoundError:+        pass+    with open(req, "w", encoding="utf-8") as f:+        f.write("\n".join(req_lines) + "\n")+    p = agent._spawn_helper(exe, f"-RAdomBridge:{process}(Req={req})")+    t0 = time.time()+    while time.time() - t0 < timeout:+        if os.path.exists(res):+            time.sleep(0.3)+            return {"success": True, "result": open(res, encoding="utf-8", errors="replace").read(), "elapsedSec": round(time.time() - t0, 1)}+        time.sleep(0.5)+    try:+        p.kill()+    except Exception:+        pass+    return {"success": False, "errorCode": "extension_timeout", "elapsedSec": round(time.time() - t0, 1),+            "_hint": "No result file. " + EXT_STATUS_NOTE}+++def h_extension_ping(args):+    r = _ext_dispatch("Ping", ["Msg=hello"], "adombridge_ping.txt", 30)+    r["installedOnDisk"] = _extension_installed()+    return r+++def h_export_step(args):+    """Headless STEP with real holes/copper/models — Barrett's AdomBridge:ExportStep."""+    g = _need_licensed()+    if g:+        return g+    pcb = args.get("filePath") or DEMO_PCB+    out = args.get("output") or (os.path.splitext(pcb)[0] + ".step")+    lines = [f"PcbDoc={pcb}", f"Out={out}", f"Holes={args.get('holes', 'All')}",+             f"PadHoles={'true' if args.get('padHoles', True) else 'false'}", f"Copper={args.get('copper', 'None')}",+             f"Models={args.get('models', 'Step')}", f"Folded={'true' if args.get('folded') else 'false'}"]+    r = _ext_dispatch("ExportStep", lines, "adombridge_exportstep.txt", float(args.get("timeout", 300)))+    r["output"] = out+    r["outputExists"] = os.path.exists(out)+    r["outputBytes"] = os.path.getsize(out) if r["outputExists"] else 0+    if r.get("success") and not r["outputExists"]:+        r["success"] = False+        r["errorCode"] = "step_not_written"+    r.setdefault("_hint", "Copper=All bakes traces as solids (30x bigger file); Holes=All gives real drilled geometry.")+    return r+++def h_import_kicad(args):+    if not args.get("filePath"):+        return {"success": False, "error": "filePath (.kicad_sym / .kicad_sch / .kicad_pcb) is required"}+    r = _ext_dispatch("ImportKiCad", [f"Mode={args.get('mode', 'Import')}", f"File={args['filePath']}"], "adombridge_importkicad.txt", float(args.get("timeout", 300)))+    r.setdefault("_hint", "Output lands in 'Imported <stem>' beside the source. Footprints are NOT imported by Altium's KiCad importer — link one with altium_set_params {footprint}.")+    return r+++# ---- phase 5: account / workspace / licence / Altium 365 -------------------------++A365_CFG = None+++def _a365_cfg_path():+    return agent.cache_dir() / "a365.json"+++def _a365_load() -> dict:+    try:+        return json.loads(_a365_cfg_path().read_text())+    except Exception:+        return {}+++def h_a365_configure(args):+    """Store the workspace domain + a Bearer token (created in the workspace web UI under+    Admin → Developer on plans that expose it; the Develop trial workspace has no such page)."""+    cfg = _a365_load()+    for k in ("workspace", "token"):+        if args.get(k):+            cfg[k] = args[k]+    if not cfg.get("workspace"):+        return {"success": False, "error": "workspace is required, e.g. 'john-john-3.365.altium.com'"}+    _a365_cfg_path().write_text(json.dumps(cfg))+    return {"success": True, "workspace": cfg["workspace"], "hasToken": bool(cfg.get("token")),+            "_hint": "altium_a365_status to verify; altium_a365_query {query} to run GraphQL."}+++def h_a365_status(args):+    cfg = _a365_load()+    out = {"success": True, "configured": bool(cfg.get("workspace")), "workspace": cfg.get("workspace"),+           "hasToken": bool(cfg.get("token")), "endpoint": f"https://{cfg.get('workspace')}/api/graphql/" if cfg.get("workspace") else None}+    if not cfg.get("token"):+        out["_hint"] = ("No token. Tokens are minted in the workspace web UI at Admin → Developer (Pro/Enterprise "+                        "workspaces). The Altium Develop TRIAL workspace shows no Developer page (measured 2026-08-29: "+                        "Admin has only Settings / Part Providers / Explorer / Usage and Billing / Extensions), so the "+                        "GraphQL API is not reachable on it. altium_a365_configure {workspace, token} once you have one.")+        return out+    r = h_a365_query({"query": "{ desProjects(first: 1) { totalCount } }"})+    out["tokenLive"] = bool(r.get("success"))+    out["probe"] = r.get("data") or r.get("error")+    return out+++def h_a365_query(args):+    import urllib.request+    cfg = _a365_load()+    if not cfg.get("workspace") or not cfg.get("token"):+        return {"success": False, "errorCode": "a365_not_configured", "_hint": "altium_a365_configure {workspace, token}"}+    q = args.get("query")+    if not q:+        return {"success": False, "error": "query (GraphQL) is required", "_hint": "e.g. { desProjects(first:10){ nodes { name } } }"}+    body = json.dumps({"query": q, "variables": args.get("variables") or {}}).encode()+    req = urllib.request.Request(f"https://{cfg['workspace']}/api/graphql/", data=body,+                                 headers={"Content-Type": "application/json", "Authorization": f"Bearer {cfg['token']}"})+    try:+        with urllib.request.urlopen(req, timeout=float(args.get("timeout", 60))) as resp:+            data = json.loads(resp.read().decode())+    except Exception as e:+        return {"success": False, "errorCode": "a365_http_error", "error": str(e)[:300],+                "_hint": "401 = token expired/invalid (mint a new one); 404 = wrong workspace domain."}+    return {"success": "errors" not in data, "data": data.get("data"), "errors": data.get("errors"),+            "_hint": "Explore the schema at https://<workspace>/api/voyager."}+++def h_signin_status(args):+    """Account + workspace connection as Altium itself reports it."""+    lic = altium_detect.licence_state()+    out = {"success": True, "running": lic.get("running"), "licensed": lic.get("licensed"), "licenceState": lic.get("state")}+    if not lic.get("running"):+        out["_hint"] = "altium_launch first."+        return out+    if agent.is_alive():+        st = agent.call("run_process", {"process": "Client:CurrentUser", "params": ""}, timeout=15, autostart=False)+        out["currentUserProcess"] = st.get("data")+    # A whole-window UIA tree takes >45 s on this box (even scoped), so ask one exact+    # question instead: is there a menu item literally named "Not Connected"?+    r = caller_identity.ad_callback("desktop_find_control", {"hwnd": lic["hwnd"], "name": "Not Connected", "role": "menuitem"}, timeout=25)+    found = False+    if isinstance(r, dict):+        if r.get("success") and (r.get("best") or r.get("control")):+            found = True+        elif isinstance(r.get("output"), str):+            try:+                found = bool(json.loads(r["output"]).get("best"))+            except Exception:+                found = False+    ws = "Not Connected" if found else "(connected: the menu shows the workspace name)"+    out["workspaceMenu"] = ws+    out["workspaceConnected"] = not found+    out["workspace"] = None+    out["_hint"] = ("Signed-in account is on the License Management page (altium_license {action:'show'}). "+                    "'Not Connected' = no Altium 365 workspace attached: altium_connect_workspace opens the picker.")+    return out+++def h_connect_workspace(args):+    """Open Altium's workspace picker (the 'Not Connected' menu). Its list is a custom+    control with no UIA children, so the verb returns a screenshot with coordMap for+    the caller to click, rather than guessing."""+    lic = altium_detect.licence_state()+    if not lic.get("running"):+        return {"success": False, "errorCode": "altium_not_running"}+    caller_identity.ad_callback("desktop_ui_expand", {"hwnd": lic["hwnd"], "name": "Not Connected", "role": "menuitem"})+    pop = []+    for _ in range(8):+        time.sleep(0.4)+        pop = [w for w in altium_detect.altium_windows() if w["className"] == "TWorkspacesForm"]+        if pop:+            break+    if not pop:+        return {"success": False, "errorCode": "picker_not_open", "_hint": "Altium may already be connected (menu text is the workspace name) — altium_signin_status."}+    shot = caller_identity.ad_callback("desktop_screenshot_window", {"hwnd": pop[0]["hwnd"]})+    return {"success": True, "picker": pop[0], "shot": shot, "windowEtiquette": "background",+            "_hint": "The picker lists candidate workspaces; click the wanted row with ab desktop_click {space:'image', shotId, x, y}. It is a popup — it closes on focus loss."}+++LICENSE_PAGE_COORDS = {  # image-space on a 1400x840 safe capture of a maximised main window (measured 2026-08-29)+    "refresh": (1288, 178), "use_license": (1252, 388), "start_trial": (788, 607), "sign_out": (386, 179)}+++def h_license(args):+    """Drive the License Management page. It is a WebView2 — not in UIA — so actions are+    image-space clicks on a fresh capture; every call returns that capture so the caller+    can see the state and click anything else itself."""+    lic = altium_detect.licence_state()+    if not lic.get("running"):+        return {"success": False, "errorCode": "altium_not_running", "_hint": "altium_launch first."}+    action = args.get("action", "show")+    if agent.is_alive():+        agent.call("run_process", {"process": "Client:LicenseSubscriptionNotification", "params": ""}, timeout=15, autostart=False)+    # bring the License Management view up via its process launcher (background)+    agent.call("run_process", {"process": "Client:MyAccount", "params": ""}, timeout=20) if agent.is_alive() else None+    time.sleep(1.0)+    win = caller_identity.ad_callback("desktop_screenshot_window", {"hwnd": lic["hwnd"]}) or {}+    shot = _shot_of_main() or {}+    shot["shotId"] = (win.get("coordMap") or {}).get("shotId") or win.get("shotId")+    shot["coordMap"] = win.get("coordMap")+    out = {"success": True, "licenceState": altium_detect.licence_state().get("state"), "shot": shot, "windowEtiquette": "background"}+    if action in LICENSE_PAGE_COORDS and shot.get("shotId"):+        x, y = LICENSE_PAGE_COORDS[action]+        r = caller_identity.ad_callback("desktop_click", {"space": "image", "shotId": shot["shotId"], "x": x, "y": y, "hwnd": lic["hwnd"]})+        out["clicked"] = {"action": action, "x": x, "y": y, "result": r}+        out["windowEtiquette"] = "foreground"+        time.sleep(2.0)+        out["licenceStateAfter"] = altium_detect.licence_state().get("state")+        out["shotAfter"] = caller_identity.ad_callback("desktop_screenshot_window", {"hwnd": lic["hwnd"]})+    out["_hint"] = ("actions: show | refresh | use_license | start_trial | sign_out. Coordinates were measured on a "+                    "maximised window; verify against shot.image before trusting a click. start_trial opens the browser "+                    "workspace-creation flow (name, terms, Activate) — see the altium-install skill.")+    return out+++def h_place_on_sch(args):+    g = _need_licensed()+    if g:+        return g+    if not args.get("libraryPath"):+        return {"success": False, "error": "libraryPath (.SchLib) required"}+    agent.call("open", {"path": args["libraryPath"]}, timeout=60)+    r = agent.call("place_on_sch", {"sch": args.get("schPath", ""), "libref": args.get("symbolName", "CH340G")}, timeout=float(args.get("timeout", 90)))+    r["shot"] = _shot_of_main() if r.get("success") else None+    r["windowEtiquette"] = "background"+    return r+++def h_place_on_pcb(args):+    g = _need_licensed()+    if g:+        return g+    if not args.get("libraryPath"):+        return {"success": False, "error": "libraryPath (.PcbLib) required"}+    agent.call("open", {"path": args["libraryPath"]}, timeout=60)+    r = agent.call("place_on_pcb", {"pcb": args.get("pcbPath", ""), "footprint": args.get("footprintName", "")}, timeout=float(args.get("timeout", 90)))+    r["shot"] = _shot_of_main() if r.get("success") else None+    r["windowEtiquette"] = "background"+    return r+++def h_pcb_report(args):+    g = _need_licensed()+    if g:+        return g+    if args.get("path"):+        agent.call("open", {"path": args["path"]}, timeout=60)+    return agent.call("pcb_report", {"name": args.get("name", ""), "want": args.get("want", "")}, timeout=float(args.get("timeout", 60)))+++def h_build_footprint(args):+    g = _need_licensed()+    if g:+        return g+    if not args.get("libraryPath") or not args.get("spec"):+        return {"success": False, "error": "libraryPath (.PcbLib) and spec (Windows path to a kicad2altium spec) are required"}+    r = agent.call("build_footprint", {"libraryPath": args["libraryPath"], "spec": args["spec"]}, timeout=float(args.get("timeout", 240)))+    r["shot"] = _shot_of_main() if r.get("success") else None+    r["windowEtiquette"] = "background"+    return r+++def h_build_symbol_modes(args):+    g = _need_licensed()+    if g:+        return g+    if not args.get("libraryPath") or not args.get("spec"):+        return {"success": False, "error": "libraryPath (.SchLib) and spec (Windows path to a symbol_modes spec) are required"}+    r = agent.call("build_symbol_modes", {"libraryPath": args["libraryPath"], "spec": args["spec"]}, timeout=float(args.get("timeout", 240)))+    r["shot"] = _shot_of_main() if r.get("success") else None+    r["windowEtiquette"] = "background"+    return r+++def h_symbol_mode(args):+    g = _need_licensed()+    if g:+        return g+    if args.get("libraryPath"):+        agent.call("open", {"path": args["libraryPath"]}, timeout=60)+    mode = int(args.get("mode", 0))+    # The editor paints its OWN mode: Comp.DisplayMode changes data only (measured+    # 2026-08-30, frames identical). Drive the editor's process: Mode0, then Next x mode.+    agent.call("run_process", {"process": "Sch:ToggleComponentDisplayMode", "params": "Action=Mode0"}, timeout=30)+    for _ in range(mode):+        agent.call("run_process", {"process": "Sch:ToggleComponentDisplayMode", "params": "Action=Next"}, timeout=30)+    r = agent.call("symbol_mode", {"mode": mode}, timeout=float(args.get("timeout", 60)))+    agent.call("run_process", {"process": "Sch:Zoom", "params": "Action=All"}, timeout=30)+    time.sleep(0.8)+    r["shot"] = _shot_of_main() if r.get("success") else None+    r["windowEtiquette"] = "background"+    return r+++def h_add_models(args):+    """Attach N PCB footprint models (Altium's footprint-variant mechanism) + parameters to a SchLib component."""+    g = _need_licensed()+    if g:+        return g+    if not args.get("libraryPath") or not args.get("symbolName"):+        return {"success": False, "error": "libraryPath (.SchLib) and symbolName are required"}+    models = args.get("models") or []   # [{name, pcblib, description}]+    spec = ";".join(f"{m.get('name','')}|{m.get('pcblib','')}|{(m.get('description') or '').replace(';',',').replace('|','/')}" for m in models)+    params = args.get("params") or {}+    pspec = ";".join(f"{k}={str(v).replace(';',',')}" for k, v in params.items())+    r = agent.call("add_models", {"libraryPath": args["libraryPath"], "name": args["symbolName"], "models": spec,+                                  "params": pspec, "comment": args.get("comment", ""), "description": args.get("description", "")},+                   timeout=float(args.get("timeout", 120)))+    r["shot"] = _shot_of_main() if r.get("success") else None+    return r+++def h_normalize_bodies(args):+    g = _need_licensed()+    if g:+        return g+    if args.get("libraryPath"):+        agent.call("open", {"path": args["libraryPath"]}, timeout=60)+    return agent.call("normalize_bodies", {"name": args.get("footprintName", "")}, timeout=float(args.get("timeout", 120)))+++def h_test_board(args):+    g = _need_licensed()+    if g:+        return g+    agent.call("open", {"path": args["libraryPath"]}, timeout=60)+    r = agent.call("test_board", {"footprint": args.get("footprintName", ""), "wMm": args.get("wMm", 15), "hMm": args.get("hMm", 15),+                                  "pcb": args.get("pcbPath", ""), "assigns": args.get("assigns", ""), "stubs": args.get("stubs", "")},+                   timeout=float(args.get("timeout", 240)))+    r["windowEtiquette"] = "background"+    return r+++def h_swap_footprint(args):+    g = _need_licensed()+    if g:+        return g+    if args.get("pcbPath"):+        agent.call("open", {"path": args["pcbPath"]}, timeout=60)+    r = agent.call("swap_footprint", {"footprint": args.get("footprintName", ""), "assigns": args.get("assigns", ""), "libraryPath": args.get("libraryPath", "")},+                   timeout=float(args.get("timeout", 240)))+    r["windowEtiquette"] = "background"+    return r+++def h_board_shape(args):+    g = _need_licensed()+    if g:+        return g+    if args.get("pcbPath"):+        agent.call("open", {"path": args["pcbPath"]}, timeout=60)+    return agent.call("board_shape", {"x0Mm": args.get("x0Mm", 0), "y0Mm": args.get("y0Mm", 0), "wMm": args.get("wMm", 15), "hMm": args.get("hMm", 15)}, timeout=float(args.get("timeout", 120)))+++def h_remove_symbol(args):+    g = _need_licensed()+    if g:+        return g+    if args.get("libraryPath"):+        agent.call("open", {"path": args["libraryPath"]}, timeout=60)+    return agent.call("remove_symbol", {"name": args.get("symbolName", "")}, timeout=float(args.get("timeout", 60)))+++def _sch_lib_verb(args, cmd, params, tkey="timeout", tdef=90):+    g = _need_licensed()+    if g:+        return g+    if args.get("libraryPath"):+        agent.call("open", {"path": args["libraryPath"]}, timeout=60)+    return agent.call(cmd, params, timeout=float(args.get(tkey, tdef)))+++def h_clone_symbol(args):+    return _sch_lib_verb(args, "clone_symbol", {"from": args.get("from", ""), "to": args.get("to", "")})+++def h_rename_symbol(args):+    return _sch_lib_verb(args, "rename_symbol", {"from": args.get("from", ""), "to": args.get("to", "")})+++def h_read_symbol(args):+    return _sch_lib_verb(args, "read_symbol", {"name": args.get("symbolName", "")})+++def h_copy_footprint(args):+    return _sch_lib_verb(args, "copy_footprint", {"from": args.get("from", ""), "to": args.get("to", "")})+++def h_rename_footprint(args):+    return _sch_lib_verb(args, "rename_footprint", {"from": args.get("from", ""), "to": args.get("to", "")})+++def h_remove_footprint(args):+    return _sch_lib_verb(args, "remove_footprint", {"name": args.get("footprintName", "")})+++def h_cleanup_libs(args):+    return _sch_lib_verb(args, "cleanup_libs", {})+++def h_set_visibility(args):+    return _sch_lib_verb(args, "set_visibility", {"name": args.get("symbolName", ""), "param": args.get("param", ""), "visible": "1" if args.get("visible", True) else "0"})+++def h_xcopy_footprint(args):+    g = _need_licensed()+    if g:+        return g+    if args.get("toLib"):+        agent.call("open", {"path": args["toLib"]}, timeout=60)+    return agent.call("xcopy_footprint", {"fromLib": args.get("fromLib", ""), "from": args.get("from", ""), "to": args.get("to", args.get("from", ""))}, timeout=float(args.get("timeout", 120)))+++def h_import_part(args):+    """Barrett's fat verb, on the fast lane: one call imports a whole part.+    Args: schLib, pcbLib, symbolSpec (symbol_modes spec path) OR pins, footprintSpec+    (kicad2altium spec path), step, params{}, comment, description, modelName."""+    g = _need_licensed()+    if g:+        return g+    steps = []+    fp_name = None+    if args.get("footprintSpec"):+        r = agent.call("build_footprint", {"libraryPath": args["pcbLib"], "spec": args["footprintSpec"]}, timeout=240)+        if not r.get("success"):+            return {"success": False, "failedAt": "build_footprint", **r}+        fp_name = (r.get("data") or {}).get("footprint")+        steps.append({"build_footprint": fp_name})+    if args.get("step") and fp_name:+        r = agent.call("add_body", {"libraryPath": args["pcbLib"], "name": fp_name, "step": args["step"],+                                    "bw_mil": 100, "bh_mil": 100, "h_mil": 40, "so_mil": 0}, timeout=180)+        steps.append({"add_body": bool(r.get("success"))})+        agent.call("normalize_bodies", {"name": fp_name}, timeout=90)+    sym_name = args.get("symbolName")+    if args.get("symbolSpec"):+        agent.call("open", {"path": args["schLib"]}, timeout=60)+        r = agent.call("build_symbol_modes", {"libraryPath": args["schLib"], "spec": args["symbolSpec"]}, timeout=240)+        if not r.get("success"):+            return {"success": False, "failedAt": "build_symbol_modes", "steps": steps, **r}+        sym_name = (r.get("data") or {}).get("libref")+        steps.append({"build_symbol_modes": sym_name})+    if sym_name and fp_name:+        r = agent.call("add_models", {"libraryPath": args["schLib"], "name": sym_name,+                                      "models": f"{fp_name}|{args.get('pcbLib','')}|{(args.get('description') or '')[:80]}",+                                      "params": ";".join(f"{k}={v}" for k, v in (args.get("params") or {}).items()),+                                      "comment": args.get("comment", sym_name), "description": args.get("description", "")}, timeout=120)+        steps.append({"add_models": bool(r.get("success"))})+    for lib in (args.get("pcbLib"), args.get("schLib")):+        if lib:+            agent.call("open", {"path": lib}, timeout=60)+            agent.call("run_process", {"process": "WorkspaceManager:SaveObject", "params": "ObjectKind=FocusedDocument"}, timeout=60)+    return {"success": True, "symbol": sym_name, "footprint": fp_name, "steps": steps,+            "_hint": "One-call part import (Barrett's import_part on the resident lane). Verify with altium_read_symbol + altium_pcb_report."}+++def h_install_library_bundle(args):+    """The no-AI web-control lane, Altium edition — mirrors kicad_install_library_bundle.+    Unpacks a staged zip (<Lib>.SchLib + <Lib>.PcbLib [+ <Lib>.IntLib], regardless of a+    wrapping folder), installs it into the user's Installed Libraries, and (capture:true)+    returns a proof frame of the library. Hardened for untrusted zips. The bridge runs on the+    user's Windows box, so unpack is a local zipfile op — no shell, no callback."""+    import os, re, zipfile, base64+    g = _need_licensed()+    if g:+        return g+    zp = args.get("zipPath")+    if not zp:+        return {"success": False, "error": "zipPath is required (stage the bundle with write_file / write_file_chunk first)"}+    name = args.get("libraryName") or ""+    if name and not re.match(r"^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$", name):+        return {"success": False, "error": "libraryName must match ^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$"}+    if not os.path.isfile(zp):+        return {"success": False, "failedAt": "unpack", "error": f"staged zip not found: {zp}"}+    if os.path.getsize(zp) > 268435456:+        return {"success": False, "failedAt": "unpack", "error": "zip over 256MB"}+    dest = os.path.join(os.environ.get("PUBLIC", r"C:\Users\Public"), "AdomAltiumLibs",+                        re.sub(r"[^A-Za-z0-9_.-]", "_", (name or os.path.splitext(os.path.basename(zp))[0])))+    try:+        import shutil+        if os.path.isdir(dest):+            shutil.rmtree(dest, ignore_errors=True)+        os.makedirs(dest, exist_ok=True)+        destroot = os.path.realpath(dest)+        total = 0+        with zipfile.ZipFile(zp) as z:+            for info in z.infolist():+                total += info.file_size+                if total > 536870912:+                    return {"success": False, "failedAt": "unpack", "error": "unpacked over 512MB"}+                nm = info.filename.replace("\\", "/")+                if nm.startswith("/") or ".." in nm.split("/") or (len(nm) > 1 and nm[1] == ":"):+                    return {"success": False, "failedAt": "unpack", "error": f"zip slip: {info.filename}"}+                target = os.path.realpath(os.path.join(dest, *nm.split("/")))+                if not (target == destroot or target.startswith(destroot + os.sep)):+                    return {"success": False, "failedAt": "unpack", "error": f"zip slip: {info.filename}"}+            z.extractall(dest)+    except zipfile.BadZipFile:+        return {"success": False, "failedAt": "unpack", "error": "not a valid zip"}+    libs = []+    for root, _d, files in os.walk(dest):+        for f in files:+            if f.lower().endswith((".schlib", ".pcblib", ".intlib")):+                libs.append(os.path.join(root, f))+    schlibs = [l for l in libs if l.lower().endswith(".schlib")]+    pcblibs = [l for l in libs if l.lower().endswith(".pcblib")]+    intlibs = [l for l in libs if l.lower().endswith(".intlib")]+    if not libs:+        return {"success": False, "failedAt": "unpack", "error": "bundle holds no .SchLib/.PcbLib/.IntLib"}+    installed = []+    for lib in (intlibs or (schlibs + pcblibs)):+        winpath = lib.replace("/", "\\")+        ir = agent.call("install_library", {"path": winpath}, timeout=120)+        installed.append({"lib": os.path.basename(lib), "ok": bool(ir.get("success"))})+    st = agent.call("state", {}, timeout=30) or {}+    running = bool(st.get("windows") or (st.get("data") or {}).get("windows"))+    out = {"success": True, "library": name or os.path.splitext(os.path.basename(zp))[0],+           "symbols": len(schlibs), "footprints": len(pcblibs), "intlibs": len(intlibs),+           "installed": installed, "altiumRunning": running, "restartNeeded": running,+           "_hint": "Registered in Installed Libraries. A running Altium predates the install and "+                    "needs a restart to see the new library — the page/AI tells the user; the verb "+                    "never closes their Altium."}+    if args.get("capture") and schlibs and not running:+        agent.call("show_library", {"path": schlibs[0].replace("/", "\\")}, timeout=90)+        shot = _shot_of_main()+        if shot and shot.get("image"):+            out["thumbnailDataUrl"] = "data:image/png;base64," + shot["image"]+            out["shot"] = shot+        if args.get("closeEditor"):+            agent.call("run_process", {"process": "Client:CloseDocument", "params": ""}, timeout=30)+    elif args.get("capture"):+        out["proofSkipped"] = "Altium was already running; its library tables predate this install."+    return out++def _shell(command, timeout=60):+    """Run a Windows shell command on the box via ab; returns {ok, stdout, error}."""+    try:+        import caller_identity, json as _json+        r = caller_identity.ad_callback("shell_execute", {"command": command, "timeoutSeconds": int(timeout),+                                        "reason": "altium library bundle unpack"}, timeout=timeout + 10)+        if isinstance(r, str):+            try:+                r = _json.loads(r)+            except Exception:+                return {"ok": True, "stdout": r}+        if isinstance(r, dict):+            # ab may nest the CLI envelope under "output" as a JSON string+            if isinstance(r.get("output"), str) and "stdout" not in r:+                try:+                    r = {**r, **_json.loads(r["output"])}+                except Exception:+                    pass+            out = r.get("stdout") or r.get("output") or ""+            return {"ok": r.get("exitCode", 0) == 0 or bool(out), "stdout": out, "error": r.get("stderr") or r.get("error")}+        return {"ok": True, "stdout": str(r)}+    except Exception as e:+        return {"ok": False, "error": str(e)[:200]}+++def _ps(script, timeout=60):+    """Run a PowerShell script on the box (base64 UTF-16LE -EncodedCommand, so quoting is safe)."""+    import base64+    enc = base64.b64encode(script.encode("utf-16-le")).decode()+    return _shell(f"powershell -NoProfile -EncodedCommand {enc}", timeout=timeout)+++def _todo(name, phase):+    def h(args):+        return {"success": False, "errorCode": "not_implemented", "verb": f"altium_{name}",+                "_hint": f"Planned for phase {phase} of the altium-bridge plan. Today: altium_eval / altium_run_command can do it by hand."}+    return h+++_VERB_CATALOG = {+    "readiness": "Read-only: installed / running / licensed / agent alive. Zero side effects.",+    "status": "readiness + windows + dialogs (ab polls this after a timeout).",+    "describe": "This catalog.",+    "diagnostics": "Env, installs, pids, windows, agent state, ab callback availability.",+    "window_info": "Every window owned by X2.exe + open dialogs (by PID, never by title).",+    "state": "Screenshot array of every Altium window (base64) + logical open-document state.",+    "check_dialogs": "Open dialogs with body text, buttons, category and a safe default.",+    "dismiss_dialogs": "Press a dialog button in the background {hwnd, button}; safeOnly:true limits to catalogued safe defaults.",+    "launch": "Start X2.exe and wait for the main window (~60s cold).",+    "close": "WM_CLOSE the main window; reports a save prompt instead of answering it; force:true taskkills.",+    "agent_start": "Install + bootstrap the resident DelphiScript agent via X2 -R RunScript; waits for heartbeat.",+    "agent_stop": "Drop the stop file; loop exits.",+    "agent_status": "Heartbeat age, request count, workspace.",+    "agent_recover": "Un-halt the script engine: press OK on the script Error modal, X2 -REditScript:Stop, clear stale files.",+    "eval": "Run DelphiScript STATEMENTS (Begin…End;). Through the agent when resident, else X2 -R RunScriptText.",+    "run_command": "RunProcess any of the 1,109 Server:Command entries {command, params:'A=1|B=2'}.",+    "run_script": "One-shot RunScript {prjscr, proc} via X2 -R — only when the agent is NOT resident.",+    "list_commands": "Search the process-launcher catalog parsed from Altium's .rcs files {query, server}.",+    "open_document": "Open a document/project by absolute path through the agent (background).",+    "progress": "Live measured progress blocks + per-phase history.",+    "uninstall": "Remove everything this bridge created (agent workspace, caches).",+    "show_project": "Open a .PrjPcb (default: the shipped Bluetooth Sentinel example) in the background and return a frame.",+    "show_schematic": "Open a .SchDoc, Sch:Zoom All, return a frame.",+    "show_2d_board": "Open a .PcbDoc, PCB:SwitchTo2D + Zoom All, return a frame.",+    "show_3d_board": "Open a .PcbDoc, PCB:SwitchTo3D + Zoom All, return a frame.",+    "show_library": "Open a .SchLib/.PcbLib (default: SpiritLevel example) and list its components.",+    "show_symbol": "Open a .SchLib and make {symbolName} current (first if omitted); zoom all; frame.",+    "show_footprint": "Open a .PcbLib and make {footprintName} current in 2D; frame.",+    "show_3d_chip": "Open a .PcbLib, make {footprintName} current, switch to 3D; frame.",+    "list_library": "Component names in a .SchLib/.PcbLib {libraryPath}.",+    "demo": "Six-beat tour (symbol → footprint → 3D part → schematic → 2D → 3D board), one beat per call: {stage}.",+    "compile": "Compile the project (ERC) and return the Messages panel {projectPath}.",+    "run_erc": "Alias of compile.",+    "run_drc": "Open a .PcbDoc, run PCB:DesignRuleCheck (dialog driven via UIA), return violations[].",+    "run_outjob": "Run an OutJob medium {outjobPath, medium:'Folder Structure', projectPath}; lists the files written.",+    "export_gerber": "Fabrication.OutJob → Folder Structure (Gerber/NC drill/ODB++ as configured).",+    "export_bom": "Assembly.OutJob → Folder Structure (BOM, pick-and-place, test points).",+    "create_footprint": "Create a footprint in a .PcbLib (created if missing): {name, libraryPath, pads:'des,x,y,w,h,rect|round,hole;...'} (mils).",+    "create_symbol": "Create a schematic symbol in a .SchLib (created if missing): {name, libraryPath, designator, bodyWidthMil, bodyHeightMil, pins:'des,name,side,offset;...'}.",+    "save": "Save the focused document (WorkspaceManager:SaveObject) {filePath}.",+    "install_library": "Register a .IntLib/.SchLib/.PcbLib/.DbLib in Installed Libraries {libraryPath}.",+    "compile_intlib": "Compile a .LibPkg to .IntLib {libpkgPath, output}.",+    "extract_intlib": "Extract editable SchLib/PcbLib sources from a .IntLib {intlibPath}.",+    "set_params": "Set parameters / comment / footprint link on a SchLib symbol {libraryPath, symbolName, params, footprint, comment}.",+    "add_body": "Add a 3D body to a PcbLib footprint {libraryPath, footprintName, step | bwMil,bhMil,hMil,soMil}.",+    "install_extension": "Install the AdomBridge .NET extension (Altium CLOSED) — enables export_step / import_kicad.",+    "extension_ping": "Prove the extension is loaded (X2 -RAdomBridge:Ping).",+    "export_step": "Headless STEP export with real holes/copper/models {filePath, output, holes, copper, models}.",+    "import_kicad": "Import a KiCad file with no wizard {filePath, mode:'Import'|'List'}.",+    "a365_configure": "Store workspace domain + Bearer token for the Altium 365 GraphQL API {workspace, token}.",+    "a365_status": "Is A365 configured, is the token live (probes desProjects).",+    "a365_query": "Run a GraphQL query against https://<workspace>/api/graphql/ {query, variables}.",+    "signin_status": "Account/licence/workspace-connection state as Altium reports it.",+    "connect_workspace": "Open the Not Connected → workspace picker and return its screenshot.",+    "license": "Show/drive the License Management page {action: show|refresh|use_license|start_trial|sign_out} (WebView2 → image clicks).",+}++COMMAND_HANDLERS = {+    "readiness": h_readiness, "status": h_status, "diagnostics": h_diagnostics,+    "window_info": h_window_info, "state": h_state, "check_dialogs": h_check_dialogs,+    "dismiss_dialogs": h_dismiss_dialogs, "launch": h_launch, "close": h_close,+    "agent_start": h_agent_start, "agent_stop": h_agent_stop, "agent_status": h_agent_status,+    "agent_recover": h_agent_recover,+    "eval": h_eval, "run_command": h_run_command, "run_script": h_run_script,+    "list_commands": h_list_commands, "open_document": h_open_document, "open_project": h_open_document,+    "progress": h_progress, "uninstall": h_uninstall,+    "show_project": h_show_project, "show_schematic": h_show_schematic,+    "show_2d_board": h_show_2d_board, "show_3d_board": h_show_3d_board,+    "show_library": h_show_library, "show_symbol": h_show_symbol, "show_footprint": h_show_footprint,+    "show_3d_chip": h_show_3d_chip, "list_library": h_list_library, "demo": h_demo,+    "compile": h_compile, "run_erc": h_run_erc, "run_drc": h_run_drc, "run_outjob": h_run_outjob,+    "export_gerber": h_export_gerber, "export_bom": h_export_bom,+    "create_footprint": h_create_footprint, "create_symbol": h_create_symbol, "save": h_save,+    "install_library": h_install_library, "install_library_bundle": h_install_library_bundle, "compile_intlib": h_compile_intlib, "extract_intlib": h_extract_intlib,+    "set_params": h_set_params, "add_body": h_add_body,+    "place_on_sch": h_place_on_sch, "place_on_pcb": h_place_on_pcb, "pcb_report": h_pcb_report, "build_footprint": h_build_footprint, "build_symbol_modes": h_build_symbol_modes, "add_models": h_add_models, "remove_symbol": h_remove_symbol, "clone_symbol": h_clone_symbol, "rename_symbol": h_rename_symbol, "read_symbol": h_read_symbol, "copy_footprint": h_copy_footprint, "rename_footprint": h_rename_footprint, "remove_footprint": h_remove_footprint, "cleanup_libs": h_cleanup_libs, "set_visibility": h_set_visibility, "xcopy_footprint": h_xcopy_footprint, "import_part": h_import_part, "normalize_bodies": h_normalize_bodies, "test_board": h_test_board, "board_shape": h_board_shape, "swap_footprint": h_swap_footprint, "symbol_mode": h_symbol_mode,+    "install_extension": h_install_extension, "extension_ping": h_extension_ping,+    "export_step": h_export_step, "import_kicad": h_import_kicad,+    "a365_configure": h_a365_configure, "a365_status": h_a365_status, "a365_query": h_a365_query,+    "signin_status": h_signin_status, "connect_workspace": h_connect_workspace, "license": h_license,+    "signin": h_signin_status,+}+for _n, _p in (("show_symbol", 3), ("show_footprint", 3), ("show_3d_chip", 3), ("show_library", 3),+               ("show_schematic", 3), ("show_2d_board", 3), ("show_3d_board", 3), ("show_project", 3),+               ("run_outjob", 4), ("run_drc", 4), ("run_erc", 4), ("export_gerber", 4), ("export_step", 4),+               ("export_pdf", 4), ("export_bom", 4), ("create_symbol", 4), ("create_footprint", 4),+               ("list_library", 4), ("signin", 5), ("license", 5), ("a365_status", 5), ("a365_query", 5),+               ("demo", 3), ("close_document", 3), ("save", 3), ("compile", 4), ("screenshot_all", 1)):+    COMMAND_HANDLERS.setdefault(_n, _todo(_n, _p))+++def h_describe(args):+    return {"success": True, "bridgeVersion": BRIDGE_VERSION, "prefix": "altium_",+            "verbs": {f"altium_{k}": v for k, v in _VERB_CATALOG.items()},+            "planned": sorted(f"altium_{k}" for k in COMMAND_HANDLERS if k not in _VERB_CATALOG),+            "_hint": "Start with altium_readiness. Facts: one script at a time; a modal freezes everything; RunScriptText is statements-only."}+++COMMAND_HANDLERS["describe"] = h_describe+++def dispatch_command(command: str, args: dict) -> dict:+    handler = COMMAND_HANDLERS.get(command)+    if handler is None and "_" in command:+        tail = command.split("_", 1)[1]+        if tail in COMMAND_HANDLERS:+            command, handler = tail, COMMAND_HANDLERS[tail]+    if handler is None:+        return {"success": False, "error": f"Unknown command: {command}",+                "_hint": f"Valid: {', '.join(sorted(COMMAND_HANDLERS))}"}+    t0 = time.time()+    driving = command not in ("readiness", "status", "describe", "diagnostics", "window_info", "check_dialogs",+                              "progress", "agent_status", "list_commands", "state")+    stopper = None+    if driving:+        try:+            from handlers import taskbar+            taskbar.set_driving_badge()+            est, conf = progress.estimate(command, "warm")+            if est and est >= 3:+                stopper = taskbar.start_activity_progress(command, est, conf)+        except Exception:+            pass+    ok = True+    try:+        result = handler(args or {})+        ok = bool(isinstance(result, dict) and result.get("success", True))+    finally:+        try:+            progress.log_run(command, time.time() - t0, ok)+        except Exception:+            pass+        if driving:+            try:+                from handlers import taskbar+                if stopper is not None:+                    taskbar.end_activity_progress(command, failed=not ok)+            except Exception:+                pass+    if isinstance(result, dict):+        result.setdefault("bridgeVersion", BRIDGE_VERSION)+    return result+++class Handler(BaseHTTPRequestHandler):+    def do_GET(self):+        if self.path in ("/health", "/status"):+            lic = altium_detect.licence_state()+            inst = _install()+            if not inst:+                led, summary = "yellow", "Altium not detected"+            elif not lic.get("running"):+                led, summary = "yellow", "Altium not running"+            elif lic.get("licensed") is False:+                led, summary = "yellow", f"Altium {lic.get('state')}"+            elif agent.is_alive():+                led, summary = "green", "Altium + agent ready"+            else:+                led, summary = "green", "Altium running (agent idle)"+            self._respond(200, {"status": "ok", "led": led, "summary": summary,+                                "tooltip": f"altium-bridge {BRIDGE_VERSION} - {summary}",+                                "bridgeVersion": BRIDGE_VERSION, "licence": lic})+        else:+            self._respond(404, {"error": "Not found"})++    def do_POST(self):+        if self.path != "/command":+            self._respond(404, {"error": "Not found"})+            return+        n = int(self.headers.get("Content-Length", 0))+        try:+            req = json.loads(self.rfile.read(n) or b"{}")+        except json.JSONDecodeError as e:+            self._respond(400, {"success": False, "error": f"Invalid JSON: {e}"})+            return+        command = req.get("command", "")+        args = req.get("args", {}) or {}+        caller_identity.set_from_request(self.headers, args)+        print(f"[altium] {command}{caller_identity.log_suffix()} args={json.dumps(args)[:200]}")+        try:+            self._respond(200, dispatch_command(command, args))+        except Exception as e:+            traceback.print_exc()+            self._respond(500, {"success": False, "error": f"Internal error: {e}"})+        finally:+            caller_identity.clear()++    def _respond(self, code, data):+        body = json.dumps(data).encode("utf-8")+        self.send_response(code)+        self.send_header("Content-Type", "application/json")+        self.send_header("Content-Length", str(len(body)))+        self.end_headers()+        self.wfile.write(body)++    def log_message(self, fmt, *a):+        pass+++def main():+    try:+        sys.stdout.reconfigure(line_buffering=True)+        sys.stderr.reconfigure(line_buffering=True)+    except Exception:+        pass+    port = 0+    if "--port" in sys.argv:+        port = int(sys.argv[sys.argv.index("--port") + 1])+    host = os.environ.get("ADOM_BIND_HOST", "127.0.0.1")+    srv = ThreadingHTTPServer((host, port), Handler)+    print(f"[altium] bridge {BRIDGE_VERSION} listening on {host}:{srv.server_address[1]}")+    _install()+    print(f"[altium] installs: {_installs}")+    srv.serve_forever()+++if __name__ == "__main__":+    main()