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.70 (issue #15): rename cut-over — updateManifestUrl/docs/homepage/hero + mesa URL to the kicad-bridge slug; Adom Desktop -> Adom Bridge and HD -> Hydrogen across user-facing copy and KiCad-visible library descriptions
18 files changed
+588−257
BRIDGE_VERSION+1−1CONTRIBUTING.md+7−7README.md+55−27SKILL.md+144−144adom-bridge-kicad-manifest.json+7−7adom-bridge-kicad-v0.9.70.zipadom_library.py+26−5bridge.json+5−5handlers/demo.py+4−4handlers/install_footprint.py+9−2handlers/install_library.py+40−7handlers/software_opengl.py+2−2handlers/upgrade.py+267−25proc.py+1−1server.py+4−4skills/kicad-bridge-dev/SKILL.md+4−4skills/kicad-bridge-publish/SKILL.md+11−11skills/kicad-tour/SKILL.md+1−1BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.69+0.9.70
CONTRIBUTING.md+7−7@@ -1,6 +1,6 @@ # Contributing -This bridge is part of the [Adom Desktop](https://github.com/adom-inc/adom-desktop) bridge ecosystem. It lives in its own repo (split from the main monorepo in 2026-05) specifically so the community can fork and improve it without coordinating with Adom Desktop's release cycle.+This bridge is part of the [Adom Bridge](https://github.com/adom-inc/adom-desktop) bridge ecosystem. It lives in its own repo (split from the main monorepo in 2026-05) specifically so the community can fork and improve it without coordinating with Adom Bridge's release cycle. ## Two-way street @@ -9,22 +9,22 @@ This bridge is part of the [Adom Desktop](https://github.com/adom-inc/adom-deskt ## How shipping works -1. **Develop locally.** Clone this repo, edit, test. The bridge runs alongside an installed [Adom Desktop](https://github.com/adom-inc/adom-desktop); use `adom-desktop bridge_install` with a local `file://` manifest URL to test your changes against a real GUI.+1. **Develop locally.** Clone this repo, edit, test. The bridge runs alongside an installed [Adom Bridge](https://github.com/adom-inc/adom-desktop); use `adom-desktop bridge_install` with a local `file://` manifest URL to test your changes against a real GUI. 2. **Open a PR** against `main`. The CI in `.github/workflows/validate.yml` checks that `bridge.json` is valid + verb prefixes look right + the BRIDGE_VERSION was bumped vs. main. 3. **Maintainer reviews + merges.** No squash-merge — we preserve your authored commits.-4. **Maintainer tags + ships.** A maintainer bumps the version in lockstep (`bridge.json` / `BRIDGE_VERSION` / the manifest), builds the runtime zip and publishes it as a **Release asset** with `adom-wiki release create` + `release upload`, alongside the manifest on [the Adom Wiki](https://wiki.adom.inc/adom/adom-desktop-bridges). Anyone running Adom Desktop v1.7.19+ picks it up within 4 hours or on the next `adom-desktop refresh_bridges`.+4. **Maintainer tags + ships.** A maintainer bumps the version in lockstep (`bridge.json` / `BRIDGE_VERSION` / the manifest), builds the runtime zip and publishes it as a **Release asset** with `adom-wiki release create` + `release upload`, alongside the manifest on [the Adom Wiki](https://wiki.adom.inc/adom/adom-bridge-sdk). Anyone running Adom Bridge v1.7.19+ picks it up within 4 hours or on the next `adom-desktop refresh_bridges`. We DON'T auto-ship from GitHub Actions today. Reason: publishing needs an Adom Wiki API token (`adom-wiki` auth), and we keep that secret on maintainer machines rather than in GH Actions secrets. The community trust model around auto-publishing community PRs to a shared wiki space is part of a separate effort ("Chunk D" in adom-desktop's TODO.md) — comment on an issue if you want to help design that. ## Bridge.json schema -See [the SDK guide on the Adom Wiki](https://wiki.adom.inc/adom/adom-desktop-bridges) for the authoritative schema. Quick summary:+See [the SDK guide on the Adom Wiki](https://wiki.adom.inc/adom/adom-bridge-sdk) for the authoritative schema. Quick summary: - `manifest_version: 1` - `name` — must match the directory name in adom-desktop's `plugins/` (when bundled) - `version` — semver, bumped on every PR that ships-- `spawn.port: 0` — **always use 0** as of Adom Desktop v1.8.31+. Adom Desktop picks an OS-assigned ephemeral port and passes it via `--port <N>` to your bridge process. Don't hardcode.-- `spawn.healthEndpoint: "/status"` — path-only (recommended). Adom Desktop builds the URL using the runtime port.+- `spawn.port: 0` — **always use 0** as of Adom Bridge v1.8.31+. Adom Bridge picks an OS-assigned ephemeral port and passes it via `--port <N>` to your bridge process. Don't hardcode.+- `spawn.healthEndpoint: "/status"` — path-only (recommended). Adom Bridge builds the URL using the runtime port. - `verbPrefixes: ["<name>_"]` — the CLI verb prefixes this bridge owns. ## Versioning@@ -61,7 +61,7 @@ bash scripts/demo-thirdparty-bridge.sh # in adom-desktop repo When adom-desktop was a monorepo, all bridges shipped on adom-desktop's NSIS release cycle — meaning a community contributor couldn't release a KiCad fix without waiting for an adom-desktop maintainer to cut a new desktop installer. As of 2026-05 the bridges live in independent repos so: - Community can release at their own cadence-- Adom Desktop's installer ships a "known-good" snapshot but auto-updates from the wiki for newer versions+- Adom Bridge's installer ships a "known-good" snapshot but auto-updates from the wiki for newer versions - A maintainer outside Adom Industries can take over a bridge (e.g. a KiCad 11 expert maintains the KiCad bridge) without commit access to adom-desktop itself See [adom-desktop TODO.md → Chunks C/D](https://github.com/adom-inc/adom-desktop/blob/main/TODO.md) for the broader design.
README.md+55−27@@ -1,28 +1,14 @@-# Adom Desktop — KiCad Bridge+# Adom Bridge — KiCad Bridge -A **reverse bridge** that lets Adom Desktop (AD) — and the AI driving it — control the user's own+A **reverse bridge** that lets Adom Bridge (AD) — 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. -> ### Two artifacts on this page — which one do you want?->-> | | **Release ZIP** (`adom-bridge-kicad-v*.zip`) | **Package** (`adom-wiki pkg install adom/adom-desktop-kicad-bridge`) |-> |---|---|---|-> | What it is | The **bridge runtime** — the actual Python server that drives KiCad | **Skills + docs only**, for a container/agent |-> | Who loads it | **Adom Desktop** downloads it automatically via `adom-bridge-kicad-manifest.json` | You, into a container, to teach an agent the `kicad_*` verbs |-> | Contains | code, certs, UIA scripts, board template | `SKILL.md` files (no binaries, no images) |-> | You install it manually? | **No** — AD handles it | Yes |->-> **The package is not the bridge.** Installing the package does not give you a working-> bridge; AD loading the release zip does. Both are kept deliberately small — the zip is-> auto-bundled into AD, so it ships only what the runtime needs.-- KiCad is the user's **host app**. The bridge *detects* an existing KiCad and, if none is present, *installs* one for them — it never asks the user to download or click through anything by hand. -- **Page / docs:** https://wiki.adom.inc/adom/adom-desktop-kicad-bridge-- **Bridge SDK:** https://wiki.adom.inc/adom/adom-desktop-bridges+- **Page / docs:** https://wiki.adom.inc/adom/kicad-bridge+- **Bridge SDK:** https://wiki.adom.inc/adom/adom-bridge-sdk - **Verb namespace:** `kicad_*` · **Status verb:** `kicad_bridge_status` · **Health:** `GET /status` > Every screenshot in this README was captured on **ADOMBASELINE**, a stock Hyper-V VM with **no@@ -33,10 +19,52 @@ KiCad is the user's **host app**. The bridge *detects* an existing KiCad and, if ## Demo — narrated live editor tour (real screen recording) -[](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-bridge-demo.mp4)+[](https://wiki.adom.inc/blob/app/kicad-bridge/kicad-bridge-demo.mp4)++**▶ [Watch the demo](https://wiki.adom.inc/blob/app/kicad-bridge/kicad-bridge-demo.mp4)** (48s, 🔊 narrated) — the bridge driving real KiCad on a Windows VM: **select** a footprint and **zoom** the copper in the PCB editor, **rotate** the assembled board in 3D, then tour the schematic, symbol, and footprint editors. Genuine screen recording of the live windows (zoom / select / 3D rotation), narrated section by section — not animated stills. -**▶ [Watch the demo](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-bridge-demo.mp4)** (48s, 🔊 narrated) — the bridge driving real KiCad on a Windows VM: **select** a footprint and **zoom** the copper in the PCB editor, **rotate** the assembled board in 3D, then tour the schematic, symbol, and footprint editors. Genuine screen recording of the live windows (zoom / select / 3D rotation), narrated section by section — not animated stills. +---++## The demo verb — `kicad_demo`++One verb that shows the whole bridge off, built for the AI that demos Adom Bridge+during Hydrogen's install. Six beats, in the order a hardware person thinks:++| Beat | Shows |+|---|---|+| 1 | the **schematic symbol** in the user's own Symbol Editor |+| 2 | the **footprint** for that same part |+| 3 | that part in **3D** |+| 4 | a **schematic** using it (U1 + two 10k resistors) |+| 5 | the **2D board layout** |+| 6 | the finished **board in 3D** |++```bash+adom-desktop kicad_demo '{}' # start: prepares, warms KiCad, opens beat 1+adom-desktop kicad_demo '{"step":"footprint"}' # ...then follow data.nextCall each time+adom-desktop kicad_demo '{"all":true}' # bulk (non-interactive; slower than one request budget)+adom-desktop kicad_close '{"force":true}' # tear down+```++**It narrates itself.** Every beat returns `say` (a line to speak), `pointOut`+(what's actually on screen), `window` (match it in `kicad_screenshot_all`) and+`nextCall`. One beat per call is the default precisely so the AI can talk, screenshot,+then move on.++**It ships nothing.** The two demo parts, the schematic and the board are all+generated in code, and the 3D bodies come from KiCad's *own* bundled+`3dmodels/*.3dshapes` — so the runtime zip stays small and the 3D views are real.+Parts install into the user's real `Adom` library; the project lands in+`Documents/adom-kicad-demo`.++**No KiCad? It offers to install it.** `kicad_demo` returns an offer instead of an+error; `kicad_demo '{"installKiCad": true}'` silently installs the official build+(per-user, no UAC) and then runs the tour. Watching the AI install your EDA tool is+itself a good demo beat.++**It never takes your screen.** Every window opens in the background and all input is+window-targeted (UIA / `PostMessage`), so you can keep working while it runs. --- @@ -47,7 +75,7 @@ server.py`, `port: 0` — AD picks the port and passes `ADOM_BIND_HOST`). It spe (`POST /command`, `GET /status`) and reaches KiCad through **four control surfaces**, picking the lightest one that can do the job. -+ | # | Surface | Used for | Where | |---|---------|----------|-------|@@ -67,33 +95,33 @@ generated by `tour-pack-rp2040/` (an `AdomRP2040` symbol + `QFN-56_AdomRP2040` f The RP2040 breakout: the MCU (U1), USB-C, a 12 MHz crystal, decoupling, and mounting pins. -+ ### Symbol editor — `kicad_open_symbol_editor` Opens straight to a symbol (`kicad_install_symbol` puts it in a user library first). -+ ### Footprint editor — `kicad_open_footprint_editor` `{"footprintName":"QFN-56_AdomRP2040","library":"AdomRP2040"}` loads the part directly — 56 pins + thermal pad, 7×7 mm, 0.4 mm pitch, courtyard and silkscreen. -+ ### PCB editor (2D) — `kicad_open_board` The routed breakout — copper, silkscreen, the QFN-56 land pattern, mounting holes. -+ ### 3D viewer — `kicad_open_3d_viewer {"editor":"pcb"}` The full board in 3D — board body, the USB-C connector's 3D model, the QFN chip body, SMD parts, plated through-holes. **Rendered on the CPU via the software-OpenGL fallback (below); reload 1.7 s.** -+ --- @@ -129,7 +157,7 @@ Auto-expired benign dialogs include: - **"Welcome to KiCad — starting for the first time"** first-run wizard (dismissing accepts defaults). - **"This file was created by an older version of KiCad"** conversion notice. -+ Verbs:
SKILL.md+144−144@@ -1,144 +1,144 @@-name: adom-desktop-kicad -public: true -description: "Launch and drive KiCad on the user's laptop (KiCad 7/8/9/10 supported, multi-version side-by-side) from this container via the adom-desktop CLI. Open schematics/boards/symbols/footprints, run DRC, install sym-lib-table / fp-lib-table libraries, show the 3D viewer, capture KiCad window screenshots, send keyboard shortcuts and click coordinates. Trigger words: kicad, open schematic, open board, open pcb, open symbol, open footprint, KiCad symbol editor, KiCad footprint editor, KiCad 3D viewer, run DRC, install KiCad library, sym-lib-table, fp-lib-table, KiCad screenshot, kicad_send_key, kicad_click, kicad_window_info, kicad_screenshot_all, close KiCad, kicad versions, kicad_list_versions, KiCad bridge." - -# adom-desktop — KiCad bridge - -All commands dispatch to the KiCad Python bridge running alongside the desktop app. Invoke from this container via Bash: - -```bash -adom-desktop kicad_<action> '<json_args>' -``` - -The leading `kicad_` routes the call to the KiCad plugin; the `<action>` names below are the plugin-side command names. - -## FIRST-TIME / COLD-START — read before you panic - -A fresh machine can be missing KiCad, or the reverse-bridge plugin auto-installs on your first call. These states are **EXPECTED** — don't declare defeat. Branch on the stable `errorCode` (not the prose), and relay the returned `_hint` to the user verbatim. - -| 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. | -| `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. | - -**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). - -## 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.) -- **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. - -## Multi-version (KiCad 9 + KiCad 10 side-by-side) - -Users can have multiple KiCad versions installed at the same time. The bridge auto-discovers everything under `C:/Program Files/KiCad/<version>/` and **defaults to the newest installed version** if you don't specify one. - -```bash -# Discover what's installed (run this first if unsure) -adom-desktop kicad_list_versions '{}' -# → {"versions":[{"version":"10.0", "default":true, ...}, {"version":"9.0", ...}], "default":"10.0", "count":2} -``` - -Every `kicad_open_*` (and `install_*`, `run_drc`) command accepts an optional **`kicadVersion`** arg to pin a specific install: - -```bash -# Open in KiCad 10 (newest — default if you omit kicadVersion) -adom-desktop kicad_open_board '{"filePath":"C:/designs/foo.kicad_pcb"}' - -# Open in KiCad 9 explicitly (e.g. to check that an older project still works) -adom-desktop kicad_open_board '{"filePath":"C:/designs/foo.kicad_pcb","kicadVersion":"9.0"}' -``` - -If you pass a version that's not installed, the bridge returns a hint listing the installed versions. The success response includes `kicadVersionUsed` so you can confirm which install actually ran. - -> **Note on libraries / config**: each KiCad version has its own `sym-lib-table` and `fp-lib-table` (in `%APPDATA%/kicad/<version>/`). `kicad_install_library` writes to the table of whichever version was selected — so if the user installs a library and then asks "open it in KiCad 9", you may need to install it under both versions. - -## "What EDA tools do I have?" — readiness & host-app detection - -**KiCad is the user's HOST APP. Adom Desktop *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. - -- 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. -- 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 - -| 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_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` | -| `kicad_open_board` | `open_board` | Open a `.kicad_pcb` in the PCB Editor | `filePath`, optional `kicadVersion` | -| `kicad_open_symbol_editor` | `open_symbol_editor` | Launch Symbol Editor (optional: open specific library/symbol) | `libraryName`, `symbolName`, optional `kicadVersion` | -| `kicad_open_footprint_editor` | `open_footprint_editor` | Launch Footprint Editor | `libraryName`, `footprintName`, optional `kicadVersion` | -| `kicad_open_3d_viewer` | `open_3d_viewer` | Show the board (or footprint) in 3D viewer | `editor` ("auto"/"pcb"/"fp" — "auto" prefers the board when both editors are open, no need to close sub-editors), optional `kicadVersion` | -| `kicad_close_symbol_editor` | `close_symbol_editor` | Close Symbol Editor | — | -| `kicad_close_footprint_editor` | `close_footprint_editor` | Close Footprint Editor | — | -| `kicad_close_3d_viewer` | `close_3d_viewer` | Close 3D viewer | — | -| `kicad_close` | `close` | Close KiCad (all editors) | — | -| `kicad_window_info` | `window_info` | Enumerate open KiCad windows (HWND, title, bounds, editor type) | — | -| `kicad_install_library` | `install_library` | Register a `.kicad_sym` + `.pretty/` pair in sym-lib-table + fp-lib-table | `libraryPath`, `libraryType`, `libraryName`, optional `kicadVersion` | -| `kicad_install_symbol` | `install_symbol` | Add a single symbol to an existing library | `fileName`, `fileContent`, optional `kicadVersion` | -| `kicad_install_footprint` | `install_footprint` | Add a single footprint to an existing library | `library`, `footprint_path`, optional `kicadVersion` | -| `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_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 | — | - -## Quick examples - -```bash -# Open a schematic -adom-desktop kicad_open_schematic '{"path":"C:/designs/foo.kicad_sch"}' - -# List open KiCad windows -adom-desktop kicad_window_info '{}' - -# Screenshot every KiCad window -adom-desktop kicad_screenshot_all '{}' - -# Run DRC -adom-desktop kicad_run_drc '{"board_path":"C:/designs/foo.kicad_pcb"}' - -# Install a library pair -adom-desktop kicad_install_library '{ - "name":"my-lib", - "sym_path":"C:/libs/my-lib.kicad_sym", - "fp_path":"C:/libs/my-lib.pretty" -}' -``` - -## Error shape - -Every failing response includes a `_hint` field with a human-readable next step — surface it to the user verbatim when a KiCad command fails. - -## Bridge architecture (v1.8.31+) — you don't need to know any port - -Earlier versions of the KiCad bridge listened on hardcoded port `8772`. **As of v1.8.31, bridges use OS-assigned ephemeral ports** — every spawn gets a new free port. You never need to know it. - -- The CLI (`adom-desktop kicad_*`) automatically routes through adom-desktop's direct API (default port 47200 as of v1.8.33, with auto-fallback to 47201-47209 if 47200 is taken). adom-desktop then forwards to the bridge's actual runtime port. -- The bridge port is internal plumbing and can change every spawn. `bridge_list` reports it as `spawn.runtimePort` for debugging only — do NOT hardcode that anywhere. -- Why: this lets adom-desktop coexist with Hydrogen Desktop (which used to claim 8772 itself) and any other tool that grabs ports in the 8000 range. Side-by-side bridges from different apps no longer collide. -- The relay path (Docker → wss proxy → Windows GUI) is unchanged — same `adom-desktop kicad_*` verbs, same JSON shape. - -If you ever find yourself wanting to talk directly to the KiCad bridge process: don't. Go through the verb dispatch. If the verb you need doesn't exist, request a feature in `adom-inc/adom-desktop`. - -## Related - -- `adom-desktop-discovery` (gallia) — ensures the CLI + relay are installed. -- `adom-desktop-fusion` — sibling bridge for Fusion 360. -- `adom-desktop-direct-api` — the direct API contract (port 47200 + 47201-47209 fallback, discovery file at `~/.adom/direct-api-port`). -- Repo: `adom-inc/adom-desktop/plugins/kicad/` +---+name: adom-desktop-kicad+public: true+description: "Launch and drive KiCad on the user's laptop (KiCad 7/8/9/10 supported, multi-version side-by-side) from this container via the adom-desktop CLI. Open schematics/boards/symbols/footprints, run DRC, install sym-lib-table / fp-lib-table libraries, show the 3D viewer, capture KiCad window screenshots, send keyboard shortcuts and click coordinates. Trigger words: kicad, open schematic, open board, open pcb, open symbol, open footprint, KiCad symbol editor, KiCad footprint editor, KiCad 3D viewer, run DRC, install KiCad library, sym-lib-table, fp-lib-table, KiCad screenshot, kicad_send_key, kicad_click, kicad_window_info, kicad_screenshot_all, close KiCad, kicad versions, kicad_list_versions, KiCad bridge."+---++# adom-desktop — KiCad bridge++All commands dispatch to the KiCad Python bridge running alongside the desktop app. Invoke from this container via Bash:++```bash+adom-desktop kicad_<action> '<json_args>'+```++The leading `kicad_` routes the call to the KiCad plugin; the `<action>` names below are the plugin-side command names.++## FIRST-TIME / COLD-START — read before you panic++A fresh machine can be missing KiCad, or the reverse-bridge plugin auto-installs on your first call. These states are **EXPECTED** — don't declare defeat. Branch on the stable `errorCode` (not the prose), and relay the returned `_hint` to the user verbatim.++| 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. |+| `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. |++**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).++## 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.)+- **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.++## Multi-version (KiCad 9 + KiCad 10 side-by-side)++Users can have multiple KiCad versions installed at the same time. The bridge auto-discovers everything under `C:/Program Files/KiCad/<version>/` and **defaults to the newest installed version** if you don't specify one.++```bash+# Discover what's installed (run this first if unsure)+adom-desktop kicad_list_versions '{}'+# → {"versions":[{"version":"10.0", "default":true, ...}, {"version":"9.0", ...}], "default":"10.0", "count":2}+```++Every `kicad_open_*` (and `install_*`, `run_drc`) command accepts an optional **`kicadVersion`** arg to pin a specific install:++```bash+# Open in KiCad 10 (newest — default if you omit kicadVersion)+adom-desktop kicad_open_board '{"filePath":"C:/designs/foo.kicad_pcb"}'++# Open in KiCad 9 explicitly (e.g. to check that an older project still works)+adom-desktop kicad_open_board '{"filePath":"C:/designs/foo.kicad_pcb","kicadVersion":"9.0"}'+```++If you pass a version that's not installed, the bridge returns a hint listing the installed versions. The success response includes `kicadVersionUsed` so you can confirm which install actually ran.++> **Note on libraries / config**: each KiCad version has its own `sym-lib-table` and `fp-lib-table` (in `%APPDATA%/kicad/<version>/`). `kicad_install_library` writes to the table of whichever version was selected — so if the user installs a library and then asks "open it in KiCad 9", you may need to install it under both versions.++## "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.++- 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.+- 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++| 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_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` |+| `kicad_open_board` | `open_board` | Open a `.kicad_pcb` in the PCB Editor | `filePath`, optional `kicadVersion` |+| `kicad_open_symbol_editor` | `open_symbol_editor` | Launch Symbol Editor (optional: open specific library/symbol) | `libraryName`, `symbolName`, optional `kicadVersion` |+| `kicad_open_footprint_editor` | `open_footprint_editor` | Launch Footprint Editor | `libraryName`, `footprintName`, optional `kicadVersion` |+| `kicad_open_3d_viewer` | `open_3d_viewer` | Show the board (or footprint) in 3D viewer | `editor` ("auto"/"pcb"/"fp" — "auto" prefers the board when both editors are open, no need to close sub-editors), optional `kicadVersion` |+| `kicad_close_symbol_editor` | `close_symbol_editor` | Close Symbol Editor | — |+| `kicad_close_footprint_editor` | `close_footprint_editor` | Close Footprint Editor | — |+| `kicad_close_3d_viewer` | `close_3d_viewer` | Close 3D viewer | — |+| `kicad_close` | `close` | Close KiCad (all editors) | — |+| `kicad_window_info` | `window_info` | Enumerate open KiCad windows (HWND, title, bounds, editor type) | — |+| `kicad_install_library` | `install_library` | Register a `.kicad_sym` + `.pretty/` pair in sym-lib-table + fp-lib-table | `libraryPath`, `libraryType`, `libraryName`, optional `kicadVersion` |+| `kicad_install_symbol` | `install_symbol` | Add a single symbol to an existing library | `fileName`, `fileContent`, optional `kicadVersion` |+| `kicad_install_footprint` | `install_footprint` | Add a single footprint to an existing library | `library`, `footprint_path`, optional `kicadVersion` |+| `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_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 | — |++## Quick examples++```bash+# Open a schematic+adom-desktop kicad_open_schematic '{"path":"C:/designs/foo.kicad_sch"}'++# List open KiCad windows+adom-desktop kicad_window_info '{}'++# Screenshot every KiCad window+adom-desktop kicad_screenshot_all '{}'++# Run DRC+adom-desktop kicad_run_drc '{"board_path":"C:/designs/foo.kicad_pcb"}'++# Install a library pair+adom-desktop kicad_install_library '{+ "name":"my-lib",+ "sym_path":"C:/libs/my-lib.kicad_sym",+ "fp_path":"C:/libs/my-lib.pretty"+}'+```++## Error shape++Every failing response includes a `_hint` field with a human-readable next step — surface it to the user verbatim when a KiCad command fails.++## Bridge architecture (v1.8.31+) — you don't need to know any port++Earlier versions of the KiCad bridge listened on hardcoded port `8772`. **As of v1.8.31, bridges use OS-assigned ephemeral ports** — every spawn gets a new free port. You never need to know it.++- The CLI (`adom-desktop kicad_*`) automatically routes through adom-desktop's direct API (default port 47200 as of v1.8.33, with auto-fallback to 47201-47209 if 47200 is taken). adom-desktop then forwards to the bridge's actual runtime port.+- The bridge port is internal plumbing and can change every spawn. `bridge_list` reports it as `spawn.runtimePort` for debugging only — do NOT hardcode that anywhere.+- Why: this lets adom-desktop coexist with Hydrogen (which used to claim 8772 itself) and any other tool that grabs ports in the 8000 range. Side-by-side bridges from different apps no longer collide.+- The relay path (Docker → wss proxy → Windows GUI) is unchanged — same `adom-desktop kicad_*` verbs, same JSON shape.++If you ever find yourself wanting to talk directly to the KiCad bridge process: don't. Go through the verb dispatch. If the verb you need doesn't exist, request a feature in `adom-inc/adom-desktop`.++## Related++- `adom-desktop-discovery` (gallia) — ensures the CLI + relay are installed.+- `adom-desktop-fusion` — sibling bridge for Fusion 360.+- `adom-desktop-direct-api` — the direct API contract (port 47200 + 47201-47209 fallback, discovery file at `~/.adom/direct-api-port`).+- Repo: `adom-inc/adom-desktop/plugins/kicad/`
adom-bridge-kicad-manifest.json+7−7@@ -1,19 +1,19 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.69",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-kicad-bridge/0.9.69/adom-bridge-kicad-v0.9.69.zip",- "sha256": "9f8f6ae26a034c9bf4e97a30b218659bffa30a8cac9b92c2a0acd0129954b6d8",- "size": 365869,+ "version": "0.9.70",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.70/adom-bridge-kicad-v0.9.70.zip",+ "sha256": "91f626909c22c745703250b86630182d254c752c2e4369e2aa8ccbbcbe11a702",+ "size": 365778, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-07-25T14:13:50Z",- "hero": "https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-hero.png",+ "released_at": "2026-08-08T23:07:12Z",+ "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python", "PowerShell" ],- "updateManifestUrl": "https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/adom-bridge-kicad-manifest.json"+ "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/kicad-bridge/files/adom-bridge-kicad-manifest.json" }
adom-bridge-kicad-v0.9.70.zipadded⋯ 1 unchanged line ⋯
adom_library.py+26−5@@ -1,6 +1,6 @@ """Manage the single global Adom symbol library (Adom.kicad_sym). -All symbols installed by Adom Desktop live in one KiCad symbol+All symbols installed by Adom Bridge live in one KiCad symbol library file. This module handles: - Creating the library file if it doesn't exist@@ -17,7 +17,7 @@ from lib_table import LibTable, LibEntry # The one library name used everywhere ADOM_LIB_NAME = "Adom" ADOM_LIB_FILE = "Adom.kicad_sym"-ADOM_LIB_DESCRIPTION = "Adom Desktop - custom symbols"+ADOM_LIB_DESCRIPTION = "Adom Bridge - custom symbols" # Regex to extract top-level (symbol "NAME" ...) blocks. # Top-level symbols are indented with exactly one leading whitespace group@@ -38,6 +38,21 @@ def _get_paths(kicad_info: dict) -> tuple[Path, Path] | None: """Return (sym_lib_table_path, adom_kicad_sym_path) or None.""" config_dir = kicad_info.get("config_dir") user_dir = kicad_info.get("user_dir")+ # Bootstrap config on a never-launched KiCad (same self-heal install_footprint+ # does). %APPDATA%/kicad/<ver>/ + Documents/KiCad/<ver>/ don't exist until the+ # first GUI launch; without this, symbol installs fail "config/user directory+ # not found" on a fresh install even though KiCad is present. (ADOMBASELINE 2026-07.)+ if not config_dir or not user_dir:+ try:+ import kicad_detect+ c, u = kicad_detect.ensure_win_user_config(+ kicad_info.get("version") or kicad_info.get("activeVersion") or "",+ kicad_info.get("base_dir"),+ )+ if c and u:+ config_dir, user_dir = str(c), str(u)+ except Exception:+ pass if not config_dir or not user_dir: return None table_path = Path(config_dir) / "sym-lib-table"@@ -143,10 +158,16 @@ def _extract_top_level_symbols(text: str) -> list[tuple[str, str]]: Each block includes the symbol and all its sub-units. """ results = []- # Find all top-level symbol openings (indented with exactly 2 spaces)- for m in re.finditer(r'^ \(symbol "([^"]+)"', text, re.MULTILINE):+ # Find all top-level symbol openings. KiCad indents these one level under+ # (kicad_symbol_lib ...); the indent char varies — the classic pretty-printer+ # used 2 spaces, but modern KiCad (generator "kicad_symbol_editor", file+ # version 20231120+) uses a TAB. Match ANY leading whitespace and rely on the+ # `_N_N` name-suffix filter below to drop the nested sub-unit symbols. Keying+ # on exactly 2 spaces silently extracted ZERO symbols from every tab-indented+ # library → install_symbol failed "No symbols found". (ADOMBASELINE 2026-07.)+ for m in re.finditer(r'^[ \t]+\(symbol\s+"([^"]+)"', text, re.MULTILINE): name = m.group(1)- # Skip sub-unit symbols+ # Skip sub-unit symbols (named "Parent_0_1", "Parent_1_1", ...) if re.search(r"_\d+_\d+$", name): continue block = _extract_symbol_block(text, m.start())
bridge.json+5−5@@ -2,9 +2,9 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.69",+ "version": "0.9.70", "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-desktop",+ "homepage": "https://wiki.adom.inc/adom/adom-bridge", "author": "Adom Inc.", "license": "MIT", "spawn": {@@ -94,8 +94,8 @@ "kicad-10", "kicad-11-ready" ],- "docs": "https://wiki.adom.inc/adom/adom-desktop-kicad-bridge",- "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/adom-desktop-kicad-bridge/files/adom-bridge-kicad-manifest.json",+ "docs": "https://wiki.adom.inc/adom/kicad-bridge",+ "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/kicad-bridge/files/adom-bridge-kicad-manifest.json", "detect": { "hostApp": "KiCad", "appPathsExe": "kicad.exe",@@ -144,7 +144,7 @@ ], "configKey": "project_watch" },- "hero": "https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-hero.png",+ "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python", "PowerShell"
handlers/demo.py+4−4@@ -1,6 +1,6 @@ """kicad_demo — the bridge's first-class, self-contained showcase. -Adom Desktop demos this bridge during Hydrogen Desktop's install, so an AI with+Adom Bridge demos this bridge during Hydrogen's install, so an AI with no context must be able to run ONE verb and narrate a convincing tour of what the bridge can do. Six steps, in the order a hardware person thinks: @@ -19,7 +19,7 @@ Design constraints that shaped this file: * **Narration, not just status.** Every step returns `say` (a line the AI can speak), `pointOut` (what is actually on screen worth mentioning) and the window title to screenshot. The AI reads verb OUTPUT, not our docs.-* **KiCad may not exist yet.** During HD install that is the likely case, so+* **KiCad may not exist yet.** During Hydrogen install that is the likely case, so the not-installed path is a first-class, actionable answer — not an error. """ @@ -43,7 +43,7 @@ R_SYM, R_FP = "AdomDemo_R", "AdomDemo_R0805" STEPS = ["symbol", "footprint", "footprint3d", "schematic", "board", "board3d"] # Screenshot labels. `schematic` / `board_2d` / `board_3d` deliberately match the-# labels fusion_demo returns, so an AI driving BOTH bridges (HD's installer does)+# labels fusion_demo returns, so an AI driving BOTH bridges (Hydrogen's installer does) # sees one vocabulary instead of two. STEP_LABEL = {"symbol": "symbol", "footprint": "footprint", "footprint3d": "part_3d", "schematic": "schematic", "board": "board_2d", "board3d": "board_3d"}@@ -389,7 +389,7 @@ def _run_step(kicad_info: dict, step: str, prep: dict) -> dict: def handle_demo(kicad_info: dict, args: dict) -> dict: """Run the bridge's showcase tour (all six steps, or one at a time)."""- # KiCad absent is the LIKELY case during an HD install demo. Never dead-end the+ # KiCad absent is the LIKELY case during an Hydrogen install demo. Never dead-end the # user with "install it yourself" — OFFER, and install it for them on the spot. if not kicad_info.get("installed"): if not args.get("installKiCad"):
handlers/install_footprint.py+9−2@@ -22,7 +22,7 @@ from handlers.open_footprint_editor import handle_open_footprint_editor ADOM_FP_LIB_NAME = "Adom" ADOM_FP_DIR_NAME = "Adom.pretty"-ADOM_FP_LIB_DESCRIPTION = "Adom Desktop - custom footprints"+ADOM_FP_LIB_DESCRIPTION = "Adom Bridge - custom footprints" def handle_install_footprint(kicad_info: dict, args: dict) -> dict:@@ -86,7 +86,14 @@ def handle_install_footprint(kicad_info: dict, args: dict) -> dict: user_dir = kicad_info.get("user_dir") config_dir = kicad_info.get("config_dir") if not user_dir or not config_dir:- return {"success": False, "error": "KiCad user/config directory not found"}+ import kicad_detect+ c, u = kicad_detect.ensure_win_user_config(+ kicad_info.get("version"), kicad_info.get("base_dir")+ )+ if c and u:+ config_dir, user_dir = str(c), str(u)+ if not user_dir or not config_dir:+ return {"success": False, "error": "KiCad user/config directory not found (bootstrap failed)"} fp_dir = Path(user_dir) / "footprints" fp_dir.mkdir(parents=True, exist_ok=True)
handlers/install_library.py+40−7@@ -29,7 +29,7 @@ def _install_footprint(kicad_info: dict, args: dict) -> dict: """Install a footprint library (.kicad_mod → .pretty directory).""" lib_path = args.get("libraryPath", "") lib_name = args.get("libraryName", "")- description = args.get("description", "Installed by Adom Desktop")+ description = args.get("description", "Installed by Adom Bridge") model_3d_path = args.get("model3dPath", "") source = Path(lib_path)@@ -43,7 +43,21 @@ def _install_footprint(kicad_info: dict, args: dict) -> dict: config_dir = kicad_info.get("config_dir") user_dir = kicad_info.get("user_dir") if not config_dir or not user_dir:- return {"success": False, "error": "KiCad config/user directory not found"}+ # Fresh install / KiCad never launched — bootstrap the per-user config+ # (seeds lib tables from the install template) the way first-run does.+ import kicad_detect+ c, u = kicad_detect.ensure_win_user_config(+ kicad_info.get("version"), kicad_info.get("base_dir")+ )+ if c and u:+ config_dir, user_dir = str(c), str(u)+ if not config_dir or not user_dir:+ return {+ "success": False,+ "error": "KiCad config/user directory not found and could not be bootstrapped",+ "_hint": "Is KiCad installed? Run kicad_list_versions. If it's freshly installed and "+ "never launched, this bootstrap should have created %APPDATA%/kicad/<ver>/.",+ } # Footprint libraries are .pretty directories containing .kicad_mod files footprints_dir = Path(user_dir) / "footprints"@@ -184,7 +198,7 @@ def handle_install_library(kicad_info: dict, args: dict) -> dict: # --- Symbol library install (original logic) --- lib_name = args.get("libraryName", "")- description = args.get("description", "Installed by Adom Desktop")+ description = args.get("description", "Installed by Adom Bridge") source = Path(lib_path) if not source.exists():@@ -205,8 +219,22 @@ def handle_install_library(kicad_info: dict, args: dict) -> dict: } config_dir = kicad_info.get("config_dir")+ user_dir = kicad_info.get("user_dir")+ if not config_dir or not user_dir:+ # Fresh install / KiCad never launched — bootstrap the per-user config.+ import kicad_detect+ c, u = kicad_detect.ensure_win_user_config(+ kicad_info.get("version"), kicad_info.get("base_dir")+ )+ if c and u:+ config_dir = config_dir or str(c)+ user_dir = user_dir or str(u) if not config_dir:- return {"success": False, "error": "KiCad config directory not found"}+ return {+ "success": False,+ "error": "KiCad config directory not found and could not be bootstrapped",+ "_hint": "Run kicad_list_versions to confirm KiCad is installed.",+ } user_dir = kicad_info.get("user_dir") if not user_dir:@@ -241,10 +269,15 @@ def handle_install_library(kicad_info: dict, args: dict) -> dict: ) if table.has_library(lib_name):+ # Idempotent: an already-registered library is SUCCESS, not an error — the+ # desired end state (library present + registered) already holds, and the+ # .kicad_sym/.pretty file was just (re)copied above. Re-running install+ # should be a no-op-ish success so callers/retries don't treat it as failure. return {- "success": False,- "error": f"Library '{lib_name}' already exists in {type_config['table_file']}",- "_hint": "Choose a different libraryName, or call list_symbol_libraries / list_footprint_libraries to see existing entries.",+ "success": True,+ "alreadyRegistered": True,+ "output": f"Library '{lib_name}' already registered in {type_config['table_file']} (file refreshed at {dest}).",+ "_hint": "Library was already present — nothing to change. This is a successful no-op.", } table.add_library(entry)
handlers/software_opengl.py+2−2@@ -23,7 +23,7 @@ from pathlib import Path from handlers.upgrade import _ssl_context # certifi-backed TLS (portable Python has no CA store) -_MESA_URL = "https://wiki.adom.inc/download/adom/adom-desktop-kicad-bridge/0.9.60/mesa-llvmpipe-x64.zip"+_MESA_URL = "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.60/mesa-llvmpipe-x64.zip" _MESA_DLLS = ("opengl32.dll", "libgallium_wgl.dll", "dxil.dll") _KICAD_EXES = ( "kicad.exe", "pcbnew.exe", "eeschema.exe", "gerbview.exe",@@ -62,7 +62,7 @@ def handle_enable_software_opengl(kicad_info: dict, args: dict) -> dict: } print(f"[KiCad Bridge] enable_software_opengl: downloading Mesa llvmpipe...", flush=True) try:- req = urllib.request.Request(_MESA_URL, headers={"User-Agent": "adom-desktop-kicad-bridge"})+ req = urllib.request.Request(_MESA_URL, headers={"User-Agent": "kicad-bridge"}) with urllib.request.urlopen(req, timeout=180, context=_ssl_context()) as r: data = r.read() zf = zipfile.ZipFile(io.BytesIO(data))
handlers/upgrade.py+267−25@@ -13,7 +13,9 @@ bridge fetches THAT and installs it silently (`/S`). """ import json+import os import re+import ssl import sys import tempfile import urllib.error@@ -23,13 +25,48 @@ from pathlib import Path 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+# 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+# regardless of the interpreter. Verification stays ON (no MITM window).+_BUNDLED_CACERT = os.path.join(+ os.path.dirname(os.path.abspath(__file__)), "..", "certs", "cacert.pem"+)+++def _ssl_context() -> ssl.SSLContext:+ """A verifying SSL context that works even on AD's certless portable Python."""+ ctx = ssl.create_default_context()+ # Primary: the bundled certifi roots (works on any interpreter).+ try:+ if os.path.exists(_BUNDLED_CACERT):+ ctx.load_verify_locations(cafile=_BUNDLED_CACERT)+ except Exception:+ pass+ # Secondary (belt-and-suspenders): the Windows system ROOT/CA store, if usable.+ if sys.platform == "win32":+ try:+ pem = "".join(+ ssl.DER_cert_to_PEM_cert(cert_der)+ for store in ("ROOT", "CA")+ for cert_der, enc, _trust in ssl.enum_certificates(store)+ if enc == "x509_asn"+ )+ if pem:+ ctx.load_verify_locations(cadata=pem)+ except Exception:+ pass+ return ctx+ # Baked fallback if the GitLab tag query fails (network blip). Bump on new stable. _FALLBACK_LATEST = "10.0.4" _TAGS_URL = ( "https://gitlab.com/api/v4/projects/kicad%2Fcode%2Fkicad/repository/tags?per_page=30" ) _DL_URL = "https://downloads.kicad.org/kicad/windows/explore/stable/download/kicad-{v}-x86_64.exe"-_UA = "adom-desktop-kicad-bridge"+_UA = "kicad-bridge" def _is_stable(tag: str) -> bool:@@ -45,7 +82,7 @@ def latest_stable_version() -> tuple[str, str]: """Return (version, source) where source is 'gitlab' or 'fallback'.""" try: req = urllib.request.Request(_TAGS_URL, headers={"User-Agent": _UA})- with urllib.request.urlopen(req, timeout=10) as r:+ with urllib.request.urlopen(req, timeout=10, context=_ssl_context()) as r: tags = [t["name"] for t in json.loads(r.read().decode())] stables = sorted( (t for t in tags if _is_stable(t)),@@ -104,6 +141,85 @@ def handle_check_for_updates(kicad_info: dict, args: dict) -> dict: } +def _elevation_facts() -> dict:+ """Hard, verifiable elevation/UAC facts for THIS process — no guessing.++ Settles "is the bridge actually elevated, and why didn't a UAC prompt appear":+ - tokenElevationType: 1=Default (no split token — UAC disabled OR built-in+ Administrator), 2=Full (elevated via UAC), 3=Limited (normal user token).+ - tokenElevated: the token's TokenElevation flag (the ground truth).+ - uacEnableLUA: HKLM ...\\Policies\\System EnableLUA — 0 means UAC is OFF+ machine-wide, so admin accounts get a full token with NO prompt ever.+ """+ facts: dict = {"platform": sys.platform}+ if sys.platform != "win32":+ return facts+ import ctypes+ import ctypes.wintypes as wt+ import getpass+ facts["user"] = getpass.getuser()+ try:+ facts["isUserAnAdmin"] = bool(ctypes.windll.shell32.IsUserAnAdmin())+ except Exception as e:+ facts["isUserAnAdmin"] = f"error: {e}"+ try:+ advapi32 = ctypes.WinDLL("advapi32", use_last_error=True)+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)+ TOKEN_QUERY = 0x0008+ token = wt.HANDLE()+ if not advapi32.OpenProcessToken(+ kernel32.GetCurrentProcess(), TOKEN_QUERY, ctypes.byref(token)+ ):+ raise OSError(f"OpenProcessToken failed: {ctypes.get_last_error()}")+ for name, cls in (("tokenElevationType", 18), ("tokenElevated", 20)):+ val = wt.DWORD()+ ret_len = wt.DWORD()+ ok = advapi32.GetTokenInformation(+ token, cls, ctypes.byref(val), ctypes.sizeof(val), ctypes.byref(ret_len)+ )+ facts[name] = int(val.value) if ok else f"error: {ctypes.get_last_error()}"+ kernel32.CloseHandle(token)+ facts["tokenElevationTypeMeaning"] = {+ 1: "Default — no split token (UAC disabled machine-wide, or built-in Administrator): full admin, NO UAC prompt ever",+ 2: "Full — elevated via an approved UAC prompt (or elevated parent)",+ 3: "Limited — standard filtered token, NOT admin",+ }.get(facts.get("tokenElevationType"), "unknown")+ except Exception as e:+ facts["tokenError"] = str(e)+ try:+ import winreg+ with winreg.OpenKey(+ winreg.HKEY_LOCAL_MACHINE,+ r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System",+ ) as k:+ facts["uacEnableLUA"] = winreg.QueryValueEx(k, "EnableLUA")[0]+ # 0 = elevate silently WITHOUT a prompt (common on VMs) — explains+ # "I never clicked UAC" even when a process ends up elevated.+ try:+ facts["uacConsentPromptBehaviorAdmin"] = winreg.QueryValueEx(+ k, "ConsentPromptBehaviorAdmin"+ )[0]+ except OSError:+ facts["uacConsentPromptBehaviorAdmin"] = "not set (default 5: prompt)"+ except Exception as e:+ facts["uacEnableLUA"] = f"error: {e}"+ return facts+++def _remote_installer_size(url: str) -> int | None:+ """The server's Content-Length for the installer (follows redirects). Lets us+ verify a downloaded/cached file isn't silently TRUNCATED — a dropped+ connection reads as EOF, logs 'download complete', passes the MZ+50MB sanity+ checks, and then the NSIS integrity check fails instantly at install time."""+ try:+ req = urllib.request.Request(url, method="HEAD", headers={"User-Agent": _UA})+ with urllib.request.urlopen(req, timeout=30, context=_ssl_context()) as r:+ cl = r.headers.get("Content-Length")+ return int(cl) if cl else None+ except Exception:+ return None++ def handle_upgrade(kicad_info: dict, args: dict) -> dict: """Download the official KiCad installer and run it silently (/S).""" if sys.platform != "win32":@@ -113,6 +229,33 @@ def handle_upgrade(kicad_info: dict, args: dict) -> dict: "errorCode": "unsupported_platform", } + # diagnoseOnly: return the elevation/UAC ground truth + cached-installer state+ # WITHOUT downloading or installing anything. Used to settle "is this process+ # actually elevated / why did no UAC prompt appear / why rc=666660".+ if args.get("diagnoseOnly"):+ facts = _elevation_facts()+ t = args.get("version") or latest_stable_version()[0]+ cached = Path(tempfile.gettempdir()) / f"kicad-{t}-x86_64.exe"+ facts["cachedInstaller"] = str(cached) if cached.exists() else None+ facts["cachedInstallerBytes"] = cached.stat().st_size if cached.exists() else 0+ expected = _remote_installer_size(download_url(t))+ facts["serverInstallerBytes"] = expected+ facts["cachedInstallerTruncated"] = (+ bool(cached.exists() and expected and cached.stat().st_size != expected)+ )+ return {+ "success": True,+ "diagnoseOnly": True,+ **facts,+ "_hint": (+ "Read tokenElevationTypeMeaning + uacEnableLUA together: EnableLUA=0 means UAC is "+ "disabled machine-wide, so an admin account always runs with a full token and NO UAC "+ "prompt ever appears — IsUserAnAdmin=True without anyone clicking anything. "+ "tokenElevated=1 with an rc=666660 install failure means elevation is NOT the cause "+ "(look at SmartScreen/AV or the installer's silent flag instead)."+ ),+ }+ target = args.get("version") or latest_stable_version()[0] current = kicad_info.get("version") if kicad_info else None if (@@ -131,24 +274,86 @@ def handle_upgrade(kicad_info: dict, args: dict) -> dict: url = download_url(target) dest = Path(tempfile.gettempdir()) / f"kicad-{target}-x86_64.exe" - # Download — urllib follows the downloads.kicad.org -> s3.cern.ch redirect.- try:- req = urllib.request.Request(url, headers={"User-Agent": _UA})- total = 0- with urllib.request.urlopen(req, timeout=120) as r, open(dest, "wb") as f:- while True:- chunk = r.read(1 << 20)- if not chunk:- break- f.write(chunk)- total += len(chunk)- except (urllib.error.URLError, OSError) as e:- return {- "success": False,- "error": f"download failed: {e}",- "errorCode": "download_failed",- "url": url,- }+ # 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+ # re-download makes a post-elevation retry near-instant.+ def _valid_installer(p: Path) -> bool:+ try:+ if not p.exists() or p.stat().st_size < 50 * 1024 * 1024:+ return False+ with open(p, "rb") as fh:+ return fh.read(2) == b"MZ"+ except OSError:+ return False++ # The server's true size — the anti-truncation ground truth. A dropped+ # connection reads as EOF ("download complete"), passes MZ+50MB checks, then+ # NSIS's integrity check fails instantly at install time (the rc=666660+ # failure mode). Compare every cached/downloaded file against this.+ expected_bytes = _remote_installer_size(url)+ print(f"[KiCad Bridge] kicad_upgrade: server reports installer size "+ f"{expected_bytes if expected_bytes else 'unknown'} bytes", flush=True)++ total = dest.stat().st_size if dest.exists() else 0+ cache_ok = (+ _valid_installer(dest)+ and not args.get("reDownload")+ and (expected_bytes is None or total == expected_bytes)+ )+ if _valid_installer(dest) and expected_bytes and total != expected_bytes:+ print(f"[KiCad Bridge] kicad_upgrade: cached installer is TRUNCATED "+ f"({total} of {expected_bytes} bytes) — discarding and re-downloading", flush=True)+ if cache_ok:+ print(f"[KiCad Bridge] kicad_upgrade: reusing cached installer {dest} "+ f"({total // (1024*1024)} MB, size verified) — pass reDownload:true to force a fresh fetch", flush=True)+ else:+ # Download — urllib follows the downloads.kicad.org -> s3.cern.ch redirect.+ # Progress is printed so `bridge_log_read` reveals where a slow/stuck+ # install is. timeout is the per-read blocking timeout (300s for slow VMs).+ print(f"[KiCad Bridge] kicad_upgrade: downloading {url}", flush=True)+ try:+ req = urllib.request.Request(url, headers={"User-Agent": _UA})+ total = 0+ next_log = 25 * 1024 * 1024+ with urllib.request.urlopen(req, timeout=300, context=_ssl_context()) as r, open(dest, "wb") as f:+ if expected_bytes is None:+ cl = r.headers.get("Content-Length")+ expected_bytes = int(cl) if cl else None+ while True:+ chunk = r.read(1 << 20)+ if not chunk:+ break+ f.write(chunk)+ total += len(chunk)+ if total >= next_log:+ print(f"[KiCad Bridge] kicad_upgrade: downloaded {total // (1024*1024)} MB...", flush=True)+ next_log += 25 * 1024 * 1024+ print(f"[KiCad Bridge] kicad_upgrade: download finished ({total} of "+ f"{expected_bytes if expected_bytes else 'unknown'} bytes) -> {dest}", flush=True)+ except (urllib.error.URLError, OSError) as e:+ print(f"[KiCad Bridge] kicad_upgrade: DOWNLOAD FAILED: {e}", flush=True)+ return {+ "success": False,+ "error": f"download failed: {e}",+ "errorCode": "download_failed",+ "url": url,+ }+ if expected_bytes and total != expected_bytes:+ print(f"[KiCad Bridge] kicad_upgrade: TRUNCATED download ({total} of {expected_bytes}) — deleting", flush=True)+ try:+ dest.unlink()+ except OSError:+ pass+ return {+ "success": False,+ "error": f"download truncated: got {total} of {expected_bytes} bytes "+ f"(connection dropped mid-transfer). The file was deleted.",+ "errorCode": "download_truncated",+ "url": url,+ "_hint": "Re-run kicad_upgrade — it will re-download. If this repeats, the mirror "+ "or the VM's link is flaky; try again later.",+ } # Sanity: a real KiCad installer is a >50 MB Windows PE (MZ header). if total < 50 * 1024 * 1024:@@ -170,14 +375,44 @@ 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)+ # 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+ # "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.+ def _is_admin():+ if sys.platform != "win32":+ return None+ try:+ import ctypes+ return bool(ctypes.windll.shell32.IsUserAnAdmin())+ except Exception:+ return None+ elevated = _is_admin()+ # KiCad 10's installer is built on NsisMultiUser: in SILENT mode it REQUIRES an+ # explicit install scope — bare "/S" is "invalid command-line parameters" and+ # exits instantly with rc=666660 (NsisMultiUser's documented error level; its+ # sibling 666661 = elevation restricted). THE root cause of the fast-fail.+ # /allusers /S → per-machine (Program Files; needs elevation)+ # /currentuser /S → per-user (%LOCALAPPDATA%\Programs\KiCad; NO admin, NO UAC)+ # scope arg: "machine" | "user" | default auto (machine when elevated, else the+ # zero-UAC per-user install — kicad_detect scans both locations).+ scope = args.get("scope") or ("machine" if elevated else "user")+ scope_flag = "/allusers" if scope == "machine" else "/currentuser"+ print(f"[KiCad Bridge] kicad_upgrade: elevated={elevated}; launching installer "+ f"{scope_flag} /S (scope={scope})...", flush=True) try:- result = proc.run([str(dest), "/S"], timeout=900)+ result = proc.run([str(dest), scope_flag, "/S"], timeout=900) rc = result.returncode+ print(f"[KiCad Bridge] kicad_upgrade: installer exited rc={rc}", flush=True) except Exception as e:+ print(f"[KiCad Bridge] kicad_upgrade: INSTALLER LAUNCH/RUN FAILED: {e}", flush=True) return { "success": False, "error": f"installer launch failed: {e}", "errorCode": "install_failed",+ "elevated": elevated, "path": str(dest), "_hint": "If a UAC prompt appeared, approve it; or run the downloaded installer manually.", }@@ -195,15 +430,22 @@ def handle_upgrade(kicad_info: dict, args: dict) -> dict: "targetVersion": target, "previousVersion": current, "installerExit": rc,+ "elevated": elevated,+ "installScope": scope, "installedVersions": installed, "downloadUrl": url, "_hint": (- f"KiCad {target} installed."+ f"KiCad {target} installed ({scope} scope)." if now_has else (- f"Installer ran (exit {rc}) but {target} wasn't detected — it may have needed a "- f"UAC approval, or the bridge needs a restart to re-scan. Installed: "- f"{', '.join(installed) or 'none'}."+ f"Installer ran (exit {rc}) but {target} wasn't detected. elevated={elevated}, scope={scope}. "+ + ("rc=666660 is NsisMultiUser 'invalid command-line parameters' and 666661 is 'elevation "+ "restricted' — retry with the other scope: "+ '{"scope":"user"} needs NO admin/UAC (installs to %LOCALAPPDATA%\\Programs\\KiCad); '+ '{"scope":"machine"} needs elevation. '+ if rc in (666660, 666661) else+ "Check SmartScreen/AV blocking the .exe or a pending reboot. ")+ + f"Installed: {', '.join(installed) or 'none'}." ) ), }
proc.py+1−1@@ -1,6 +1,6 @@ """Safe subprocess wrapper — defensive against host spawn environments. -When a supervisor (Adom Desktop) spawns this bridge, the bridge's own child+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
server.py+4−4@@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Adom KiCad Bridge Server — localhost HTTP server for KiCad integration. -Receives commands from the Adom Desktop (Tauri app) and controls+Receives commands from the Adom Bridge (Tauri app) and controls KiCad via kicad-cli, direct executable launch, and library table manipulation. Usage:@@ -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 HD install).+# v0.9.63+: first-class showcase tour (AD 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;@@ -490,7 +490,7 @@ COMMAND_HANDLERS = { "export_bom_csv": handle_export_bom_csv, # v0.9.10+: KiCad version check + upgrade via the official downloads.kicad.org # installer (read-only check; silent /S install). Exposed as kicad_check_for_updates- # / kicad_upgrade through Adom Desktop's `kicad_` verb prefix.+ # / kicad_upgrade through Adom Bridge's `kicad_` verb prefix. "check_for_updates": handle_check_for_updates, "upgrade": handle_upgrade, }@@ -873,7 +873,7 @@ class KiCadBridgeHandler(BaseHTTPRequestHandler): def main(): global kicad_info, all_kicad_versions - # Line-buffer stdout/stderr. When a supervisor (Adom Desktop) redirects our+ # Line-buffer stdout/stderr. When a supervisor (Adom Bridge) redirects our # 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
skills/kicad-bridge-dev/SKILL.md+4−4@@ -1,12 +1,12 @@ --- name: kicad-bridge-dev user-invocable: false-description: DEVELOPER-only skill (user-invocable:false — installed for maintainers, never offered to everyday users). Building/debugging the Adom Desktop KiCad bridge: architecture, the 4 control surfaces, how to add a verb, and every fresh-machine gotcha this bridge has hit. Trigger words — kicad bridge dev, edit kicad bridge, add kicad verb, kicad bridge architecture, debug kicad bridge, kicad control surfaces, kicad fresh machine.+description: DEVELOPER-only skill (user-invocable:false — installed for maintainers, never offered to everyday users). Building/debugging the Adom Bridge KiCad bridge: architecture, the 4 control surfaces, how to add a verb, and every fresh-machine gotcha this bridge has hit. Trigger words — kicad bridge dev, edit kicad bridge, add kicad verb, kicad bridge architecture, debug kicad bridge, kicad control surfaces, kicad fresh machine. --- # kicad-bridge-dev -Developer notes for the `adom-desktop-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 AD-core-vs-author boundary. ## What it is @@ -142,7 +142,7 @@ subprocess (like kicad-cli) — NOT AD's gated `run_script`. ### ⚖️ Every file you add to this repo may ship to every AD user -The release zip is **auto-bundled into Adom Desktop**, so repo weight is user-facing weight. Before+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: | Adding… | Goes in | Ships in the zip? |@@ -187,7 +187,7 @@ threads (withdrawn in the 1.9.183 notice); the per-call mechanism is ## 🎬 The demo verb (`kicad_demo`) — match the ecosystem contract -HD's installer demos BOTH bridges, so `kicad_demo` deliberately returns the SAME+Hydrogen's installer demos BOTH bridges, so `kicad_demo` deliberately returns the SAME shape as `fusion_demo`. Do not invent fields: an AI driving both should not have to special-case them.
skills/kicad-bridge-publish/SKILL.md+11−11@@ -1,12 +1,12 @@ --- 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 Desktop 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 AD, 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 -How to cut and publish a KiCad-bridge release. **Use the `adom-wiki` CLI for ALL wiki interactions — never raw curl.** Two independent artifacts: the RUNTIME (zip + manifest, on the page `/files`) and the SKILLS PKG (`adom/adom-desktop-kicad-bridge`, container-side docs).+How to cut and publish a KiCad-bridge release. **Use the `adom-wiki` CLI for ALL wiki interactions — never raw curl.** Two independent artifacts: the RUNTIME (zip + manifest, on the page `/files`) and the SKILLS PKG (`adom/kicad-bridge`, container-side docs). ## 1. Version lockstep (bump ALL three together) @@ -18,7 +18,7 @@ They must match. `bridge.json` also carries `updateManifestUrl` (durable auto-up ## 2. Build the zip — KEEP IT TINY (it is auto-bundled into AD) -The runtime zip ships inside Adom Desktop for **every** user, so it carries ONLY what the+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 had grown to 488.9 KB carrying 219 KB of hero *fonts*, five `skills/` trees, demo scripts and READMEs — none referenced by runtime code. Slimming it to 335.7 KB was a 31% cut with zero@@ -49,10 +49,10 @@ Manifest fields: `manifest_version:1, name:"kicad", version, url` (the `/blob/ap ## 3. Publish the runtime (CLI) ```bash-adom-wiki repo push adom/adom-desktop-kicad-bridge \+adom-wiki repo push adom/kicad-bridge \ --files adom-bridge-kicad-v<ver>.zip adom-bridge-kicad-manifest.json bridge.json \ -m "v<ver>: <what changed>"-adom-wiki release create adom/adom-desktop-kicad-bridge <ver> \+adom-wiki release create adom/kicad-bridge <ver> \ --title "KiCad bridge v<ver>" --changelog "..." \ --binary-path adom-bridge-kicad-v<ver>.zip ```@@ -74,14 +74,14 @@ description."* Cut to 145 chars.) The full explanation belongs in the README bod Canonical description — copy this shape: > Skills for your container so your AI knows how to drive the KiCad bridge.-> The bridge runtime itself is the release zip — Adom Desktop loads that.+> The bridge runtime itself is the release zip — Adom Bridge loads that. The longer version (the "Two artifacts on this page — which one do you want?" comparison table: what it is / who loads it / contains / do you install it manually, closing with **"The package is not the bridge."**) lives near the TOP of `README.md`, where there's room for it. **No images in the pkg.** The README references page art by ABSOLUTE-`https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/<name>.png` URLs, so the PNGs render on+`https://wiki.adom.inc/blob/app/kicad-bridge/<name>.png` URLs, so the PNGs render on the page without riding in the install tarball. (Audited 2026-07-15: `files[]` listed `hero.png` + `arch.png` + `verbs.png` + `outcomes.png` = **496 KB** downloaded by every `pkg install`, referenced by nothing. Removing them took the tarball to 29.5 KB.) Push page art@@ -104,7 +104,7 @@ The runtime SKILL.md (`adom-desktop-kicad`, ships in the zip) and the pkg's root adom-desktop --target <name> refresh_bridges '{"name":"kicad"}' # pulls new cache adom-desktop --target <name> bridge_kill '{"name":"kicad"}' # force fresh spawn (old instance keeps old code) adom-desktop --target <name> bridge_check_updates '{}' # current == latest?-adom-wiki repo show adom/adom-desktop-kicad-bridge adom-bridge-kicad-manifest.json # served version matches?+adom-wiki repo show adom/kicad-bridge adom-bridge-kicad-manifest.json # served version matches? ``` Verify the served bridge.json (`repo show ... bridge.json`) has the fields you expect and the zip's sha matches. A fresh verb call after a kill returns `bridge_starting` — retry.@@ -112,7 +112,7 @@ Verify the served bridge.json (`repo show ... bridge.json`) has the fields you e ## 6. Refreshing the README screenshots (window tour) The README embeds screenshots hosted on the page `/files` (referenced as-`https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/<name>.png`). Regenerate them on a **real+`https://wiki.adom.inc/blob/app/kicad-bridge/<name>.png`). Regenerate them on a **real VM** (e.g. ADOMBASELINE) whenever the UI/flow changes — never hand-mock them: 1. On the VM: `kicad_enable_software_opengl` (if GPU-less) → open each window with sample data from@@ -122,7 +122,7 @@ VM** (e.g. ADOMBASELINE) whenever the UI/flow changes — never hand-mock them: 3. Save into `docs/img/` with the canonical names the README uses: `schematic-editor.png`, `symbol-editor.png`, `footprint-editor.png`, `pcb-editor-2d.png`, `3d-viewer.png`, `dialog-firstrun-wizard.png`, `control-surfaces.svg`.-4. `adom-wiki repo push adom/adom-desktop-kicad-bridge --files docs/img/*.png ...` then+4. `adom-wiki repo push adom/kicad-bridge --files docs/img/*.png ...` then `curl -so /dev/null -w '%{http_code}'` each blob URL to confirm 200. 5. `shotlog paste <png> -c kicad-bridge -d "<caption>"` to show John the live captures. @@ -147,7 +147,7 @@ times because the window was open, correct and maximized — but behind everythi ## 8. Upstream: what the SDK still owes every bridge -Two issues are open on `adom/adom-desktop-bridges` (filed 2026-07-19) — if you hit these problems,+Two issues are open on `adom/adom-bridge-sdk` (filed 2026-07-19) — if you hit these problems, they're known, and the fix belongs in the SDK, not just here: 1. **SDK should require every bridge to explain pkg-vs-zip** (one line, on the page + in
skills/kicad-tour/SKILL.md+1−1@@ -38,7 +38,7 @@ The packs are mounted by the bridge at: - `C:/Github/adom-desktop/plugins/kicad/<PACK>/` - (When AD is installer-deployed, look under- `%LOCALAPPDATA%/Adom Desktop/bridges-cache/kicad/<PACK>/` — adjust paths.)+ `%LOCALAPPDATA%/Adom Bridge/bridges-cache/kicad/<PACK>/` — adjust paths.) ### Step 1 — install the custom symbol + footprint libraries