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.63: kicad_demo — first-class showcase tour (symbol->footprint->3D->schematic->2D board->3D board), generates its own assets, offers to install KiCad if missing
6 files changed
+430−9
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.62+0.9.63
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.62",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-kicad-bridge/0.9.62/adom-bridge-kicad-v0.9.62.zip",- "sha256": "0d12a8ce22f54893548c6030fbe7e25cd4d3bcfa94c5f20c12f093234540a282",- "size": 343743,+ "version": "0.9.63",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-kicad-bridge/0.9.63/adom-bridge-kicad-v0.9.63.zip",+ "sha256": "c2c1249e3c49c9668ecc59397fdb88f87944406a7c595e554b42cf727b8d9264",+ "size": 351315, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-07-19T22:58:48Z",+ "released_at": "2026-07-20T11:09:33Z", "hero": "https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-hero.png", "languages": [ "Python",
adom-bridge-kicad-v0.9.63.zipadded⋯ 1 unchanged line ⋯
bridge.json+4−3@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.62",+ "version": "0.9.63", "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", "author": "Adom Inc.",@@ -61,7 +61,8 @@ "kicad_export_step", "kicad_export_bom_csv", "kicad_check_for_updates",- "kicad_upgrade"+ "kicad_upgrade",+ "kicad_demo" ], "statusVerb": "kicad_status", "timeouts": {@@ -147,4 +148,4 @@ "PowerShell" ], "releasedAt": "2026-06-26T16:00:00Z"-}+}
handlers/demo.pyadded+414@@ -0,0 +1,414 @@+"""kicad_demo — the bridge's first-class, self-contained showcase.++Adom Desktop demos this bridge during Hydrogen Desktop'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:++ symbol -> footprint -> 3D of that part -> schematic -> 2D board -> 3D board++Design constraints that shaped this file:++* **Ships zero assets.** The runtime zip is auto-bundled into AD, 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.+* **Real 3D for free.** The generated footprints point at KiCad's OWN bundled+ `share/kicad/3dmodels/*.3dshapes` models, resolved from the detected install+ at runtime. Nothing to download, and the 3D viewer shows a real part. If the+ model is missing we still place the pads and say so rather than failing.+* **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+ the not-installed path is a first-class, actionable answer — not an error.+"""++import base64+import os+from pathlib import Path++from .install_symbol import handle_install_symbol+from .install_footprint import handle_install_footprint+from .open_symbol_editor import handle_open_symbol_editor+from .open_footprint_editor import handle_open_footprint_editor, handle_open_3d_viewer+from .open_files import handle_open_board, handle_open_schematic+from .upgrade import handle_upgrade+from kicad_detect import detect_kicad++DEMO_LIB = "Adom"+IC_SYM, IC_FP = "AdomDemo_IC", "AdomDemo_SOIC8"+R_SYM, R_FP = "AdomDemo_R", "AdomDemo_R0805"++STEPS = ["symbol", "footprint", "footprint3d", "schematic", "board", "board3d"]+++# ---------------------------------------------------------------- 3D models --+def _model_path(kicad_info: dict, lib: str, name: str):+ """Absolute path to one of KiCad's OWN bundled 3D models, or None.++ Layout is stable across KiCad 6-10: <base>/share/kicad/3dmodels/<lib>/<name>.+ We probe .wrl then .step so the demo degrades gracefully instead of writing+ a dangling (model ...) that makes the 3D viewer look broken.+ """+ base = kicad_info.get("base_dir")+ if not base:+ return None+ root = Path(base) / "share" / "kicad" / "3dmodels" / lib+ for ext in (".wrl", ".step"):+ p = root / (name + ext)+ if p.exists():+ return str(p).replace("\\", "/")+ return None+++# ------------------------------------------------------------ asset writers --+def _sym_block(name: str) -> str:+ """The bare `(symbol "<name>" ...)` block.++ Returned on its own (not wrapped in a kicad_symbol_lib) because it is needed+ in TWO places: the .kicad_sym we install, and the schematic's `lib_symbols`.+ Slicing it back out of the wrapper is how you silently ship a schematic with+ no symbols in it — so it is generated once, here.+ """+ if name == IC_SYM:+ pins = []+ names = ["VDD", "IN1", "IN2", "GND", "OUT2", "OUT1", "EN", "NC"]+ for i in range(4): # left 1-4, top-down+ pins.append(f' (pin bidirectional line (at -12.7 {5.08 - i * 2.54:.2f} 0) (length 2.54) '+ f'(name "{names[i]}") (number "{i + 1}"))')+ for i in range(4): # right 5-8, bottom-up, like a real SOIC+ pins.append(f' (pin bidirectional line (at 12.7 {-3.81 + i * 2.54:.2f} 180) (length 2.54) '+ f'(name "{names[4 + i]}") (number "{i + 5}"))')+ return f''' (symbol "{IC_SYM}"+ (pin_names (offset 1.016)) (exclude_from_sim no) (in_bom yes) (on_board yes)+ (property "Reference" "U" (at -10.16 9.53 0) (effects (font (size 1.27 1.27)) (justify left)))+ (property "Value" "{IC_SYM}" (at -10.16 -9.53 0) (effects (font (size 1.27 1.27)) (justify left)))+ (property "Footprint" "{DEMO_LIB}:{IC_FP}" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))+ (property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))+ (symbol "{IC_SYM}_0_1"+ (rectangle (start -10.16 7.62) (end 10.16 -7.62)+ (stroke (width 0.254) (type default)) (fill (type background)))+ )+ (symbol "{IC_SYM}_1_1"+{chr(10).join(pins)}+ )+ )+'''+ return f''' (symbol "{R_SYM}"+ (pin_numbers (hide yes)) (pin_names (offset 0) (hide yes))+ (exclude_from_sim no) (in_bom yes) (on_board yes)+ (property "Reference" "R" (at 2.54 1.27 0) (effects (font (size 1.27 1.27)) (justify left)))+ (property "Value" "10k" (at 2.54 -1.27 0) (effects (font (size 1.27 1.27)) (justify left)))+ (property "Footprint" "{DEMO_LIB}:{R_FP}" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))+ (symbol "{R_SYM}_0_1"+ (rectangle (start -1.016 2.54) (end 1.016 -2.54)+ (stroke (width 0.254) (type default)) (fill (type none)))+ )+ (symbol "{R_SYM}_1_1"+ (pin passive line (at 0 3.81 270) (length 1.27) (name "~") (number "1"))+ (pin passive line (at 0 -3.81 90) (length 1.27) (name "~") (number "2"))+ )+ )+'''+++def _sym_lib(name: str) -> str:+ """Installable .kicad_sym = the wrapper + the block."""+ return ('(kicad_symbol_lib (version 20231120) (generator "adom-demo")\n'+ + _sym_block(name) + ')\n')+++def _fp_soic8(model: str | None, at: str = "") -> str:+ pads = []+ ys = [1.905, 0.635, -0.635, -1.905]+ for i, y in enumerate(ys): # 1-4 left+ pads.append(f' (pad "{i + 1}" smd roundrect (at -2.475 {y}) (size 1.95 0.6) '+ f'(layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25))')+ for i, y in enumerate(reversed(ys)): # 5-8 right+ pads.append(f' (pad "{i + 5}" smd roundrect (at 2.475 {y}) (size 1.95 0.6) '+ f'(layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25))')+ model_block = (f' (model "{model}" (offset (xyz 0 0 0)) (scale (xyz 1 1 1)) (rotate (xyz 0 0 0)))\n'+ if model else "")+ return f'''(footprint "{IC_FP}"+ (version 20240108) (generator "adom-demo") (layer "F.Cu"){at}+ (attr smd)+ (property "Reference" "U**" (at 0 -3.7 0) (layer "F.SilkS") (effects (font (size 0.8 0.8) (thickness 0.12))))+ (property "Value" "{IC_FP}" (at 0 3.7 0) (layer "F.Fab") (effects (font (size 0.8 0.8) (thickness 0.12))))+ (fp_line (start -1.95 -2.45) (end 1.95 -2.45) (stroke (width 0.12) (type solid)) (layer "F.SilkS"))+ (fp_line (start -1.95 2.45) (end 1.95 2.45) (stroke (width 0.12) (type solid)) (layer "F.SilkS"))+ (fp_circle (center -2.6 2.6) (end -2.45 2.6) (stroke (width 0.2) (type solid)) (fill none) (layer "F.SilkS"))+ (fp_rect (start -3.7 -2.7) (end 3.7 2.7) (stroke (width 0.05) (type solid)) (fill none) (layer "F.CrtYd"))+{chr(10).join(pads)}+{model_block})+'''+++def _fp_r0805(model: str | None, at: str = "") -> str:+ model_block = (f' (model "{model}" (offset (xyz 0 0 0)) (scale (xyz 1 1 1)) (rotate (xyz 0 0 0)))\n'+ if model else "")+ return f'''(footprint "{R_FP}"+ (version 20240108) (generator "adom-demo") (layer "F.Cu"){at}+ (attr smd)+ (property "Reference" "R**" (at 0 -1.6 0) (layer "F.SilkS") (effects (font (size 0.6 0.6) (thickness 0.1))))+ (property "Value" "10k" (at 0 1.6 0) (layer "F.Fab") (effects (font (size 0.6 0.6) (thickness 0.1))))+ (fp_rect (start -1.7 -0.9) (end 1.7 0.9) (stroke (width 0.05) (type solid)) (fill none) (layer "F.CrtYd"))+ (pad "1" smd roundrect (at -0.9125 0) (size 1.025 1.4) (layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25))+ (pad "2" smd roundrect (at 0.9125 0) (size 1.025 1.4) (layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25))+{model_block})+'''+++def _uuid(n: int) -> str:+ return f"00000000-0000-4000-8000-{n:012d}"+++def _schematic() -> str:+ """Modern (v7+) schematic. The `(instances ...)` block is MANDATORY — without+ it KiCad 8/9/10 opens the sheet but renders NO symbols (silently)."""+ sheet = _uuid(1)+ parts = [(IC_SYM, "U1", 101.6, 76.2), (R_SYM, "R1", 152.4, 63.5), (R_SYM, "R2", 152.4, 88.9)]+ # lib_symbols entries are keyed by the FULL lib id ("Adom:Name") — the instances+ # below reference exactly that, or KiCad renders an empty sheet.+ libs = (_sym_block(IC_SYM).replace(f'(symbol "{IC_SYM}"', f'(symbol "{DEMO_LIB}:{IC_SYM}"', 1)+ + _sym_block(R_SYM).replace(f'(symbol "{R_SYM}"', f'(symbol "{DEMO_LIB}:{R_SYM}"', 1))+ inst = ""+ for i, (sym, ref, x, y) in enumerate(parts):+ u = _uuid(200 + i)+ inst += f''' (symbol (lib_id "{DEMO_LIB}:{sym}") (at {x} {y} 0) (unit 1)+ (exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) (uuid "{u}")+ (property "Reference" "{ref}" (at {x + 12} {y - 2.54} 0) (effects (font (size 1.27 1.27)) (justify left)))+ (property "Value" "{sym}" (at {x + 12} {y + 2.54} 0) (effects (font (size 1.27 1.27)) (justify left)))+ (instances (project "adom-demo" (path "/{sheet}" (reference "{ref}") (unit 1))))+ )+'''+ return f'''(kicad_sch (version 20231120) (generator "eeschema") (generator_version "8.0")+ (uuid "{sheet}") (paper "A4")+ (title_block (title "Adom KiCad Bridge — Demo") (company "Adom"))+ (lib_symbols+{libs} )+{inst} (sheet_instances (path "/" (page "1")))+)+'''+++def _board(kicad_info: dict) -> str:+ soic = _model_path(kicad_info, "Package_SO.3dshapes", "SOIC-8_3.9x4.9mm_P1.27mm")+ r08 = _model_path(kicad_info, "Resistor_SMD.3dshapes", "R_0805_2012Metric")+ fps = (_fp_soic8(soic, "\n (at 120 80)").replace("\n", "\n ")+ + "\n " + _fp_r0805(r08, "\n (at 145 70)").replace("\n", "\n ")+ + "\n " + _fp_r0805(r08, "\n (at 145 90)").replace("\n", "\n "))+ edge = ""+ pts = [(100, 60), (170, 60), (170, 100), (100, 100), (100, 60)]+ for i in range(len(pts) - 1):+ (x1, y1), (x2, y2) = pts[i], pts[i + 1]+ edge += (f' (gr_line (start {x1} {y1}) (end {x2} {y2}) '+ f'(stroke (width 0.15) (type solid)) (layer "Edge.Cuts") (uuid "{_uuid(300 + i)}"))\n')+ return f'''(kicad_pcb (version 20240108) (generator "pcbnew") (generator_version "8.0")+ (general (thickness 1.6)) (paper "A4")+ (layers+ (0 "F.Cu" signal) (31 "B.Cu" signal)+ (36 "B.SilkS" user "B.Silkscreen") (37 "F.SilkS" user "F.Silkscreen")+ (38 "B.Mask" user) (39 "F.Mask" user) (40 "B.Paste" user) (41 "F.Paste" user)+ (44 "Edge.Cuts" user) (45 "Margin" user) (49 "F.Fab" user) (50 "B.Fab" user)+ (46 "B.CrtYd" user "B.Courtyard") (47 "F.CrtYd" user "F.Courtyard")+ )+ (setup (pad_to_mask_clearance 0))+{edge} {fps}+)+'''+++# ------------------------------------------------------------------ the verb --+def _demo_dir() -> Path:+ d = Path.home() / "Documents" / "adom-kicad-demo"+ d.mkdir(parents=True, exist_ok=True)+ return d+++def _b64(s: str) -> str:+ return base64.b64encode(s.encode("utf-8")).decode()+++def _prepare(kicad_info: dict) -> dict:+ """Install the two demo parts into the Adom library + write the project."""+ out = {"installed": [], "warnings": []}+ for fname, sym, content in ((f"{IC_SYM}.kicad_sym", IC_SYM, _sym_lib(IC_SYM)),+ (f"{R_SYM}.kicad_sym", R_SYM, _sym_lib(R_SYM))):+ r = handle_install_symbol(kicad_info, {"fileName": fname, "symbolName": sym,+ "fileContent": _b64(content), "quietInstall": True})+ (out["installed"] if r.get("success") else out["warnings"]).append(+ sym if r.get("success") else f"{sym}: {r.get('error')}")++ soic = _model_path(kicad_info, "Package_SO.3dshapes", "SOIC-8_3.9x4.9mm_P1.27mm")+ r08 = _model_path(kicad_info, "Resistor_SMD.3dshapes", "R_0805_2012Metric")+ out["models3d"] = {"soic8": soic, "r0805": r08}+ if not (soic and r08):+ out["warnings"].append(+ "KiCad's bundled 3D models were not found — pads/board still render, "+ "component bodies will be missing in the 3D views.")+ for fname, fp, content in ((f"{IC_FP}.kicad_mod", IC_FP, _fp_soic8(soic)),+ (f"{R_FP}.kicad_mod", R_FP, _fp_r0805(r08))):+ r = handle_install_footprint(kicad_info, {"fileName": fname, "footprintName": fp,+ "fileContent": _b64(content), "quietInstall": True})+ (out["installed"] if r.get("success") else out["warnings"]).append(+ fp if r.get("success") else f"{fp}: {r.get('error')}")++ d = _demo_dir()+ (d / "adom-demo.kicad_sch").write_text(_schematic(), encoding="utf-8")+ (d / "adom-demo.kicad_pcb").write_text(_board(kicad_info), encoding="utf-8")+ (d / "adom-demo.kicad_pro").write_text('{"board":{},"meta":{"filename":"adom-demo.kicad_pro","version":1},"schematic":{},"sheets":[]}', encoding="utf-8")+ out["projectDir"] = str(d)+ out["schematic"] = str(d / "adom-demo.kicad_sch")+ out["board"] = str(d / "adom-demo.kicad_pcb")+ return out+++def _run_step(kicad_info: dict, step: str, prep: dict) -> dict:+ """Run one tour step. Returns narration-ready info, never raises."""+ has3d = bool((prep.get("models3d") or {}).get("soic8"))+ if step == "symbol":+ r = handle_open_symbol_editor(kicad_info, {"libraryName": DEMO_LIB, "symbolName": IC_SYM})+ return {"ok": bool(r.get("success")), "window": "Symbol Editor",+ "title": "1/6 · Schematic symbol",+ "say": (f"This is the {IC_SYM} symbol, in the user's own KiCad symbol editor. "+ "The bridge installed it into their Adom library a second ago — no file "+ "copying, no dialogs."),+ "pointOut": ["8 named pins (VDD, IN1/IN2, GND, OUT1/OUT2, EN, NC)",+ "the body rectangle and pin numbering",+ f"it is registered in the '{DEMO_LIB}' library, not a scratch file"],+ "raw": r}+ if step == "footprint":+ r = handle_open_footprint_editor(kicad_info, {"libraryName": DEMO_LIB, "footprintName": IC_FP})+ return {"ok": bool(r.get("success")), "window": "Footprint Editor",+ "title": "2/6 · The footprint for that symbol",+ "say": (f"Same part, now its land pattern: {IC_FP}. This is what actually gets "+ "soldered — the symbol is the idea, the footprint is the copper."),+ "pointOut": ["8 SMD pads on 1.27 mm pitch", "silkscreen outline + the pin-1 dot",+ "the courtyard rectangle (keep-out for the assembler)"],+ "raw": r}+ if step == "footprint3d":+ r = handle_open_3d_viewer(kicad_info, {"editor": "footprint"})+ return {"ok": bool(r.get("success")), "window": "3D Viewer",+ "title": "3/6 · That part in 3D",+ "say": ("And here is the same footprint in three dimensions" ++ (", with the real SOIC-8 body from KiCad's own 3D model library."+ if has3d else " — pads only; KiCad's bundled 3D models weren't found.")),+ "pointOut": (["the SOIC-8 package body sitting on its pads", "gold pad plating"]+ if has3d else ["the bare pads (no bundled 3D model on this machine)"]),+ "raw": r}+ if step == "schematic":+ r = handle_open_schematic(kicad_info, {"filePath": prep["schematic"]})+ return {"ok": bool(r.get("success")), "window": "Schematic Editor",+ "title": "4/6 · A schematic using it",+ "say": ("Now a real schematic sheet: the demo IC as U1 with two 10k resistors, "+ "R1 and R2. The bridge generated and opened this project itself."),+ "pointOut": ["U1 plus R1/R2, each properly annotated",+ "the title block: 'Adom KiCad Bridge — Demo'"],+ "raw": r}+ if step == "board":+ r = handle_open_board(kicad_info, {"filePath": prep["board"]})+ return {"ok": bool(r.get("success")), "window": "PCB Editor",+ "title": "5/6 · The 2D board layout",+ "say": ("The PCB side: the same three parts placed on a 70 by 40 millimetre "+ "board outline, copper, silkscreen and courtyards all real."),+ "pointOut": ["the SOIC-8 land pattern and two 0805 resistors",+ "the Edge.Cuts board outline",+ "'Pads 12' in the status bar — proof the footprints loaded"],+ "raw": r}+ if step == "board3d":+ r = handle_open_3d_viewer(kicad_info, {"editor": "pcb"})+ return {"ok": bool(r.get("success")), "window": "3D Viewer",+ "title": "6/6 · The whole board in 3D",+ "say": ("And the finished board in 3D — this is the payoff: from a symbol to a "+ "rendered assembly, every step driven from the cloud on the user's own "+ "machine."),+ "pointOut": (["all three component bodies on the green board"] if has3d+ else ["the board and copper (component bodies need KiCad's 3D models)"])+ + ["you can orbit it with the mouse — it is their real KiCad"],+ "raw": r}+ return {"ok": False, "error": f"unknown step '{step}'", "validSteps": STEPS}+++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+ # 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"):+ return {+ "success": False,+ "error": "KiCad is not installed",+ "data": {+ "offer": "I can install KiCad for you right now — it takes a few minutes.",+ "acceptWith": "kicad_demo {\"installKiCad\": true}",+ "installVerb": "kicad_upgrade",+ "steps": STEPS,+ },+ "_hint": (+ "ASK THE USER: \"You don't have KiCad yet — want me to install it for you? \" "+ "Never hand them a download link or ask them to click through an installer. "+ "On yes, call kicad_demo {\"installKiCad\": true} — it installs the official "+ "build silently (per-user, no UAC) and then runs the whole tour in the same "+ "call. Watching the AI install their EDA tool is itself the best demo beat, "+ "so narrate it. Takes a few minutes on a fresh machine (large download)."+ ),+ }+ # They said yes — install it, then re-detect and carry on into the tour.+ up = handle_upgrade(kicad_info, {})+ fresh = detect_kicad() or {}+ if not fresh.get("installed"):+ return {+ "success": False,+ "error": "KiCad install did not complete",+ "data": {"upgrade": up},+ "_hint": ("The silent install ran but KiCad still isn't detected. Tell the user "+ "plainly, then check kicad_upgrade {\"diagnoseOnly\":true} — the usual "+ "causes are SmartScreen/AV blocking the downloaded installer or a "+ "pending reboot. Don't pretend the demo succeeded."),+ }+ kicad_info = fresh+ installed_now = True+ else:+ installed_now = False++ step = args.get("step")+ if step and step not in STEPS:+ return {"success": False, "error": f"unknown step '{step}'", "data": {"validSteps": STEPS}}++ prep = _prepare(kicad_info)+ steps = [step] if step else STEPS+ results, failed = [], []+ for s in steps:+ r = _run_step(kicad_info, s, prep)+ r["step"] = s+ results.append(r)+ if not r.get("ok"):+ failed.append(s)++ done = [r["step"] for r in results if r.get("ok")]+ return {+ "success": not failed,+ "output": (f"Demo tour: {len(done)}/{len(steps)} steps opened "+ f"({', '.join(done) or 'none'})" + (f"; failed: {', '.join(failed)}" if failed else "")),+ "data": {+ "steps": results,+ "projectDir": prep.get("projectDir"),+ "installedParts": prep.get("installed"),+ "models3d": prep.get("models3d"),+ "warnings": prep.get("warnings"),+ "installedKiCadJustNow": installed_now,+ "screenshotVerb": "kicad_screenshot_all",+ "cleanup": "kicad_close {\"force\":true}",+ },+ "_hint": (+ ("You just installed KiCad for them — say so, it lands well. " if installed_now else "")+ + "This is a NARRATED tour — each entry in data.steps carries `title`, `say` (speak it, "+ "don't read JSON), `pointOut` (what's actually on screen) and `window` (match that "+ "title in kicad_screenshot_all to show it). Best flow: call kicad_demo with one "+ "`step` at a time (" + " -> ".join(STEPS) + "), say the line, screenshot that window, "+ "then move on — a single bulk call opens all six at once and the user can't follow. "+ "Windows open in the BACKGROUND by design; screenshot them rather than stealing focus. "+ "Everything is generated by the bridge (no shipped sample files) and installs into the "+ "user's real KiCad. Tear down with kicad_close {\"force\":true}."+ ),+ }
server.py+6@@ -59,6 +59,8 @@ 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).+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; # upgrade silent-installs the official signed NSIS build.@@ -323,6 +325,9 @@ _VERB_CATALOG = { "export_step": {"summary": "Export a STEP 3D model from a board.", "long": False, "hint": "Absolute board path + output; exports a STEP assembly via kicad-cli.", "related": ["kicad_open_3d_viewer", "kicad_export_gerber"], "pitfalls": ["only components with 3D models attached appear in the STEP"]},+ "demo": {"summary": "Run the bridge's showcase tour: symbol -> footprint -> 3D part -> schematic -> 2D board -> 3D board.", "long": True,+ "hint": "The demo verb. Generates its own parts + project (ships no sample files), installs them into the user's real KiCad, and opens each view. Every step returns `say` (speak it), `pointOut` (what's on screen) and `window` (match in kicad_screenshot_all). Pass {\"step\":\"symbol\"} to pace it one beat at a time; no args runs all six.",+ "related": ["kicad_screenshot_all", "kicad_close", "kicad_upgrade"], "pitfalls": ["if KiCad isn't installed it returns an actionable error — call kicad_upgrade, don't send the user to a download page", "windows open in the BACKGROUND; screenshot them instead of stealing focus", "a bulk run opens all six windows at once — step-by-step narrates better"]}, "export_molecule": {"summary": "One-shot molecule export pack: STEP + silk renders + sch/pro discovery for the Adom molecule pipeline.", "long": True, "hint": "Absolute board path; exports STEP (+ silkscreen top/bottom PNGs when kicad-cli pcb render exists), finds the sibling .kicad_sch/.kicad_pro, and returns data.nextSteps — the exact container commands (step2glb → adom-molecule create) to finish.", "related": ["kicad_export_step", "kicad_export_svg"], "pitfalls": ["adom-molecule requires glb+pcb+sch+pro — check data.missing before running the container half", "silk renders need KiCad 9+ (kicad-cli pcb render); older KiCads skip them gracefully"]},@@ -480,6 +485,7 @@ COMMAND_HANDLERS = { "export_svg": handle_export_svg, "export_step": handle_export_step, "export_molecule": handle_export_molecule,+ "demo": handle_demo, "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