app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
← Commit history
v0.9.75: naming sweep per adom/definitions — bare AD/HD abbreviations removed everywhere; say Bridge (ab) and Hydrogen (ah)
20 files changed
+700−282
BRIDGE_VERSION+1−1README.md+9−9SKILL.md+8−8adom-bridge-kicad-manifest.json+5−5adom-bridge-kicad-v0.9.75.zipbridge.json+1−1caller_identity.py+17−17handlers/demo.py+2−2handlers/diagnostics.py+193handlers/kicad_ui.py+2−2handlers/software_opengl.py+1−1handlers/uia.py+144handlers/upgrade.py+5−5kicad_detect.py+87−6proc.py+2−2server.py+29−29skills/kicad-bridge-dev/SKILL.md+18−18skills/kicad-bridge-publish/SKILL.md+6−6skills/kicad-interaction/SKILL.md+165−165skills/kicad-tour/SKILL.md+5−5BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.74+0.9.75
README.md+9−9@@ -1,6 +1,6 @@ # Adom Bridge — KiCad Bridge -A **reverse bridge** that lets Adom Bridge (AD) — and the AI driving it — control the user's own+A **reverse bridge** that lets Adom Bridge (ab) — and the AI driving it — control the user's own KiCad on Windows: open and drive every editor, install symbol/footprint libraries, place parts, run DRC/ERC, export manufacturing files, and screenshot any window back to the AI. @@ -70,8 +70,8 @@ window-targeted (UIA / `PostMessage`), so you can keep working while it runs. ## How it fits together -The bridge is a `spawn.kind: python` process AD launches on the user's machine (`entrypoint:-server.py`, `port: 0` — AD picks the port and passes `ADOM_BIND_HOST`). It speaks HTTP+The bridge is a `spawn.kind: python` process Bridge launches on the user's machine (`entrypoint:+server.py`, `port: 0` — Bridge picks the port and passes `ADOM_BIND_HOST`). It speaks HTTP (`POST /command`, `GET /status`) and reaches KiCad through **four control surfaces**, picking the lightest one that can do the job. @@ -130,14 +130,14 @@ plated through-holes. **Rendered on the CPU via the software-OpenGL fallback (be The bridge never tells the user to go download KiCad. `kicad_upgrade` fetches the official installer and runs it silently, picking the scope automatically: -- **Elevated AD** → `/allusers /S` (system-wide, `%ProgramFiles%\KiCad`).-- **Non-elevated AD** → `/currentuser /S` (`%LocalAppData%\Programs\KiCad`, **no UAC prompt**).+- **Elevated Bridge** → `/allusers /S` (system-wide, `%ProgramFiles%\KiCad`).+- **Non-elevated Bridge** → `/currentuser /S` (`%LocalAppData%\Programs\KiCad`, **no UAC prompt**). `kicad_readiness` reports whether KiCad is installed and ready without side effects; the AI routes on it before offering to install. Hard-won install details (all handled for you): - KiCad 10's NsisMultiUser installer **requires** a scope flag — bare `/S` errors `rc=666660`.-- AD's portable Python has **no CA bundle**; the bridge ships `certs/cacert.pem` and uses it for TLS.+- Bridge's portable Python has **no CA bundle**; the bridge ships `certs/cacert.pem` and uses it for TLS. - Downloads are size-checked against `Content-Length` (a truncated installer otherwise fails at NSIS). - `%APPDATA%/kicad/<ver>/` lib tables don't exist until first launch — the bridge **bootstraps** them so `install_library`/`install_footprint` work on a never-opened KiCad.@@ -204,13 +204,13 @@ related verbs, and pitfalls. ## Running / dependencies -AD provisions everything; there is nothing to install by hand.+Bridge provisions everything; there is nothing to install by hand. -- **Python** ≥ 3.11 (AD provisions it) — stdlib only, plus the bundled `certs/cacert.pem`.+- **Python** ≥ 3.11 (Bridge provisions it) — stdlib only, plus the bundled `certs/cacert.pem`. - **KiCad** ≥ 7.0 (host app; `kicad_upgrade` installs it if absent). - **OS:** Windows (macOS/Linux detection stubs exist; the GUI surfaces are Windows-first). -The bridge binds `ADOM_BIND_HOST` on an AD-assigned port — never `0.0.0.0`. Auto-updates via+The bridge binds `ADOM_BIND_HOST` on a Bridge-assigned port — never `0.0.0.0`. Auto-updates via `updateManifestUrl` in `bridge.json`. ---
SKILL.md+8−8@@ -20,21 +20,21 @@ A fresh machine can be missing KiCad, or the reverse-bridge plugin auto-installs | Signal (`errorCode` / field) | What it means | What YOU do | |---|---|---|-| `host_app_not_installed` (generic; `hostApp:"KiCad"`. Legacy alias `kicad_not_installed` in `errorCodeLegacy`) | KiCad (the user's host app) isn't on this machine. AD detects host apps but never auto-installs them. | **Don't send the user to download it.** Offer to install it, then run `kicad_upgrade '{}'` — the agent installs the official build from scratch. Then retry. |+| `host_app_not_installed` (generic; `hostApp:"KiCad"`. Legacy alias `kicad_not_installed` in `errorCodeLegacy`) | KiCad (the user's host app) isn't on this machine. Bridge detects host apps but never auto-installs them. | **Don't send the user to download it.** Offer to install it, then run `kicad_upgrade '{}'` — the agent installs the official build from scratch. Then retry. | | `kicad_version_not_installed` | The `kicadVersion` you asked for isn't installed. | Run `kicad_list_versions '{}'`; retry with an installed version or omit `kicadVersion` to use the newest. | | `pluginAutoInstall` present in a response | Your first `kicad_*` call auto-installed the reverse-bridge plugin into KiCad (idempotent). | Nothing — expected. Subsequent calls are near-zero cost. |-| bridge not responding on the first call | AD is lazy-spawning the bridge (+ ~9s KiCad detection). | Wait a beat and retry; it binds once and stays up. |+| bridge not responding on the first call | Bridge is lazy-spawning the bridge (+ ~9s KiCad detection). | Wait a beat and retry; it binds once and stays up. | **Readiness before you drive:** - `kicad_readiness '{}'` — read-only, zero side effects: is KiCad installed/ready? (`{detected, ready, versionCount, defaultVersion}`.) There's **no prewarm** — KiCad is a host app you install on request via `kicad_upgrade`, not a downloaded runtime asset. - `kicad_describe '{}'` — the full machine-readable verb catalog (the completeness escape hatch; the per-call `_hint`s are what carry you in practice).-- For **"what EDA tools do I have"** across all bridges, use AD's `bridge_readiness` (see below).+- For **"what EDA tools do I have"** across all bridges, use Bridge's `bridge_readiness` (see below). ## Installing KiCad on a fresh machine (hard-won, v0.9.26+) `kicad_upgrade` downloads the official downloads.kicad.org installer and silent-installs it. Two things that WILL bite on a truly fresh machine, both handled for you now: -- **Install scope is mandatory in silent mode.** KiCad 10's installer is NsisMultiUser: bare `/S` exits instantly with **`rc=666660`** ("invalid command-line parameters") and installs nothing. The bridge auto-picks the scope: **`/allusers`** when the AD process is elevated (installs to Program Files), else **`/currentuser`** (installs to `%LOCALAPPDATA%\Programs\KiCad` — **no admin, no UAC prompt at all**). Force it with `kicad_upgrade '{"scope":"user"}'` — that's your answer to *"install KiCad without a UAC prompt."* (Elevation note: on a VM/kiosk the AD process may already be elevated even though nobody clicked UAC — an elevated installer that launches the app hands down its admin token. `kicad_upgrade '{"diagnoseOnly":true}'` reports the real token/UAC facts.)+- **Install scope is mandatory in silent mode.** KiCad 10's installer is NsisMultiUser: bare `/S` exits instantly with **`rc=666660`** ("invalid command-line parameters") and installs nothing. The bridge auto-picks the scope: **`/allusers`** when the Bridge process is elevated (installs to Program Files), else **`/currentuser`** (installs to `%LOCALAPPDATA%\Programs\KiCad` — **no admin, no UAC prompt at all**). Force it with `kicad_upgrade '{"scope":"user"}'` — that's your answer to *"install KiCad without a UAC prompt."* (Elevation note: on a VM/kiosk the Bridge process may already be elevated even though nobody clicked UAC — an elevated installer that launches the app hands down its admin token. `kicad_upgrade '{"diagnoseOnly":true}'` reports the real token/UAC facts.) - **Library installs work before KiCad's first launch.** `%APPDATA%/kicad/<ver>/` (the per-user lib tables) doesn't exist until KiCad's first GUI run, so `kicad_install_library`/`_footprint` used to fail *"config directory not found."* The bridge now bootstraps that config (seeding the lib tables from the install template) automatically — installs just work on a never-launched KiCad. **GPU-less hosts (VM/RDP/server):** KiCad pops a blocking *"Could not use OpenGL"* dialog and the heavy editors may not render. The bridge auto-expires that dialog and switches KiCad to the Cairo software canvas — see **[kicad-interaction](skills/kicad-interaction/SKILL.md)** for the dialog/canvas details.@@ -65,9 +65,9 @@ If you pass a version that's not installed, the bridge returns a hint listing th ## "What EDA tools do I have?" — readiness & host-app detection -**KiCad is the user's HOST APP. Adom Bridge *detects* it — it won't silently auto-install it as a prewarm/dependency** (AD v1.9.47+). But if KiCad isn't found, **do NOT hand the user a manual download** — offer to install it for them and run `kicad_upgrade '{}'`. That verb installs the latest stable from the official downloads.kicad.org installer (silent `/S`, Windows only; may raise a UAC prompt) and **installs from scratch, not just upgrades**. The distinction: AD-core never auto-installs a host app, but a user-requested, agent-run install is exactly right.+**KiCad is the user's HOST APP. Adom Bridge *detects* it — it won't silently auto-install it as a prewarm/dependency** (Bridge v1.9.47+). But if KiCad isn't found, **do NOT hand the user a manual download** — offer to install it for them and run `kicad_upgrade '{}'`. That verb installs the latest stable from the official downloads.kicad.org installer (silent `/S`, Windows only; may raise a UAC prompt) and **installs from scratch, not just upgrades**. The distinction: Bridge-core never auto-installs a host app, but a user-requested, agent-run install is exactly right. -- To answer **"what EDA tools / host apps do I have?"** across every bridge, call the AD-level verb **`adom-desktop bridge_readiness '{}'`** — AD aggregates each bridge's host-app detection (KiCad, Fusion 360, …) into one report. Start here for any "do I have X installed?" question.+- To answer **"what EDA tools / host apps do I have?"** across every bridge, call the Bridge-level verb **`adom-desktop bridge_readiness '{}'`** — Bridge aggregates each bridge's host-app detection (KiCad, Fusion 360, …) into one report. Start here for any "do I have X installed?" question. - For **KiCad specifically**, `adom-desktop kicad_readiness '{}'` is a zero-side-effect read-only probe: `{hostApp, detected, ready, versionCount, defaultVersion, versions[]}`. It does not install the plugin or spawn subprocesses, and works even when KiCad is absent (`detected:false`). Use `kicad_list_versions` when you need the full per-version detail (paths, IPC API, lib tables). ## Commands@@ -75,7 +75,7 @@ If you pass a version that's not installed, the bridge returns a hint listing th | CLI form | Action | Purpose | Key args | |---|---|---|---| | `kicad_list_versions` | `list_versions` | Enumerate every installed KiCad version with paths and which one is the default | — |-| `kicad_readiness` | `readiness` | Read-only host-app detection: is KiCad installed/ready? Zero side effects (no plugin load, no subprocess); works even when KiCad is absent. For cross-bridge "what do I have", use AD's `bridge_readiness` | — |+| `kicad_readiness` | `readiness` | Read-only host-app detection: is KiCad installed/ready? Zero side effects (no plugin load, no subprocess); works even when KiCad is absent. For cross-bridge "what do I have", use Bridge's `bridge_readiness` | — | | `kicad_check_for_updates` | `check_for_updates` | Read-only: installed KiCad vs latest STABLE (from downloads.kicad.org / KiCad GitLab tags); flags when the install predates the modern IPC/kipy automation API and should be upgraded | — | | `kicad_upgrade` | `upgrade` | Download the **official** KiCad installer from downloads.kicad.org and silent-install it (`/S`) — **not winget** (winget lags the real release). May raise a UAC prompt to approve | optional `version`, `force` | | `kicad_open_schematic` | `open_schematic` | Open a `.kicad_sch` in the Schematic Editor | `filePath`, optional `kicadVersion` |@@ -94,7 +94,7 @@ If you pass a version that's not installed, the bridge returns a hint listing th | `kicad_run_drc` | `run_drc` | Run DRC on the current board, return violations | `filePath`, optional `kicadVersion` | | `kicad_fix_keyboard` | `fix_keyboard` | Workaround stuck-modifier bug after screen lock | — | | `kicad_screenshot_all` | `screenshot_all` | Capture every open KiCad window (lossless PNGs) | — |-| `kicad_send_key` | `send_key` | Single key to an EXACT `hwnd` (e.g. Enter/Escape to a dialog). For modifier CHORDS ("ctrl+s", "alt+3") use AD's `desktop_press_key` — don't route chords here. | `hwnd`, `key` |+| `kicad_send_key` | `send_key` | Single key to an EXACT `hwnd` (e.g. Enter/Escape to a dialog). For modifier CHORDS ("ctrl+s", "alt+3") use Bridge's `desktop_press_key` — don't route chords here. | `hwnd`, `key` | | `kicad_click` | `click` | Send a click at (x, y) in a KiCad window | `hwnd`, `x`, `y` | | `kicad_adom_library_status` | `adom_library_status` | Report whether the Adom shared library is registered | — |
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.74",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.74/adom-bridge-kicad-v0.9.74.zip",- "sha256": "968d98586a21f4221d0900c2b117f0f032c7ec8c44be79afd2780a3a3dd34cec",- "size": 367941,+ "version": "0.9.75",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.75/adom-bridge-kicad-v0.9.75.zip",+ "sha256": "3a64441f363b2c8c504c7531288f41cbd61063ff6f4eb30428c3954947004722",+ "size": 367957, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-09T01:47:18Z",+ "released_at": "2026-08-09T10:29:21Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
adom-bridge-kicad-v0.9.75.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.74",+ "version": "0.9.75", "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.", "homepage": "https://wiki.adom.inc/adom/adom-bridge", "author": "Adom Inc.",
caller_identity.py+17−17@@ -1,12 +1,12 @@-"""Caller provenance — WHO asked (AD 1.9.180 + 1.9.183).+"""Caller provenance — WHO asked (Bridge 1.9.180 + 1.9.183). -A user runs ~20 AI threads against one desktop. AD now stamps three headers on+A user runs ~20 AI threads against one desktop. Bridge now stamps three headers on every relayed request so a bridge can attribute work to the thread that caused it. This module keeps that identity for the life of a request and re-forwards it-on any AD callback the bridge makes while carrying out that request, so the+on any Bridge callback the bridge makes while carrying out that request, so the Activity Log shows `chip-fetcher tab 3 (via kicad)` instead of a nameless bridge. -Trust rule (AD's, and it is not optional): these values are SELF-ASSERTED, like+Trust rule (Bridge's, and it is not optional): these values are SELF-ASSERTED, like `reason`. Use them for logging, UX, window labels and arbitration hints — NEVER for authorization. `if thread == "admin-tab": allow()` is a hole, not a check. """@@ -21,21 +21,21 @@ _current = threading.local() _HEADERS = ("X-Adom-Caller-Thread", "X-Adom-Caller-Container", "X-Adom-Caller-Reason") # What we name ourselves when we forward. Kept generic ("kicad") to match the-# verb prefix AD already knows us by.+# verb prefix Bridge already knows us by. DELEGATE = "kicad" def set_from_request(headers, args) -> dict: """Capture the caller from an incoming relay request. - AD delivers caller identity TWO ways and we must read BOTH:- * INBOUND relay (AD -> bridge): the CLI's --ai-thread flag and any explicit+ Bridge delivers caller identity TWO ways and we must read BOTH:+ * INBOUND relay (Bridge -> bridge): the CLI's --ai-thread flag and any explicit `caller` in args arrive as `args["caller"] = {"aiThread", "containerName", "source"}`. This is what actually shows up in practice (verified on AdomLapper 1.9.183, 2026-07-25) — NOT the X-Adom-Caller-* headers.- * Header form (X-Adom-Caller-*): the shape AD uses on its OWN direct-API+ * Header form (X-Adom-Caller-*): the shape Bridge uses on its OWN direct-API callbacks and what the SDK examples forward. Read it as a fallback so we- also work if a future AD stamps the relay path with headers.+ also work if a future Bridge stamps the relay path with headers. args wins when both are present (it carries the explicit/flag identity). """@@ -66,7 +66,7 @@ def clear() -> None: def thread_name() -> str: """The caller's thread name, or '' if none was supplied (loopback callers —- HD, another bridge, the local CLI — are exempt and send nothing)."""+ Hydrogen, another bridge, the local CLI — are exempt and send nothing).""" ident = getattr(_current, "ident", None) or {} return ident.get("X-Adom-Caller-Thread", "") @@ -78,8 +78,8 @@ def log_suffix() -> str: def forward_headers() -> dict:- """Headers to put on an AD callback made WHILE serving an AI's verb. Echoes- the three we were handed and ADDS X-Adom-Caller-Delegate naming us, so AD can+ """Headers to put on a Bridge callback made WHILE serving an AI's verb. Echoes+ the three we were handed and ADDS X-Adom-Caller-Delegate naming us, so Bridge can show both facts: who asked AND who ran it. Empty when there's no caller to forward (e.g. a self-initiated poll) — see self_caller() for that case.""" ident = getattr(_current, "ident", None) or {}@@ -98,15 +98,15 @@ def self_caller() -> dict: def ad_callback(command: str, args: dict = None, on_behalf: bool = True, timeout: int = 30):- """Call an AD verb back over the loopback direct API, forwarding the caller.+ """Call a Bridge verb back over the loopback direct API, forwarding the caller. `on_behalf=True` (default): this call is part of serving an AI's verb, so it- forwards the X-Adom-Caller-* headers + adds our Delegate — AD shows+ forwards the X-Adom-Caller-* headers + adds our Delegate — Bridge shows "chip-fetcher tab 3 (via kicad)". `on_behalf=False`: a self-initiated call (poll/timer/cleanup) — sends our own `caller` in args so no stale thread leaks. - Reads ADOM_DIRECT_API_URL + ADOM_BRIDGE_TOKEN from the spawn env (AD injects- both). Returns the parsed JSON, or {"success": False, ...} if AD is+ Reads ADOM_DIRECT_API_URL + ADOM_BRIDGE_TOKEN from the spawn env (Bridge injects+ both). Returns the parsed JSON, or {"success": False, ...} if Bridge is unreachable or the env isn't populated. NEVER raises — a labeling/notify callback failing must not fail the KiCad verb the user actually asked for. """@@ -129,4 +129,4 @@ def ad_callback(command: str, args: dict = None, on_behalf: bool = True, timeout with urllib.request.urlopen(req, timeout=timeout) as r: return _json.loads(r.read().decode() or "{}") except Exception as e:- return {"success": False, "error": f"AD callback failed: {e}"}+ return {"success": False, "error": f"Bridge callback failed: {e}"}
handlers/demo.py+2−2@@ -8,7 +8,7 @@ the bridge can do. Six steps, in the order a hardware person thinks: Design constraints that shaped this file: -* **Ships zero assets.** The runtime zip is auto-bundled into AD, so it carries+* **Ships zero assets.** The runtime zip is auto-bundled into Bridge, so it carries no sample projects (see the kicad-bridge-publish skill). Every symbol, footprint, schematic and board below is GENERATED here as text, a few KB of code instead of ~500 KB of files.@@ -476,7 +476,7 @@ def handle_demo(kicad_info: dict, args: dict) -> dict: prep = _prepare(kicad_info) # DEFAULT = one beat at a time. The full six-window tour takes ~90 s, which- # blows AD's per-request budget and returns a timeout that looks like a broken+ # blows Bridge's per-request budget and returns a timeout that looks like a broken # demo. Stepping also simply narrates better: say the line, screenshot, move on. # {"all": true} still runs the whole thing for non-interactive use. run_all = bool(args.get("all"))
handlers/diagnostics.pyadded+193@@ -0,0 +1,193 @@+"""kicad_diagnostics — read-only self-inspection so debugging the bridge never+needs Bridge's approval-gated `run_script`/shell.++Everything here runs INSIDE the bridge process, which already has full filesystem++ subprocess access on the user's desktop (no approval prompt). During the+ADOMBASELINE bring-up we kept reaching for Bridge's `run_script` (powershell) just to+answer "where did KiCad install?", "what env does the bridge see?", "is pcbnew+running?" — all of which the bridge can answer directly. This verb exposes that,+so an agent can diagnose a detection/config/env problem with zero shell approval.++Args (all optional):+ probePaths: [str] — for each path report exists/isDir/isFile/size + (dirs) a+ capped child listing. Read-only metadata.+ readFile: str — return the text content of a single file if it's small+ (<256 KB) and decodes as UTF-8. For peeking config/output.+ maxEntries: int — cap on dir-listing length per probed dir (default 60).+"""++import os+import sys+import platform+import subprocess+from pathlib import Path++_READ_LIMIT = 256 * 1024+_ENV_KEYS = (+ "LOCALAPPDATA", "APPDATA", "USERPROFILE", "HOMEDRIVE", "HOMEPATH",+ "PROGRAMFILES", "PROGRAMFILES(X86)", "PROGRAMDATA", "TEMP", "TMP",+ "ADOM_BIND_HOST", "PATH",+)+_PROC_NAMES = ("kicad", "kicad-cli", "pcbnew", "eeschema", "gerbview",+ "pl_editor", "bitmap2component", "pcb_calculator")+++def _list_processes() -> list:+ """Running KiCad-family processes (name + pid + exe). On Windows uses the+ ctypes EnumProcesses API (instant, in-process) instead of spawning tasklist,+ which timed out dumping the full process table on a busy VM."""+ procs = []+ exes = {n + ".exe" for n in _PROC_NAMES}+ if sys.platform == "win32":+ try:+ import ctypes+ import ctypes.wintypes+ psapi = ctypes.windll.psapi+ kernel32 = ctypes.windll.kernel32+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000+ arr = (ctypes.wintypes.DWORD * 8192)()+ br = ctypes.wintypes.DWORD()+ if not psapi.EnumProcesses(ctypes.byref(arr), ctypes.sizeof(arr), ctypes.byref(br)):+ return [{"error": "EnumProcesses failed"}]+ count = br.value // ctypes.sizeof(ctypes.wintypes.DWORD)+ buf = ctypes.create_unicode_buffer(1024)+ for i in range(count):+ pid = arr[i]+ if not pid:+ continue+ h = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)+ if not h:+ continue+ try:+ size = ctypes.wintypes.DWORD(1024)+ if kernel32.QueryFullProcessImageNameW(h, 0, buf, ctypes.byref(size)):+ p = buf.value+ if os.path.basename(p).lower() in exes:+ procs.append({"name": os.path.basename(p), "pid": pid, "exe": p})+ finally:+ kernel32.CloseHandle(h)+ except Exception as e: # pylint: disable=broad-except+ return [{"error": f"EnumProcesses failed: {e}"}]+ else:+ try:+ out = subprocess.run(["ps", "-eo", "pid,comm"], capture_output=True,+ text=True, timeout=10).stdout+ for line in out.splitlines()[1:]:+ bits = line.split(None, 1)+ if len(bits) == 2 and any(n in bits[1].lower() for n in _PROC_NAMES):+ procs.append({"name": bits[1].strip(), "pid": bits[0]})+ except Exception as e: # pylint: disable=broad-except+ return [{"error": f"ps failed: {e}"}]+ return procs+++def _probe(path_str: str, max_entries: int) -> dict:+ p = Path(path_str)+ info = {"path": str(p), "exists": False, "isDir": False, "isFile": False}+ try:+ info["exists"] = p.exists()+ if not info["exists"]:+ return info+ info["isDir"] = p.is_dir()+ info["isFile"] = p.is_file()+ if info["isFile"]:+ info["size"] = p.stat().st_size+ elif info["isDir"]:+ names = []+ for i, entry in enumerate(sorted(p.iterdir(), key=lambda e: e.name)):+ if i >= max_entries:+ info["truncated"] = True+ break+ names.append(entry.name + ("/" if entry.is_dir() else ""))+ info["entries"] = names+ except Exception as e: # pylint: disable=broad-except+ info["error"] = str(e)+ return info+++def handle_diagnostics(kicad_info: dict, args: dict) -> dict:+ args = args or {}+ out = {"success": True}++ # 1. What environment does THE BRIDGE see (not the desktop user — Bridge strips it).+ out["environment"] = {+ "vars": {k: os.environ.get(k) for k in _ENV_KEYS},+ "homeResolved": str(Path.home()),+ "pythonExe": sys.executable,+ "pythonVersion": sys.version.split()[0],+ "platform": platform.platform(),+ "pid": os.getpid(),+ "cwd": os.getcwd(),+ }++ # 2. KiCad-detection breakdown: every base the detector scans, whether it+ # exists, and which version dirs / kicad.exe it finds. This is the direct+ # lens on the "%LOCALAPPDATA% stripped → per-user install invisible" class.+ try:+ import kicad_detect+ bases = []+ for b in getattr(kicad_detect, "WIN_KICAD_BASES", []):+ bp = Path(b)+ entry = {"base": str(bp), "exists": bp.exists(), "versionDirs": []}+ if entry["exists"]:+ try:+ for d in sorted(bp.iterdir()):+ if d.is_dir():+ exe = d / "bin" / "kicad.exe"+ entry["versionDirs"].append(+ {"dir": d.name, "kicadExe": str(exe), "exeExists": exe.exists()})+ except Exception as e: # pylint: disable=broad-except+ entry["error"] = str(e)+ bases.append(entry)+ out["kicadSearch"] = {+ "bases": bases,+ "detected": kicad_detect.detect_all_kicad_versions(),+ }+ except Exception as e: # pylint: disable=broad-except+ out["kicadSearch"] = {"error": str(e)}++ # 3. Running KiCad-family processes.+ out["processes"] = _list_processes()++ # 4. The kicad_info the bridge is currently operating with + dir existence.+ ki = {k: kicad_info.get(k) for k in+ ("version", "activeVersion", "kicad_exe", "base_dir",+ "config_dir", "user_dir", "installed")}+ for key in ("config_dir", "user_dir", "base_dir"):+ v = kicad_info.get(key)+ ki[key + "_exists"] = bool(v and Path(v).exists())+ out["kicadInfo"] = ki++ # 5. Optional scoped path probes + a small-file read (read-only).+ probe_paths = args.get("probePaths") or []+ if isinstance(probe_paths, str):+ probe_paths = [probe_paths]+ max_entries = int(args.get("maxEntries", 60))+ if probe_paths:+ out["probes"] = [_probe(p, max_entries) for p in probe_paths]++ read_file = args.get("readFile")+ if read_file:+ rp = Path(read_file)+ fr = {"path": str(rp)}+ try:+ if not rp.is_file():+ fr["error"] = "not a file"+ elif rp.stat().st_size > _READ_LIMIT:+ fr["error"] = f"too large ({rp.stat().st_size} bytes > {_READ_LIMIT})"+ else:+ fr["content"] = rp.read_text(encoding="utf-8", errors="replace")+ except Exception as e: # pylint: disable=broad-except+ fr["error"] = str(e)+ out["fileRead"] = fr++ out["_hint"] = (+ "Read-only bridge self-diagnostics — use this INSTEAD of Bridge's approval-gated "+ "run_script/shell when debugging the bridge. `environment` shows what env the "+ "BRIDGE sees (Bridge strips it — a missing LOCALAPPDATA here explains a per-user "+ "KiCad going undetected). `kicadSearch.bases` shows every path the detector "+ "scans and what it finds. `processes` lists running KiCad exes. Pass "+ "probePaths:[...] to stat/list any path and readFile:\"...\" to peek a small "+ "config/output file — all without a shell-approval prompt."+ )+ return out
handlers/kicad_ui.py+2−2@@ -626,8 +626,8 @@ def handle_send_key(kicad_info: dict, args: dict) -> dict: "output": f"Sent '{key}' to {title}", "data": {"key": key, "modifiers": mod_vks, "baseVk": base_vk, "baseShift": base_shift, "hwnd": target_hwnd, "title": title},- "_hint": ("Sent (back-compat chord path). For modifier chords prefer AD's "- "desktop_press_key — it owns full chord parsing and AD's focus-first "+ "_hint": ("Sent (back-compat chord path). For modifier chords prefer Bridge's "+ "desktop_press_key — it owns full chord parsing and Bridge's focus-first " "delivery. Keep kicad_send_key for single keys to an EXACT hwnd (e.g. " "Enter/Escape to a specific dialog)."), }
handlers/software_opengl.py+1−1@@ -98,7 +98,7 @@ def handle_enable_software_opengl(kicad_info: dict, args: dict) -> dict: "load it. To undo, delete opengl32.dll/libgallium_wgl.dll/dxil.dll and the *.local files " "from KiCad's bin." if ok else- "Could not deploy — see errors[]. Writing KiCad's bin (Program Files) needs an elevated AD; "+ "Could not deploy — see errors[]. Writing KiCad's bin (Program Files) needs an elevated Bridge; " "a per-user KiCad install (kicad_upgrade scope=user) avoids that." ), }
handlers/uia.pyadded+144@@ -0,0 +1,144 @@+"""uia.py — BACKGROUND UI Automation for KiCad, via .NET UIAutomation (PowerShell).++Why: opening the Footprint Editor / 3D viewer used to fall back to clicking a+toolbar icon (moves the user's MOUSE) or an Alt+3 keystroke (needs the window+FOREGROUNDED). Both hijack the desktop. UIA's Invoke pattern triggers a control+PROGRAMMATICALLY — no foreground, no focus steal, no cursor movement. KiCad is+wxWidgets but exposes an invokable UIA tree, so its toolbar buttons / menu items+are reachable by accessible name.++Self-contained: shells out to `powershell` (present on every Windows box) which+loads System.Windows.Automation. NOT Bridge's gated `run_script` — this is the bridge's+OWN subprocess (same as how it runs kicad-cli), so no shell-approval is involved.+UIA is a mature, stable API (unlike the thin/undocumented Win32 menu-ID path).+"""++import json+import subprocess+import sys++IS_WINDOWS = sys.platform == "win32"++# PowerShell that: attaches to a window by hwnd, finds a control by exact name or+# name-substring (optionally filtered by control type), and Invokes it (or, for+# SetValue, sets its text). Emits a one-line JSON result. All background.+_PS_TEMPLATE = r'''+$ErrorActionPreference = 'Stop'+try {+ Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes+ $AE = [System.Windows.Automation.AutomationElement]+ $TS = [System.Windows.Automation.TreeScope]+ $root = $AE::FromHandle([IntPtr]__HWND__)+ if ($root -eq $null) { '{"ok":false,"reason":"window_not_found"}'; exit }++ $name = __NAME__+ $contains = __CONTAINS__+ $action = '__ACTION__'+ $text = __TEXT__++ $target = $null+ if ($name) {+ $cond = New-Object System.Windows.Automation.PropertyCondition($AE::NameProperty, $name)+ $target = $root.FindFirst($TS::Descendants, $cond)+ }+ if ($target -eq $null -and $contains) {+ # bounded FindAll, filter by name substring. For an invoke we must pick a control+ # that ACTUALLY supports InvokePattern — KiCad exposes the label "Footprint Editor"+ # AND the toolbar button with the same name; grabbing the label yields+ # not_invokable. So when action=invoke, prefer an invokable match (fall back to+ # first substring match only if none is invokable).+ $all = $root.FindAll($TS::Descendants, [System.Windows.Automation.Condition]::TrueCondition)+ $firstMatch = $null+ foreach ($el in $all) {+ $n = $el.Current.Name+ if ($n -and $n.ToLower().Contains($contains.ToLower())) {+ if ($firstMatch -eq $null) { $firstMatch = $el }+ if ($action -eq 'invoke') {+ $tmp = $null+ if ($el.TryGetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern, [ref]$tmp)) { $target = $el; break }+ } elseif ($action -eq 'setvalue') {+ $tmp = $null+ if ($el.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$tmp)) { $target = $el; break }+ } else { $target = $el; break }+ }+ }+ if ($target -eq $null) { $target = $firstMatch }+ }+ if ($target -eq $null) { '{"ok":false,"reason":"control_not_found"}'; exit }++ $foundName = $target.Current.Name+ if ($action -eq 'invoke') {+ $p = $null+ if ($target.TryGetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern, [ref]$p)) {+ $p.Invoke()+ '{"ok":true,"action":"invoke","name":' + ($foundName | ConvertTo-Json) + '}'+ } else {+ # some KiCad menu items expose ExpandCollapse or are only Selectable+ $sp = $null+ if ($target.TryGetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern, [ref]$sp)) {+ $sp.Select(); '{"ok":true,"action":"select","name":' + ($foundName | ConvertTo-Json) + '}'+ } else { '{"ok":false,"reason":"not_invokable","name":' + ($foundName | ConvertTo-Json) + '}' }+ }+ } elseif ($action -eq 'setvalue') {+ $vp = $null+ if ($target.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$vp)) {+ $vp.SetValue($text)+ '{"ok":true,"action":"setvalue","name":' + ($foundName | ConvertTo-Json) + '}'+ } else { '{"ok":false,"reason":"not_settable","name":' + ($foundName | ConvertTo-Json) + '}' }+ } else {+ '{"ok":true,"action":"find","name":' + ($foundName | ConvertTo-Json) + ',"found":true}'+ }+} catch {+ '{"ok":false,"reason":"exception","error":' + ($_.Exception.Message | ConvertTo-Json) + '}'+}+'''+++def _ps_json(value) -> str:+ """Render a Python value as a PowerShell literal (string or $null)."""+ if value is None:+ return "$null"+ return json.dumps(str(value)) # JSON string == a valid PS single/double-quoted string+++def _run(hwnd: int, action: str, name=None, contains=None, text=None, timeout: float = 8.0) -> dict:+ if not IS_WINDOWS:+ return {"ok": False, "reason": "not_windows"}+ script = (_PS_TEMPLATE+ .replace("__HWND__", str(int(hwnd)))+ .replace("__NAME__", _ps_json(name))+ .replace("__CONTAINS__", _ps_json(contains))+ .replace("__ACTION__", action)+ .replace("__TEXT__", _ps_json(text)))+ try:+ proc = subprocess.run(+ ["powershell", "-NoProfile", "-NonInteractive", "-Command", script],+ capture_output=True, text=True, timeout=timeout,+ )+ out = (proc.stdout or "").strip().splitlines()+ for line in reversed(out): # last non-empty line is our JSON+ line = line.strip()+ if line.startswith("{"):+ return json.loads(line)+ return {"ok": False, "reason": "no_output", "stderr": (proc.stderr or "")[:200]}+ except subprocess.TimeoutExpired:+ return {"ok": False, "reason": "timeout"}+ except Exception as e: # pylint: disable=broad-except+ return {"ok": False, "reason": "run_failed", "error": str(e)[:200]}+++def uia_invoke(hwnd: int, name: str = None, contains: str = None, timeout: float = 8.0) -> dict:+ """Background-Invoke a control (toolbar button / menu item) in the given window,+ matched by exact accessible name OR name substring. No foreground, no cursor.+ Returns {ok, action, name} / {ok:False, reason}."""+ return _run(hwnd, "invoke", name=name, contains=contains, timeout=timeout)+++def uia_set_value(hwnd: int, text: str, name: str = None, contains: str = None, timeout: float = 8.0) -> dict:+ """Background-SetValue on a text control (e.g. a search box) — no focus/typing."""+ return _run(hwnd, "setvalue", name=name, contains=contains, text=text, timeout=timeout)+++def uia_find(hwnd: int, name: str = None, contains: str = None, timeout: float = 6.0) -> dict:+ """Read-only: is the control present in the UIA tree? {ok, found, name}."""+ return _run(hwnd, "find", name=name, contains=contains, timeout=timeout)
handlers/upgrade.py+5−5@@ -26,7 +26,7 @@ import kicad_detect import proc # A CA bundle shipped WITH the bridge (certifi's cacert.pem). This is the durable-# trust source: AD's provisioned portable Python (Runtime contract v1.9.63+) has+# trust source: Bridge's provisioned portable Python (Runtime contract v1.9.63+) has # no usable system CA store, so create_default_context() verifies against nothing # and the Windows cert enumeration proved unreliable on it — every https fetch # died with CERTIFICATE_VERIFY_FAILED. Bundling the roots guarantees working TLS@@ -37,7 +37,7 @@ _BUNDLED_CACERT = os.path.join( def _ssl_context() -> ssl.SSLContext:- """A verifying SSL context that works even on AD's certless portable Python."""+ """A verifying SSL context that works even on Bridge's certless portable Python.""" ctx = ssl.create_default_context() # Primary: the bundled certifi roots (works on any interpreter). try:@@ -276,7 +276,7 @@ def handle_upgrade(kicad_info: dict, args: dict) -> dict: # Reuse an already-downloaded installer. The KiCad installer is ~1 GB, and a # prior attempt that succeeded at DOWNLOAD but failed at INSTALL (e.g. UAC not- # approved / AD not elevated) leaves a valid .exe in Temp. Skipping the+ # approved / Bridge not elevated) leaves a valid .exe in Temp. Skipping the # re-download makes a post-elevation retry near-instant. def _valid_installer(p: Path) -> bool: try:@@ -375,9 +375,9 @@ def handle_upgrade(kicad_info: dict, args: dict) -> dict: # Silent install (NSIS /S). Installing into Program Files needs elevation, so # the user may get a UAC prompt to approve. Block until the installer exits.- # If AD isn't elevated, this is where it STALLS (a UAC prompt no one approves)+ # If Bridge isn't elevated, this is where it STALLS (a UAC prompt no one approves) # until the timeout — the log line below is the last thing you'll see.- # Is THIS process (hence AD, its parent) elevated? Definitive answer to+ # Is THIS process (hence Bridge, its parent) elevated? Definitive answer to # "did the elevated relaunch actually take" — the KiCad installer needs admin, # so if we're not elevated it exits fast with a failure code (no UAC prompt to # click) and nothing installs.
kicad_detect.py+87−6@@ -29,12 +29,29 @@ WIN_KICAD_BASES = [ Path("C:/Program Files/KiCad"), Path("C:/Program Files (x86)/KiCad"), ]-_local = os.environ.get("LOCALAPPDATA")-if _local:- WIN_KICAD_BASES.append(Path(_local) / "Programs" / "KiCad")-# Also look directly in LOCALAPPDATA (rare, but seen in some manual installs)-if _local:- WIN_KICAD_BASES.append(Path(_local) / "KiCad")+# Per-user installs land in %LOCALAPPDATA%\Programs\KiCad — both winget --scope user+# AND our own kicad_upgrade `/currentuser` (no-UAC) install. CRITICAL: Bridge provisions+# the bridge's Python with a STRIPPED environment in which LOCALAPPDATA can be MISSING,+# so we must NOT rely on that one env var — resolve the LocalAppData root from every+# source available (LOCALAPPDATA, then USERPROFILE\AppData\Local, then Path.home()).+# Without this the no-UAC install is invisible to the bridge even though Bridge's own+# glob-based readiness finds it → every verb fails "KiCad not installed" right after a+# successful install. (Root-caused on ADOMBASELINE, 2026-07.)+_local_roots: list[Path] = []+for _cand in (+ os.environ.get("LOCALAPPDATA"),+ (os.path.join(os.environ["USERPROFILE"], "AppData", "Local")+ if os.environ.get("USERPROFILE") else None),+ str(Path.home() / "AppData" / "Local"),+):+ if _cand:+ _p = Path(_cand)+ if _p not in _local_roots:+ _local_roots.append(_p)+for _root in _local_roots:+ for _sub in (_root / "Programs" / "KiCad", _root / "KiCad"):+ if _sub not in WIN_KICAD_BASES:+ WIN_KICAD_BASES.append(_sub) # Legacy single-base name kept for back-compat with any callers that import it WIN_KICAD_BASE = WIN_KICAD_BASES[0] @@ -171,6 +188,70 @@ def _win_find_user_dir(version: str): return user if user.exists() else None +def ensure_win_user_config(version: str, base_dir):+ """Bootstrap KiCad's per-user config on a fresh machine so library installs+ work BEFORE KiCad has ever been launched.++ On a brand-new KiCad install, %APPDATA%/kicad/<version>/ (which holds the+ per-user sym-/fp-lib-table) doesn't exist until the first GUI launch — so+ config_dir/user_dir come back None and every install_* verb fails with+ "config directory not found". This mirrors KiCad's first-run: create the+ config + user dirs and seed the lib tables FROM THE INSTALL TEMPLATE (so the+ standard global libraries are preserved, not clobbered by an empty table).++ Returns (config_dir, user_dir) as Paths, or (None, None) off-Windows / on error.+ """+ # Resolve APPDATA/USERPROFILE robustly — Bridge provisions the bridge's Python with+ # a STRIPPED environment where these can be unset (same failure mode as the+ # LOCALAPPDATA detection bug). Fall back to Path.home() so config bootstrap works+ # regardless. (ADOMBASELINE 2026-07.)+ appdata = os.environ.get("APPDATA") or str(Path.home() / "AppData" / "Roaming")+ userprofile = os.environ.get("USERPROFILE") or str(Path.home())+ if not appdata or not userprofile:+ return None, None+ config = Path(appdata) / "kicad" / version+ user = Path(userprofile) / "Documents" / "KiCad" / version+ try:+ config.mkdir(parents=True, exist_ok=True)+ (user / "symbols").mkdir(parents=True, exist_ok=True)+ (user / "footprints").mkdir(parents=True, exist_ok=True)+ except OSError:+ return None, None+ template = Path(base_dir) / "share" / "kicad" / "template" if base_dir else None+ for fname, root in (("sym-lib-table", "sym_lib_table"), ("fp-lib-table", "fp_lib_table")):+ dest = config / fname+ if dest.exists():+ continue+ try:+ src = (template / fname) if template else None+ if src and src.exists():+ dest.write_bytes(src.read_bytes()) # keep KiCad's standard global libs+ else:+ dest.write_text(f"({root}\n (version 7)\n)\n", encoding="utf-8")+ except OSError:+ pass+ # Seed a minimal kicad_common.json so KiCad doesn't treat this as a FIRST RUN+ # ("KiCad is starting for the first time, or some of its configuration files are+ # missing") and pop the "Welcome to KiCad" setup wizard that blocks automation.+ # The mere presence of a valid settings file suppresses it; KiCad merges its own+ # defaults over whatever we write. canvas_type 1 = KiCad's own default (hardware+ # accelerated); on a GPU-less host the OpenGL notice is separately auto-dismissed+ # and canvas flipped to 2 (software) by the dialog sweep.+ common = config / "kicad_common.json"+ if not common.exists():+ try:+ common.write_text(+ '{\n'+ ' "meta": { "filename": "kicad_common", "version": 3 },\n'+ ' "graphics": { "canvas_type": 1 }\n'+ '}\n',+ encoding="utf-8",+ )+ except OSError:+ pass+ return config, user++ # v1.7.4+ (KiCad v2 Phase 1): enriched-discovery helpers. # All return None / empty / False on failure — never raise. Detection has to be # best-effort: the caller iterates over multiple installs and one buggy install
proc.py+2−2@@ -3,8 +3,8 @@ When a supervisor (Adom Bridge) spawns this bridge, the bridge's own child processes must NOT inherit a broken/null console handle or an open stdin, or calls like `subprocess.run(... capture_output=True)` can deadlock during startup-(observed 2026-06-25: KiCad detection hung forever under AD's spawn, but ran fine-when launched from a normal shell). AD ≥1.8.185 hardened its side (valid log+(observed 2026-06-25: KiCad detection hung forever under Bridge's spawn, but ran fine+when launched from a normal shell). Bridge ≥1.8.185 hardened its side (valid log handles); this hardens ours so the bridge is robust under ANY supervisor. Always: detach stdin (DEVNULL) and, on Windows, CREATE_NO_WINDOW so no console
server.py+29−29@@ -59,7 +59,7 @@ from handlers.export import ( # v0.9.61+: one-shot desktop half of the Adom molecule pipeline (STEP + silk # renders + sibling sch/pro discovery + container next-steps manifest). from handlers.export_molecule import handle_export_molecule-# v0.9.63+: first-class showcase tour (AD demos this bridge during Hydrogen install).+# v0.9.63+: first-class showcase tour (Bridge demos this bridge during Hydrogen install). from handlers.demo import handle_demo # v0.9.10+: check/upgrade KiCad via the OFFICIAL downloads.kicad.org installer # (NOT winget, which lags the real release). check_for_updates is read-only;@@ -74,9 +74,9 @@ DEFAULT_PORT = 8772 def _read_bridge_version() -> str: """Our own bridge version, from the BRIDGE_VERSION file beside server.py.- Reported in /health so AD can detect a STALE bridge process (one that- survived an AD update) and restart it — the "not running latest bridge"- footgun. Empty string if the file is missing (treated as stale by AD)."""+ Reported in /health so Bridge can detect a STALE bridge process (one that+ survived a Bridge update) and restart it — the "not running latest bridge"+ footgun. Empty string if the file is missing (treated as stale by Bridge).""" try: p = os.path.join(os.path.dirname(os.path.abspath(__file__)), "BRIDGE_VERSION") with open(p, encoding="utf-8") as fh:@@ -118,7 +118,7 @@ def _handle_list_versions(kicad_info: dict, args: dict) -> dict: "activeVersion": None, "count": 0, "runningInstances": _detect_running_kicad_instances(),- "_hint": "No KiCad installs found. KiCad is the user's host app, so AD won't silently auto-install it as a dependency/prewarm (v1.9.47) — but DON'T make the user install it by hand. Offer to do it for them, then run `kicad_upgrade '{}'`: the agent installs the latest stable from the OFFICIAL downloads.kicad.org installer (silent /S; may raise a UAC prompt to approve). kicad_upgrade installs from scratch, not just upgrades. Windows only.",+ "_hint": "No KiCad installs found. KiCad is the user's host app, so Bridge won't silently auto-install it as a dependency/prewarm (v1.9.47) — but DON'T make the user install it by hand. Offer to do it for them, then run `kicad_upgrade '{}'`: the agent installs the latest stable from the OFFICIAL downloads.kicad.org installer (silent /S; may raise a UAC prompt to approve). kicad_upgrade installs from scratch, not just upgrades. Windows only.", "errorCode": "host_app_not_installed", "errorCodeLegacy": "kicad_not_installed", "hostApp": "KiCad",@@ -153,13 +153,13 @@ def _handle_list_versions(kicad_info: dict, args: dict) -> dict: def _handle_readiness(kicad_info: dict, args: dict) -> dict: """Read-only readiness probe — 'is the KiCad host app present?' - AD v1.9.47+: KiCad is the user's HOST APP. AD detects it, it never+ Bridge v1.9.47+: KiCad is the user's HOST APP. Bridge detects it, it never installs it. This verb answers that detection question with zero side effects: it reads the cached `all_kicad_versions` computed once at startup, does NOT install the reverse-bridge plugin, does NOT spawn subprocesses, and works even when KiCad is absent (reports detected:false). For full per-version detail use kicad_list_versions;- for "what EDA tools do I have" across all bridges use the AD-level+ for "what EDA tools do I have" across all bridges use the Bridge-level `bridge_readiness`. """ detected = bool(all_kicad_versions)@@ -182,7 +182,7 @@ def _handle_readiness(kicad_info: dict, args: dict) -> dict: "versions": versions, "_hint": ( "Read-only host-app detection (no install, no plugin load, no subprocess). "- "AD won't silently auto-install KiCad as a dependency/prewarm (v1.9.47), but the agent "+ "Bridge won't silently auto-install KiCad as a dependency/prewarm (v1.9.47), but the agent " "still installs it FOR the user on request — never hand the user a manual download. " + ("KiCad is installed and ready." if detected else "KiCad not detected — offer to install it, then run `kicad_upgrade '{}'` (installs the "@@ -194,7 +194,7 @@ def _handle_readiness(kicad_info: dict, args: dict) -> dict: # Public verb catalog for kicad_describe (Bridge SDK Verb contract + discussion # #14 amendment). Keyed by the INTERNAL command name; the kicad_ prefix is added # on output. Each entry carries summary + long(-running) + hint + related +-# pitfalls, which AD's Verbs tab renders inline. Aliases (export_gerbers) skipped.+# pitfalls, which Bridge's Verbs tab renders inline. Aliases (export_gerbers) skipped. _VERB_CATALOG = { "open_board": {"summary": "Open a .kicad_pcb in the PCB editor (pcbnew).", "long": True, "hint": "Pass an ABSOLUTE filePath; opens the board in pcbnew and returns once the window is up. Optional kicadVersion pins a specific install.",@@ -231,7 +231,7 @@ _VERB_CATALOG = { "enable_software_opengl": {"summary": "LAST-RESORT fallback: deploy Mesa llvmpipe software OpenGL into KiCad's bin so it renders on a GPU-less host (Hyper-V/RDP/headless). SLOW (CPU). Never suggest proactively.", "long": True, "hint": "Only when KiCad can't render at all AND there's no GPU. Drops Mesa opengl32/libgallium DLLs + .local redirection into KiCad bin (needs elevation to write Program Files, or use a per-user install). Close+reopen editors after.", "related": ["kicad_dismiss_dialogs", "kicad_upgrade"],- "pitfalls": ["a real GPU / Hyper-V GPU-P passthrough is far better — this is CPU rendering, slow", "writing KiCad's bin needs an elevated AD"]},+ "pitfalls": ["a real GPU / Hyper-V GPU-P passthrough is far better — this is CPU rendering, slow", "writing KiCad's bin needs an elevated Bridge"]}, "dismiss_dialogs": {"summary": "Programmatically expire KiCad dialogs (auto-clears the benign OpenGL/software-render notice; {all:true} clears every dialog).", "long": False, "hint": "Read-only-ish self-heal: clicks each dialog's OK/default button. window_info also auto-expires benign dialogs; use this to force-clear a specific/all dialogs before retrying an open/screenshot.", "related": ["kicad_window_info", "kicad_screenshot_all", "kicad_open_board"],@@ -282,7 +282,7 @@ _VERB_CATALOG = { "hint": "Captures all KiCad windows; the go-to way to VERIFY what actually happened after an open/edit.", "related": ["kicad_open_editors", "kicad_window_info", "kicad_open_board"], "pitfalls": ["a window still rendering (e.g. 3D viewer) shows blank — wait a beat first"]}, "send_key": {"summary": "Send a keystroke to an EXACT KiCad window by hwnd (SendMessage; not for chords).", "long": False,- "hint": "For modifier CHORDS ('ctrl+s', 'alt+3', 'ctrl+shift+p') use AD's desktop_press_key — it owns full chord parsing; don't route chords here. Use this verb only for the case AD's focus-first model can't cover: a single key delivered to an EXACT hwnd (e.g. Enter/Escape to a specific dialog from window_info).",+ "hint": "For modifier CHORDS ('ctrl+s', 'alt+3', 'ctrl+shift+p') use Bridge's desktop_press_key — it owns full chord parsing; don't route chords here. Use this verb only for the case Bridge's focus-first model can't cover: a single key delivered to an EXACT hwnd (e.g. Enter/Escape to a specific dialog from window_info).", "related": ["kicad_window_info", "kicad_click", "kicad_fix_keyboard"], "pitfalls": ["not for chords — use desktop_press_key", "needs a target hwnd (from window_info)"]}, "click": {"summary": "Click at coordinates inside a KiCad window.", "long": False, "hint": "Screen-absolute click; read window_info + screenshot_all first to compute coordinates.",@@ -295,17 +295,17 @@ _VERB_CATALOG = { "related": ["kicad_readiness", "kicad_check_for_updates", "kicad_upgrade"], "pitfalls": ["returns empty when KiCad isn't installed — offer kicad_upgrade, don't punt to a manual download"]}, "readiness": {"summary": "Read-only host-app detection — is KiCad installed/ready?", "long": False, "hint": "Zero side effects; returns {detected, ready, versionCount, defaultVersion}. Poll before driving.",- "related": ["kicad_list_versions", "kicad_upgrade", "kicad_describe"], "pitfalls": ["there is NO prewarm — KiCad is a host app you install via kicad_upgrade, not a download AD warms"]},+ "related": ["kicad_list_versions", "kicad_upgrade", "kicad_describe"], "pitfalls": ["there is NO prewarm — KiCad is a host app you install via kicad_upgrade, not a download Bridge warms"]}, "describe": {"summary": "This catalog — every verb the bridge exposes.", "long": False, "hint": "Read-only full verb catalog; the completeness escape hatch. Per-call _hints carry you in practice.", "related": ["kicad_readiness"], "pitfalls": []}, "diagnostics": {"summary": "Read-only bridge self-inspection (env, detection breakdown, processes, path probe).", "long": False,- "hint": "Use INSTEAD of AD's approval-gated run_script/shell. Shows the env the BRIDGE sees (stripped LOCALAPPDATA explains an undetected per-user install), every path the detector scans + what it finds, running KiCad exes, and (with probePaths/readFile args) any path's stat/listing/small-file content.",+ "hint": "Use INSTEAD of Bridge's approval-gated run_script/shell. Shows the env the BRIDGE sees (stripped LOCALAPPDATA explains an undetected per-user install), every path the detector scans + what it finds, running KiCad exes, and (with probePaths/readFile args) any path's stat/listing/small-file content.", "related": ["kicad_readiness", "kicad_list_versions", "kicad_bridge_status"], "pitfalls": ["read-only — never installs or changes anything"]}, "bridge_status": {"summary": "Probe running KiCad plugin instances (multi-instance health).", "long": False, "hint": "Read-only; lists live in-process plugin instances + their ports/health. Bounded, non-blocking.", "related": ["kicad_bridge_call", "kicad_open_editors", "kicad_install_plugin"], "pitfalls": ["an instance shows only after KiCad has run a kicad_* call that loaded the plugin"]},- "status": {"summary": "Bridge/host-app status — the verb AD polls after a non-terminal timeout.", "long": False,+ "status": {"summary": "Bridge/host-app status — the verb Bridge polls after a non-terminal timeout.", "long": False, "hint": "Canonical statusVerb (see bridge.json). Read-only; poll this after a long verb returns {stillRunning:true} to check whether it finished.", "related": ["kicad_readiness", "kicad_bridge_status"], "pitfalls": ["reports bridge + plugin liveness, not per-export progress"]}, "bridge_call": {"summary": "Call into a running KiCad plugin instance (in-process IPC).", "long": False,@@ -348,7 +348,7 @@ _VERB_CATALOG = { def _handle_describe(kicad_info: dict, args: dict) -> dict: """Bridge SDK <prefix>_describe — the full machine-readable verb catalog. - Read-only, zero side effects. AD caches it (~/.adom/bridge-verbs/kicad.json)+ Read-only, zero side effects. Bridge caches it (~/.adom/bridge-verbs/kicad.json) and renders it in the GUI Verbs tab (name/summary/hint/related/pitfalls per verb — discussion #14). In practice the per-call `_hint`s carry the AI; describe is the completeness escape hatch.@@ -381,7 +381,7 @@ def _handle_describe(kicad_info: dict, args: dict) -> dict: "Full verb catalog for the KiCad bridge — each entry carries hint/related/pitfalls. " "Every verb ALSO returns its own rich _hint when called; that's what to read in practice. " "For host-app readiness use kicad_readiness; for 'what EDA tools do I have' across bridges "- "use AD's bridge_readiness."+ "use Bridge's bridge_readiness." ), } @@ -464,7 +464,7 @@ COMMAND_HANDLERS = { "describe": _handle_describe, "diagnostics": handle_diagnostics, "bridge_status": handle_bridge_status,- # kicad_status: the canonical statusVerb AD polls after a non-terminal timeout+ # kicad_status: the canonical statusVerb Bridge polls after a non-terminal timeout # (bridge.json `statusVerb` + detect.hostAppOptionalVerbs "status"). Alias of # bridge_status; kicad_bridge_status stays for back-compat. "status": handle_bridge_status,@@ -607,7 +607,7 @@ def dispatch_command(command: str, args: dict) -> dict: return result # Pre-check: if KiCad isn't installed at all, return a helpful error. KiCad- # is the user's host app — AD won't silently auto-install it (v1.9.47) — but+ # is the user's host app — Bridge won't silently auto-install it (v1.9.47) — but # the agent installs it FOR the user via kicad_upgrade rather than handing # them a manual download. #@@ -746,7 +746,7 @@ def dispatch_command(command: str, args: dict) -> dict: # find the window by matching the editor TITLE for the verb (open handlers # don't return hwnd, and threading it through ~30 return sites is fragile), # then forward the caller identity (via kicad) per #13. Best-effort and- # non-blocking: no caller, no AD, or window not found -> skip silently,+ # non-blocking: no caller, no Bridge, or window not found -> skip silently, # never fail the user's verb. try: _thread = caller_identity.thread_name()@@ -798,7 +798,7 @@ class KiCadBridgeHandler(BaseHTTPRequestHandler): def _status_payload(self) -> dict: # Status chip (Bridge SDK "Health + status chip"): a 200 already makes the # chip green; we additionally report led/summary/tooltip truthfully so the- # chip reflects host-app readiness (AD owns the gray "offline" state).+ # chip reflects host-app readiness (Bridge owns the gray "offline" state). detected = bool(all_kicad_versions) default_v = all_kicad_versions[0]["version"] if detected else None if detected:@@ -846,9 +846,9 @@ class KiCadBridgeHandler(BaseHTTPRequestHandler): command = request.get("command", "") args = request.get("args", {}) - # AD 1.9.180: stamp WHO asked (self-asserted; logging/UX/arbitration only,- # never authorization). Kept for the life of this request so any AD- # callback we make while serving it can forward the identity (AD 1.9.183).+ # Bridge 1.9.180: stamp WHO asked (self-asserted; logging/UX/arbitration only,+ # never authorization). Kept for the life of this request so any Bridge+ # callback we make while serving it can forward the identity (Bridge 1.9.183). caller = caller_identity.set_from_request(self.headers, args) print(f"[KiCad Bridge] {command}{caller_identity.log_suffix()} | Args: {json.dumps(args)}") @@ -882,7 +882,7 @@ def main(): # output to a log FILE, Python block-buffers it by default — so a startup # hang shows a 0-byte log and is undebuggable. Line buffering makes every # startup step land in ~/.adom/bridge-logs/<name>.log in real time, which is- # exactly what AD asked bridge authors to do (fatal errors → stdout/stderr).+ # exactly what Bridge asked bridge authors to do (fatal errors → stdout/stderr). try: sys.stdout.reconfigure(line_buffering=True) sys.stderr.reconfigure(line_buffering=True)@@ -911,15 +911,15 @@ def main(): # ThreadingHTTPServer: handle each request in its own thread so a slow or # wedged handler (e.g. first-call plugin auto-install) never blocks the # health probe or other verbs — the single-threaded server otherwise serves- # one request at a time, so AD's first verb could wedge the whole bridge and+ # one request at a time, so Bridge's first verb could wedge the whole bridge and # make it look hung. daemon_threads so a stuck handler can't block shutdown. class _SingleInstanceHTTPServer(ThreadingHTTPServer): allow_reuse_address = False daemon_threads = True- # Bind loopback via ADOM_BIND_HOST (Bridge SDK Runtime contract, AD v1.9.63+):- # AD passes ADOM_BIND_HOST (always 127.0.0.1) to every spawned bridge. Binding- # it (never 0.0.0.0) avoids the Windows Firewall "allow access?" prompt AD- # guarantees users won't see. Default to 127.0.0.1 when spawned outside AD.+ # Bind loopback via ADOM_BIND_HOST (Bridge SDK Runtime contract, Bridge v1.9.63+):+ # Bridge passes ADOM_BIND_HOST (always 127.0.0.1) to every spawned bridge. Binding+ # it (never 0.0.0.0) avoids the Windows Firewall "allow access?" prompt Bridge+ # guarantees users won't see. Default to 127.0.0.1 when spawned outside Bridge. bind_host = os.environ.get("ADOM_BIND_HOST", "127.0.0.1") try: server = _SingleInstanceHTTPServer((bind_host, port), KiCadBridgeHandler)
skills/kicad-bridge-dev/SKILL.md+18−18@@ -6,11 +6,11 @@ description: DEVELOPER-only skill (user-invocable:false — installed for mainta # kicad-bridge-dev -Developer notes for the `kicad-bridge`. Source-only (belongs in the repo, never shipped to a user install). Read `adom-desktop-bridge-sdk` first for the AD-core-vs-author boundary.+Developer notes for the `kicad-bridge`. Source-only (belongs in the repo, never shipped to a user install). Read `adom-desktop-bridge-sdk` first for the Bridge-core-vs-author boundary. ## What it is -A reverse bridge AD spawns on the user's laptop (`spawn.kind: python`, `entrypoint: server.py`, `port: 0`). It adds the `kicad_*` verb namespace. AD provisions Python (Runtime contract v1.9.63+) and passes `ADOM_BIND_HOST` — bind that, never `0.0.0.0`.+A reverse bridge Bridge spawns on the user's laptop (`spawn.kind: python`, `entrypoint: server.py`, `port: 0`). It adds the `kicad_*` verb namespace. Bridge provisions Python (Runtime contract v1.9.63+) and passes `ADOM_BIND_HOST` — bind that, never `0.0.0.0`. ## The 4 control surfaces (how a verb reaches KiCad) @@ -92,9 +92,9 @@ of it) the dispatcher: long the window keeps re-raising. **Verifying background behavior (do NOT trust `GetForegroundWindow` via `run_script`):** measuring-foreground *from* an AD `run_script` is unreliable — the measuring PowerShell/conhost briefly *becomes*+foreground *from* a Bridge `run_script` is unreliable — the measuring PowerShell/conhost briefly *becomes* the foreground (you read `''` or itself), and on some machines `run_script` just times out at 75s. Use-**`desktop_list_windows`** (AD-core, no bridge, returns top-level windows in **EnumWindows / top-of-Z-order+**`desktop_list_windows`** (Bridge-core, no bridge, returns top-level windows in **EnumWindows / top-of-Z-order first**) and compare the z-index of the KiCad window vs the user's window. If the user's window has a lower index than every KiCad window, it opened in the background. This is the reliable, non-perturbing test. @@ -115,7 +115,7 @@ and **Invokes** the "Footprint Editor" / "3D Viewer" control by accessible name. programmatic: no foreground, no focus steal, no cursor move. KiCad (wxWidgets) exposes an invokable UIA tree, so this is the reliable background way to open editors when the WM_COMMAND plugin path isn't loaded. `uia.uia_invoke(hwnd, contains=...)` / `uia.uia_set_value(...)`. This is the bridge's OWN-subprocess (like kicad-cli) — NOT AD's gated `run_script`.+subprocess (like kicad-cli) — NOT Bridge's gated `run_script`. **Rules when you touch UI code:** - A new window-raising verb → add it to **`_SPAWNS_WINDOW_FOCUS`** in `server.py`. That's the@@ -140,7 +140,7 @@ subprocess (like kicad-cli) — NOT AD's gated `run_script`. **Dispatcher special cases** (`dispatch_command`): `readiness`, `describe`, `check_for_updates` return BEFORE the plugin auto-install + the KiCad-not-installed pre-check (they must work with KiCad absent). `upgrade` also bypasses that pre-check (it INSTALLS KiCad — do not gate the installer behind "KiCad not installed") and refreshes the detection cache on success. -### ⚖️ Every file you add to this repo may ship to every AD user+### ⚖️ Every file you add to this repo may ship to every Bridge user The release zip is **auto-bundled into Adom Bridge**, so repo weight is user-facing weight. Before adding a file, know which artifact it belongs to:@@ -161,17 +161,17 @@ Audit 2026-07-19 found **649 KB** of never-referenced weight (219 KB of hero *fo duplicated `skills/` trees, demo docs in the zip; 496 KB of PNGs in the pkg). Don't re-grow it — full KEEP/EXCLUDE policy lives in `kicad-bridge-publish` §2/§4. -## 🪪 Caller provenance (AD 1.9.180 / 1.9.183) — `caller_identity.py`+## 🪪 Caller provenance (Bridge 1.9.180 / 1.9.183) — `caller_identity.py` -A user runs ~20 AI threads against one desktop. AD stamps `X-Adom-Caller-Thread`+A user runs ~20 AI threads against one desktop. Bridge stamps `X-Adom-Caller-Thread` / `-Container` / `-Reason` on every relayed request. `do_POST` captures them via `caller_identity.set_from_headers()` and clears them in `finally`; the per-verb log line carries `caller="..."` (empty for loopback/anon callers). -**When you add a verb that calls an AD verb back** (`desktop_screenshot_window`,+**When you add a verb that calls a Bridge verb back** (`desktop_screenshot_window`, `desktop_set_window_identity`, `notify_user`, `desktop_taskbar`), use `caller_identity.ad_callback(command, args)` — it forwards the three headers and-adds `X-Adom-Caller-Delegate: kicad`, so AD shows `chip-fetcher tab 3 (via kicad)`.+adds `X-Adom-Caller-Delegate: kicad`, so Bridge shows `chip-fetcher tab 3 (via kicad)`. For a self-initiated call (poll/timer/cleanup) pass `on_behalf=False` so it sends `caller: {aiThread: "kicad bridge (self)"}` instead of a stale forwarded thread. @@ -211,9 +211,9 @@ capture_foreground() dutifully captures KICAD and restores KICAD — garbage-in. from a clean desktop. **Testing while adom-bridge#56 blocks spawning (AB 2.0.8 python-spawn regression):** launch-the cached server yourself on AD's stable port and the relay routes verbs to it:+the cached server yourself on Bridge's stable port and the relay routes verbs to it: `C:\Python314\python.exe "%LOCALAPPDATA%\Adom Bridge\bridges-cache\kicad\server.py" --port <stablePort>`-with `ADOM_BIND_HOST=127.0.0.1` (+`ADOM_DIRECT_API_URL=http://127.0.0.1:8766`). AD adopts+with `ADOM_BIND_HOST=127.0.0.1` (+`ADOM_DIRECT_API_URL=http://127.0.0.1:8766`). Bridge adopts whatever binds the port. Remember the running process keeps its startup code — a cache refresh does NOT swap it; kill + relaunch does, and each shell call costs a human approval, so batch them.@@ -266,7 +266,7 @@ Rules borrowed from `fusion-demo` (read that skill — it's the canonical one): - **Fixed tour order** — symbol → footprint → 3D part → schematic → 2D board → 3D board. It tells the story of how a board is actually made. - **One beat per call** is the default: the AI narrates, shows the screenshot, then- calls `data.nextCall`. A bulk run also blows AD's per-request budget.+ calls `data.nextCall`. A bulk run also blows Bridge's per-request budget. - **Land it in the user's words** ("export the Gerbers", "run DRC"), never verb names. - KiCad's honest pitch vs Fusion's: free, open source, **no trial clock and no feature tier** — say it, it's a real differentiator.@@ -278,21 +278,21 @@ beats fail with "could not open via the background (plugin) path". ## Fresh-machine gotchas (all hard-won, all fixed — don't regress them) - **Install scope is mandatory.** KiCad 10's NsisMultiUser installer: bare `/S` = `rc=666660` (invalid params). Pass `/allusers /S` (elevated) or `/currentuser /S` (per-user, `%LOCALAPPDATA%\Programs\KiCad`, **no UAC**). See `handlers/upgrade.py`.-- **TLS on the portable Python.** AD's provisioned Python has no CA bundle → `CERTIFICATE_VERIFY_FAILED`. We bundle `certs/cacert.pem` (certifi) and load it in `_ssl_context()`. Any new https fetch must use that context.+- **TLS on the portable Python.** Bridge's provisioned Python has no CA bundle → `CERTIFICATE_VERIFY_FAILED`. We bundle `certs/cacert.pem` (certifi) and load it in `_ssl_context()`. Any new https fetch must use that context. - **Truncated downloads** read as EOF and pass MZ/size checks, then NSIS fails at install. `upgrade.py` HEADs `Content-Length` and refuses/deletes a size-mismatched file. - **Config doesn't exist pre-first-launch.** `%APPDATA%/kicad/<ver>/` (lib tables) is created on KiCad's first GUI run. `kicad_detect.ensure_win_user_config()` bootstraps it (seeds tables from the install template) so `install_library`/`_footprint` work on a never-launched KiCad. - **Detection cache is built once at startup.** After `kicad_upgrade` installs KiCad, `dispatch_command` re-runs `detect_all_kicad_versions()` so subsequent verbs see it without a bridge restart. Detect paths include `%LocalAppData%\Programs\KiCad` (per-user installs).-- **The bridge's env is STRIPPED — don't trust `%LOCALAPPDATA%`.** AD provisions the bridge's Python with a minimal environment where `LOCALAPPDATA` can be UNSET. A `/currentuser` (no-UAC) `kicad_upgrade` installs to `%LOCALAPPDATA%\Programs\KiCad`, but if detection keys off `os.environ["LOCALAPPDATA"]` it comes back EMPTY and the just-installed KiCad is invisible (every verb → "not installed") even though AD's own glob-based `bridge_readiness` finds it. `kicad_detect.WIN_KICAD_BASES` therefore derives the LocalAppData root from `LOCALAPPDATA` **and** `USERPROFILE\AppData\Local` **and** `Path.home()`. Root-caused on ADOMBASELINE 2026-07 (v0.9.41). Rule: never read a user-profile path from a single env var in bridge code.+- **The bridge's env is STRIPPED — don't trust `%LOCALAPPDATA%`.** Bridge provisions the bridge's Python with a minimal environment where `LOCALAPPDATA` can be UNSET. A `/currentuser` (no-UAC) `kicad_upgrade` installs to `%LOCALAPPDATA%\Programs\KiCad`, but if detection keys off `os.environ["LOCALAPPDATA"]` it comes back EMPTY and the just-installed KiCad is invisible (every verb → "not installed") even though Bridge's own glob-based `bridge_readiness` finds it. `kicad_detect.WIN_KICAD_BASES` therefore derives the LocalAppData root from `LOCALAPPDATA` **and** `USERPROFILE\AppData\Local` **and** `Path.home()`. Root-caused on ADOMBASELINE 2026-07 (v0.9.41). Rule: never read a user-profile path from a single env var in bridge code. - **Blocking dialogs stall everything.** `handlers/close_windows.py` scans EVERY window owned by a running KiCad process (by PID — `_win_scan_kicad_dialogs`, reliable; title/owner matching is NOT), auto-expires benign ones (OpenGL notice), screenshots them, and returns hints. `window_info` self-heals by default; `kicad_dismiss_dialogs {all|forceSoftwareCanvas}`. - **GPU-less hosts.** No OpenGL → the notice pops and pcbnew/3D may not render. On expiring that notice we persist `graphics.canvas_type=2` (Cairo) to `kicad_common.json`. NOTE: a truly minimal VM (Hyper-V, no GPU) may still not render the heavy editors even in Cairo — that's the hardware. Validate GUI/screenshot work on a real-GPU machine. ## Diagnostics you can call -- **`kicad_diagnostics {}` — reach for this FIRST, and never AD's approval-gated `run_script`/shell.** The bridge runs Python on the desktop with full FS/subprocess access, so it can answer every "where did KiCad install / what env do I see / what's running" question itself. Returns: `environment` (the env the BRIDGE sees — a null `LOCALAPPDATA` here is the tell for a per-user install going undetected), `kicadSearch.bases` (every path the detector scans + the version dirs / `kicad.exe` it finds), `processes` (running KiCad exes), `kicadInfo` (+ config/user dir existence). Pass `probePaths:[...]` to stat/list any path and `readFile:"..."` to peek a small config/output file. All read-only, zero shell approval. (Added v0.9.44 precisely so debugging never needs `run_script` — which pops a 1-hour shell-approval gate.)+- **`kicad_diagnostics {}` — reach for this FIRST, and never Bridge's approval-gated `run_script`/shell.** The bridge runs Python on the desktop with full FS/subprocess access, so it can answer every "where did KiCad install / what env do I see / what's running" question itself. Returns: `environment` (the env the BRIDGE sees — a null `LOCALAPPDATA` here is the tell for a per-user install going undetected), `kicadSearch.bases` (every path the detector scans + the version dirs / `kicad.exe` it finds), `processes` (running KiCad exes), `kicadInfo` (+ config/user dir existence). Pass `probePaths:[...]` to stat/list any path and `readFile:"..."` to peek a small config/output file. All read-only, zero shell approval. (Added v0.9.44 precisely so debugging never needs `run_script` — which pops a 1-hour shell-approval gate.) - `kicad_upgrade {"diagnoseOnly":true}` — token elevation type + UAC `EnableLUA` + cached-installer/truncation facts (no install). - `kicad_dismiss_dialogs {"debug":true}` — raw process-based dialog scan (PIDs + every KiCad window + class + isDialog). -## Testing on a fresh AD / VM+## Testing on a fresh Bridge / VM `refresh_bridges {name:kicad}` pulls the new cache; the RUNNING instance keeps its old code until it dies — `bridge_kill {name:kicad}` forces a fresh spawn that loads the new version and re-detects. The first verb after a kill returns a `bridge_starting` envelope — retry. Confirm the running version via `bridge_log_read` ("starting v0.9.xx"). @@ -339,7 +339,7 @@ canceled by user"; when that happens, screenshot the `#32770` error dialog to se pixels. Passing pixels with the default clicks far off-screen (can hit another window's close box and crash KiCad). v0.9.59 guards this, but always pass fractions or `{"relative": false}`. -**Capture without a round-trip:** AD-core `desktop_screenshot_window {hwnd}` returns a+**Capture without a round-trip:** Bridge-core `desktop_screenshot_window {hwnd}` returns a container-local path in `localFullPath` (already pulled) — Read it directly, no `pull_file` needed. It's also background-safe (PrintWindow), unlike `kicad_screenshot_all` which can time out capturing every window.
skills/kicad-bridge-publish/SKILL.md+6−6@@ -1,7 +1,7 @@ --- name: kicad-bridge-publish user-invocable: false-description: DEVELOPER-only PUBLISH skill (user-invocable:false — for maintainers, never offered to everyday users). The exact recipe to ship a new version of the Adom Bridge KiCad bridge to wiki.adom.inc: version lockstep, the zip build, adom-wiki repo push + release create, the skills pkg, pushing to a target AD, and verify steps. Trigger words — publish kicad bridge, release kicad bridge, ship kicad bridge, bump kicad bridge, kicad bridge manifest, kicad bridge pkg.+description: DEVELOPER-only PUBLISH skill (user-invocable:false — for maintainers, never offered to everyday users). The exact recipe to ship a new version of the Adom Bridge KiCad bridge to wiki.adom.inc: version lockstep, the zip build, adom-wiki repo push + release create, the skills pkg, pushing to a target Bridge, and verify steps. Trigger words — publish kicad bridge, release kicad bridge, ship kicad bridge, bump kicad bridge, kicad bridge manifest, kicad bridge pkg. --- # kicad-bridge-publish@@ -16,7 +16,7 @@ How to cut and publish a KiCad-bridge release. **Use the `adom-wiki` CLI for ALL They must match. `bridge.json` also carries `updateManifestUrl` (durable auto-update), `docs` (your page), `homepage` (a **wiki.adom.inc** URL, NEVER github), `detect` (host-app paths incl. `%LocalAppData%\Programs\KiCad`), `verbPrefixes`, complete `verbs[]`, `statusVerb`. -## 2. Build the zip — KEEP IT TINY (it is auto-bundled into AD)+## 2. Build the zip — KEEP IT TINY (it is auto-bundled into Bridge) The runtime zip ships inside Adom Bridge for **every** user, so it carries ONLY what the bridge needs to RUN. No docs, no images, no skills, no samples. (Audited 2026-07-15: the zip@@ -26,11 +26,11 @@ functional change.) **KEEP** (runtime-required): - all `*.py` (server + `handlers/` + `plugin_payload/`)-- `certs/cacert.pem` — AD's portable Python has no CA bundle+- `certs/cacert.pem` — Bridge's portable Python has no CA bundle - `*.ps1` (`launch_editor.ps1`, `navigate_symbol.ps1`) — UIA scripts the handlers shell out to - `templates/blank-board.kicad_pcb` — read by `handlers/place_footprint.py` - `bridge.json`, `BRIDGE_VERSION`, `start.bat`, `start.sh`-- root `SKILL.md` ONLY (the runtime skill `adom-desktop-kicad`, `public: true` — AD installs it)+- root `SKILL.md` ONLY (the runtime skill `adom-desktop-kicad`, `public: true` — Bridge installs it) **EXCLUDE** (everything else): `skills/**` (container-side docs — they ship in the PKG, duplicating them here is pure bloat),@@ -57,12 +57,12 @@ adom-wiki release create adom/kicad-bridge <ver> \ --binary-path adom-bridge-kicad-v<ver>.zip ``` -`repo push` writes the files to the page `/files` (same place AD's bridge_cache fetches from). `release create` also registers a proper Release (SDK Artifact 1). For a field with no dedicated verb use `adom-wiki api -X PUT /api/v1/pages/... --body ...`, not curl.+`repo push` writes the files to the page `/files` (same place Bridge's bridge_cache fetches from). `release create` also registers a proper Release (SDK Artifact 1). For a field with no dedicated verb use `adom-wiki api -X PUT /api/v1/pages/... --body ...`, not curl. ## 4. Publish the skills pkg (container docs) **The pkg is skills-only — it is NOT the bridge.** The pkg teaches a container agent the `kicad_*`-verbs; the **release zip** is what AD downloads and runs as the bridge runtime. Users conflate them+verbs; the **release zip** is what Bridge downloads and runs as the bridge runtime. Users conflate them constantly, so say it — but say it in **ONE LINE**. ⚠️ **`description` is rendered inside the page's Install card. Keep it to one or two sentences.**
skills/kicad-interaction/SKILL.md+165−165@@ -1,165 +1,165 @@-name: kicad-interaction -description: Foundational rules for interacting with KiCad on the user's desktop via adom-desktop. Read BEFORE any kicad_open_board, kicad_open_3d_viewer, kicad_send_key, or kicad_screenshot_all call. Covers state checks, window management, dialog handling, file updates, and the 3D viewer workflow. Trigger words — open in kicad, kicad desktop, send to kicad, kicad 3d viewer, kicad screenshot, kicad dialog, kicad file warning, open board in kicad, kicad pcb editor. - -# kicad-interaction - -Read this skill BEFORE any KiCad desktop operation. It prevents the most common mistakes: duplicate windows, unhandled dialogs, and blind file pushes. - -## Dialogs: the bridge auto-expires the pointless ones, and tells you what it closed (v0.9.29+) - -KiCad throws blocking modal dialogs constantly (OpenGL fallback notices, "older file format" warnings, save prompts, config nags). A blocking dialog **stalls every other verb** — an editor open can't complete, a screenshot captures only the dialog. The bridge now handles this **programmatically** so you rarely babysit it: - -- **`kicad_window_info` auto-expires *benign* dialogs by default.** It scans EVERY window owned by any running KiCad process (matched by PID — reliable, never misses one), clicks OK on the harmless ones (e.g. *"Could not use OpenGL, falling back to software rendering"*, the *"Welcome to KiCad — starting for the first time"* first-run wizard, and the *"created by an older version of KiCad"* conversion notice), and reports them in `data.autoDismissed[]` + `data._autoDismissHint`. Pass `{"autoDismiss": false}` to inspect without touching them. -- **A NON-benign dialog is left up and reported** in `data.modalDialogs[]` with its `body` text (read from the dialog's Static controls — no screenshot needed) so YOU decide: read `body`, then dismiss with `kicad_send_key {hwnd, key:"return"}` (OK) / `"escape"` (Cancel), or surface it to the user. -- **`kicad_dismiss_dialogs`** — force-clear on demand. `{}` = benign only; `{"all": true}` = clear EVERY KiCad dialog (use after you've read a modal you want gone). It **screenshots each dialog before expiring it** (returned in `dismissed[].screenshot`) so you can show the user what was auto-closed and let them decide what it meant. Returns `dismissed[]`, `remaining[]`, hints. - -**GPU-less hosts (VMs / RDP / servers):** KiCad tries OpenGL, fails, and pcbnew/3D may not render. When the bridge expires the OpenGL notice it also persists `graphics.canvas_type=2` (Cairo software) to `kicad_common.json` so future launches skip OpenGL — or force it up front with `kicad_dismiss_dialogs {"forceSoftwareCanvas": true}` then reopen the editor. (Note: a truly headless VM with no usable graphics stack may still not render the heavy editors or the 3D viewer even in Cairo. Last resort only: `kicad_enable_software_opengl` deploys Mesa `llvmpipe` (CPU OpenGL) so even 3D renders — it's SLOW and for GPU-less hosts only; do NOT suggest it proactively on a normal machine.) - -**Pattern:** `open_* → window_info (auto-clears benign) → screenshot_all`. If an open "fails," a benign modal was almost certainly blocking it — call `window_info` or `kicad_dismiss_dialogs` and retry. - -## Rule 1 — Always check state first - -Before ANY KiCad operation, run: - -```bash -adom-desktop kicad_window_info '{}' -``` - -This returns all open editors, 3D viewers, and modal dialogs with their hwnds. Parse the response and decide your next action based on what's already running. - -**If the board is already open and you're pushing an updated file:** -```bash -adom-desktop kicad_close '{}' # close everything -sleep 2 -adom-desktop kicad_open_board '{"filePath":"C:/Users/john/Downloads/board.kicad_pcb"}' -``` - -**If the board is already open and you just need the 3D viewer:** -```bash -# Reuse the existing editor — get its hwnd from window_info -adom-desktop kicad_open_3d_viewer '{"hwnd": <existing_pcb_editor_hwnd>}' -``` - -**If KiCad is not running:** -```bash -adom-desktop kicad_open_board '{"filePath":"C:/Users/john/Downloads/board.kicad_pcb"}' -``` - -Never call `kicad_open_board` without checking `kicad_window_info` first. - -## Rule 2 — Never open duplicate instances - -Opening the same `.kicad_pcb` file twice triggers a "File Open Warning" dialog about interleaved saves. This is always wrong. The correct pattern when updating a file: - -1. `kicad_window_info` — see what's open -2. `kicad_close` — close everything -3. `send_files` — push the updated file -4. `kicad_open_board` — open fresh - -Do NOT open a second instance and then dismiss the warning. Close first, open once. - -## Rule 3 — Always check for dialogs after operations - -After `kicad_open_board`, `kicad_open_3d_viewer`, or any operation that might trigger a dialog: - -```bash -sleep 3 -adom-desktop kicad_window_info '{}' -``` - -Check `hasModalDialogs`. If true: -1. Screenshot the dialog: `desktop_screenshot_window '{"hwnd": <dialog_hwnd>}'` -2. Read the screenshot to understand the dialog -3. Dismiss with `kicad_send_key '{"key": "Enter", "hwnd": <dialog_hwnd>}'` or `"Escape"` as appropriate -4. Re-check `kicad_window_info` to confirm it cleared - -## Rule 4 — File transfer before open - -KiCad runs on Windows. The `.kicad_pcb` file lives on Docker. Always send first, then open: - -```bash -# Send files (send large files like STEP/WRL separately to avoid 413) -adom-desktop send_files '{"filePaths":["/docker/path/board.kicad_pcb"],"category":"kicad"}' - -# Files land in C:/Users/john/Downloads/ by default -adom-desktop kicad_open_board '{"filePath":"C:/Users/john/Downloads/board.kicad_pcb"}' -``` - -If you also have sidecar files (STEP/WRL models referenced with relative paths), send those too — they must land in the same directory as the PCB. - -## Rule 5 — 3D viewer workflow - -```bash -# 1. Check state -adom-desktop kicad_window_info '{}' - -# 2. Open 3D viewer from the PCB editor (use hwnd from step 1) -adom-desktop kicad_open_3d_viewer '{"hwnd": <pcb_editor_hwnd>}' - -# 3. Wait for render + confirm viewer appeared -sleep 5 -adom-desktop kicad_window_info '{}' - -# 4. Screenshot the 3D viewer window -adom-desktop desktop_screenshot_window '{"hwnd": <3d_viewer_hwnd>}' - -# 5. Read the screenshot to verify models rendered -``` - -### Edge-mount connectors (USB-C, micro-USB, barrel jacks) - -USB-C, micro-USB, barrel jacks, and other connectors that users plug cables into must overhang the board edge. After placing the footprint, verify in the 3D viewer that the connector mouth extends past the Edge.Cuts outline. If it doesn't, adjust the footprint Y position using the fab-layer board-edge alignment line. - -**Verification workflow:** -1. Open the 3D viewer after placing an edge-mount connector -2. Screenshot and check that the connector body sticks out past the board edge -3. If it's flush or recessed, the user cannot plug in a cable -- adjust the footprint Y coordinate -4. The footprint's fab layer typically includes a line marking where the board edge should intersect the component - -## Rule 6 — Available KiCad commands - -``` -kicad_open_board Open a .kicad_pcb in the PCB Editor -kicad_open_3d_viewer Open 3D viewer from a PCB Editor (needs hwnd) -kicad_close_3d_viewer Close the 3D viewer -kicad_close Close all KiCad windows -kicad_window_info List all open editors, viewers, and dialogs -kicad_send_key Single key to an EXACT hwnd (dialog Enter/Escape). For CHORDS use AD's desktop_press_key. -kicad_screenshot_all Screenshot all KiCad windows -kicad_adom_library_status Check Adom library installation -kicad_install_symbol Install a symbol to KiCad's library -kicad_install_footprint Install a footprint to KiCad's library -kicad_install_library Install a full library -kicad_run_drc Run Design Rule Check -desktop_screenshot_window Screenshot a specific window by hwnd -desktop_bring_to_front Focus a window by hwnd -``` - -## Rule 7 — Pup-over-webview preference for tool dashboards - -User prefers tool dashboards (chip-fetcher, chiplinter, etc.) in **pup windows**, not Hydrogen webview tabs. The canonical pattern is two pup windows: - -- **Dashboard pup window** (`sessionId: "chip-fetcher-dashboard"` or `"<tool>-dashboard"`) — the live status / card view. Never navigate this mid-batch. -- **Scrape / work pup window** (`sessionId: "chip-fetcher-scrape"` or `"<tool>-scrape"`) — where vendor pages, logins, and downloads happen. - -Both windows MUST use the same `profile` (e.g. `profile: "chip-fetcher"`) so cookies and logins persist. Only the `sessionId` varies. This two-pup pattern keeps the user's view of progress (dashboard) completely isolated from the active scraping / interaction window. - -## Rule 8 — Desktop connection prerequisite - -Before any KiCad command, verify the desktop is connected: - -```bash -adom-desktop ping -``` - -If disconnected, start the relay: -```bash -if ! curl -sf http://127.0.0.1:8766/health >/dev/null 2>&1; then - nohup adom-desktop serve > /tmp/adom-desktop-relay.log 2>&1 & - disown - sleep 3 -fi -adom-desktop ping -``` +---+name: kicad-interaction+description: Foundational rules for interacting with KiCad on the user's desktop via adom-desktop. Read BEFORE any kicad_open_board, kicad_open_3d_viewer, kicad_send_key, or kicad_screenshot_all call. Covers state checks, window management, dialog handling, file updates, and the 3D viewer workflow. Trigger words — open in kicad, kicad desktop, send to kicad, kicad 3d viewer, kicad screenshot, kicad dialog, kicad file warning, open board in kicad, kicad pcb editor.+---++# kicad-interaction++Read this skill BEFORE any KiCad desktop operation. It prevents the most common mistakes: duplicate windows, unhandled dialogs, and blind file pushes.++## Dialogs: the bridge auto-expires the pointless ones, and tells you what it closed (v0.9.29+)++KiCad throws blocking modal dialogs constantly (OpenGL fallback notices, "older file format" warnings, save prompts, config nags). A blocking dialog **stalls every other verb** — an editor open can't complete, a screenshot captures only the dialog. The bridge now handles this **programmatically** so you rarely babysit it:++- **`kicad_window_info` auto-expires *benign* dialogs by default.** It scans EVERY window owned by any running KiCad process (matched by PID — reliable, never misses one), clicks OK on the harmless ones (e.g. *"Could not use OpenGL, falling back to software rendering"*, the *"Welcome to KiCad — starting for the first time"* first-run wizard, and the *"created by an older version of KiCad"* conversion notice), and reports them in `data.autoDismissed[]` + `data._autoDismissHint`. Pass `{"autoDismiss": false}` to inspect without touching them.+- **A NON-benign dialog is left up and reported** in `data.modalDialogs[]` with its `body` text (read from the dialog's Static controls — no screenshot needed) so YOU decide: read `body`, then dismiss with `kicad_send_key {hwnd, key:"return"}` (OK) / `"escape"` (Cancel), or surface it to the user.+- **`kicad_dismiss_dialogs`** — force-clear on demand. `{}` = benign only; `{"all": true}` = clear EVERY KiCad dialog (use after you've read a modal you want gone). It **screenshots each dialog before expiring it** (returned in `dismissed[].screenshot`) so you can show the user what was auto-closed and let them decide what it meant. Returns `dismissed[]`, `remaining[]`, hints.++**GPU-less hosts (VMs / RDP / servers):** KiCad tries OpenGL, fails, and pcbnew/3D may not render. When the bridge expires the OpenGL notice it also persists `graphics.canvas_type=2` (Cairo software) to `kicad_common.json` so future launches skip OpenGL — or force it up front with `kicad_dismiss_dialogs {"forceSoftwareCanvas": true}` then reopen the editor. (Note: a truly headless VM with no usable graphics stack may still not render the heavy editors or the 3D viewer even in Cairo. Last resort only: `kicad_enable_software_opengl` deploys Mesa `llvmpipe` (CPU OpenGL) so even 3D renders — it's SLOW and for GPU-less hosts only; do NOT suggest it proactively on a normal machine.)++**Pattern:** `open_* → window_info (auto-clears benign) → screenshot_all`. If an open "fails," a benign modal was almost certainly blocking it — call `window_info` or `kicad_dismiss_dialogs` and retry.++## Rule 1 — Always check state first++Before ANY KiCad operation, run:++```bash+adom-desktop kicad_window_info '{}'+```++This returns all open editors, 3D viewers, and modal dialogs with their hwnds. Parse the response and decide your next action based on what's already running.++**If the board is already open and you're pushing an updated file:**+```bash+adom-desktop kicad_close '{}' # close everything+sleep 2+adom-desktop kicad_open_board '{"filePath":"C:/Users/john/Downloads/board.kicad_pcb"}'+```++**If the board is already open and you just need the 3D viewer:**+```bash+# Reuse the existing editor — get its hwnd from window_info+adom-desktop kicad_open_3d_viewer '{"hwnd": <existing_pcb_editor_hwnd>}'+```++**If KiCad is not running:**+```bash+adom-desktop kicad_open_board '{"filePath":"C:/Users/john/Downloads/board.kicad_pcb"}'+```++Never call `kicad_open_board` without checking `kicad_window_info` first.++## Rule 2 — Never open duplicate instances++Opening the same `.kicad_pcb` file twice triggers a "File Open Warning" dialog about interleaved saves. This is always wrong. The correct pattern when updating a file:++1. `kicad_window_info` — see what's open+2. `kicad_close` — close everything+3. `send_files` — push the updated file+4. `kicad_open_board` — open fresh++Do NOT open a second instance and then dismiss the warning. Close first, open once.++## Rule 3 — Always check for dialogs after operations++After `kicad_open_board`, `kicad_open_3d_viewer`, or any operation that might trigger a dialog:++```bash+sleep 3+adom-desktop kicad_window_info '{}'+```++Check `hasModalDialogs`. If true:+1. Screenshot the dialog: `desktop_screenshot_window '{"hwnd": <dialog_hwnd>}'`+2. Read the screenshot to understand the dialog+3. Dismiss with `kicad_send_key '{"key": "Enter", "hwnd": <dialog_hwnd>}'` or `"Escape"` as appropriate+4. Re-check `kicad_window_info` to confirm it cleared++## Rule 4 — File transfer before open++KiCad runs on Windows. The `.kicad_pcb` file lives on Docker. Always send first, then open:++```bash+# Send files (send large files like STEP/WRL separately to avoid 413)+adom-desktop send_files '{"filePaths":["/docker/path/board.kicad_pcb"],"category":"kicad"}'++# Files land in C:/Users/john/Downloads/ by default+adom-desktop kicad_open_board '{"filePath":"C:/Users/john/Downloads/board.kicad_pcb"}'+```++If you also have sidecar files (STEP/WRL models referenced with relative paths), send those too — they must land in the same directory as the PCB.++## Rule 5 — 3D viewer workflow++```bash+# 1. Check state+adom-desktop kicad_window_info '{}'++# 2. Open 3D viewer from the PCB editor (use hwnd from step 1)+adom-desktop kicad_open_3d_viewer '{"hwnd": <pcb_editor_hwnd>}'++# 3. Wait for render + confirm viewer appeared+sleep 5+adom-desktop kicad_window_info '{}'++# 4. Screenshot the 3D viewer window+adom-desktop desktop_screenshot_window '{"hwnd": <3d_viewer_hwnd>}'++# 5. Read the screenshot to verify models rendered+```++### Edge-mount connectors (USB-C, micro-USB, barrel jacks)++USB-C, micro-USB, barrel jacks, and other connectors that users plug cables into must overhang the board edge. After placing the footprint, verify in the 3D viewer that the connector mouth extends past the Edge.Cuts outline. If it doesn't, adjust the footprint Y position using the fab-layer board-edge alignment line.++**Verification workflow:**+1. Open the 3D viewer after placing an edge-mount connector+2. Screenshot and check that the connector body sticks out past the board edge+3. If it's flush or recessed, the user cannot plug in a cable -- adjust the footprint Y coordinate+4. The footprint's fab layer typically includes a line marking where the board edge should intersect the component++## Rule 6 — Available KiCad commands++```+kicad_open_board Open a .kicad_pcb in the PCB Editor+kicad_open_3d_viewer Open 3D viewer from a PCB Editor (needs hwnd)+kicad_close_3d_viewer Close the 3D viewer+kicad_close Close all KiCad windows+kicad_window_info List all open editors, viewers, and dialogs+kicad_send_key Single key to an EXACT hwnd (dialog Enter/Escape). For CHORDS use Bridge's desktop_press_key.+kicad_screenshot_all Screenshot all KiCad windows+kicad_adom_library_status Check Adom library installation+kicad_install_symbol Install a symbol to KiCad's library+kicad_install_footprint Install a footprint to KiCad's library+kicad_install_library Install a full library+kicad_run_drc Run Design Rule Check+desktop_screenshot_window Screenshot a specific window by hwnd+desktop_bring_to_front Focus a window by hwnd+```++## Rule 7 — Pup-over-webview preference for tool dashboards++User prefers tool dashboards (chip-fetcher, chiplinter, etc.) in **pup windows**, not Hydrogen webview tabs. The canonical pattern is two pup windows:++- **Dashboard pup window** (`sessionId: "chip-fetcher-dashboard"` or `"<tool>-dashboard"`) — the live status / card view. Never navigate this mid-batch.+- **Scrape / work pup window** (`sessionId: "chip-fetcher-scrape"` or `"<tool>-scrape"`) — where vendor pages, logins, and downloads happen.++Both windows MUST use the same `profile` (e.g. `profile: "chip-fetcher"`) so cookies and logins persist. Only the `sessionId` varies. This two-pup pattern keeps the user's view of progress (dashboard) completely isolated from the active scraping / interaction window.++## Rule 8 — Desktop connection prerequisite++Before any KiCad command, verify the desktop is connected:++```bash+adom-desktop ping+```++If disconnected, start the relay:+```bash+if ! curl -sf http://127.0.0.1:8766/health >/dev/null 2>&1; then+ nohup adom-desktop serve > /tmp/adom-desktop-relay.log 2>&1 &+ disown+ sleep 3+fi+adom-desktop ping+```
skills/kicad-tour/SKILL.md+5−5@@ -6,7 +6,7 @@ description: "Run a guided KiCad tour on the user's laptop via adom-desktop — # kicad-tour A guided 8-step walkthrough of the KiCad bridge end-to-end. Runs from any cloud-Docker session (or HD) — every command dispatches through `adom-desktop kicad_*`+Docker session (or Hydrogen) — every command dispatches through `adom-desktop kicad_*` to the bridge on the user's laptop. Each step produces a visible change in KiCad that a viewer can see (and that the tour-runner script can screenshot). @@ -16,8 +16,8 @@ this tour follows. ## When to use - User asks "show me what KiCad can do" / "walk me through kicad"-- Demo / onboarding flow when first connecting AD to KiCad-- Sanity check after a fresh AD install — proves every bridge verb works+- Demo / onboarding flow when first connecting Bridge to KiCad+- Sanity check after a fresh Bridge install — proves every bridge verb works ## The 8-step tour (sequential, ~90s end-to-end) @@ -37,7 +37,7 @@ PACK=tour-pack-rp2040 The packs are mounted by the bridge at: - `C:/Github/adom-desktop/plugins/kicad/<PACK>/`-- (When AD is installer-deployed, look under+- (When Bridge is installer-deployed, look under `%LOCALAPPDATA%/Adom Bridge/bridges-cache/kicad/<PACK>/` — adjust paths.) ### Step 1 — install the custom symbol + footprint libraries@@ -102,7 +102,7 @@ adom-desktop kicad_open_3d_viewer '{"editor":"fp"}' If this returns "3D Viewer did not open from Footprint Editor" on KiCad 10, either skip to step 7 (the board's 3D viewer is the more reliable showcase-anyway) OR send `Alt+3` via AD's chord-aware keypress verb (focus the Footprint+anyway) OR send `Alt+3` via Bridge's chord-aware keypress verb (focus the Footprint Editor first): ```bash