app
Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
← Commit history
v0.1.7: altium_install_library_bundle web-control verb
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()