← Commit history

0.2.63 source (not yet released, needs signing): altium-wiki-part (an Adom wiki component page onto an Altium sheet in one command: clone, convert, import with the page's identity, register the libraries, place)

Colby Knox ·317c990a76 ·1d ago ·parent 12b7dbf
11 files changed +240−10
CHANGELOG.md+8
@@ -1,5 +1,13 @@ # Changelog +## 0.2.63 (2026-10-22)+- **`altium-wiki-part`** (container command, installed by install.sh) puts an Adom wiki component page into Altium in one command.+  - `place <owner/slug> --sheet ... [--x --y --designator]`: clones the page, converts its KiCad symbol and footprint, stages them and its STEP on the PC, imports the part with `altium_import_part`, and places it with `altium_sch_place_part`. `import <owner/slug>` does the library part only.+  - The part's identity comes from the page's page.json (MPN, manufacturer, package, datasheet, Mouser / DigiKey / LCSC numbers, a link back to the page), so no parts-search quota is spent.+  - Default libraries are `Documents\Adom\WikiParts\AdomWiki.SchLib/.PcbLib`. They are registered in Installed Libraries, so a project's change order finds the footprint.+  - Measured on AD 26.11.1: TPS54202DDCR from page to placed part in 12.5 s with every parameter and all six pins. On the battery's lane project, which has no such footprint, `altium_update_pcb` put it on the board with the wiki footprint (6 pads).+- The altium-wiki-import skill leads with the one-command path; the main skill's intent map points to it.+ ## 0.2.62 (2026-10-22) - `altium_close {discardChanges:true}` answers Altium Designer Develop's per-document "Unsaved Changes" box (Save / Don't Save / Cancel) with Don't Save. It used to press Save None, which that box does not have, and the close timed out with Altium left on the prompt. The discard button is now chosen from the buttons the box has: Save None, OK for "Confirm Not Saving", Don't Save (either apostrophe), or No. Found while checking signed 0.2.61's workspace placement on Develop. Measured after the fix: an unsaved sheet on Develop closed in 6.8 s, and the dialog log shows "Don't Save" pressed. 
SKILL.md+1−1
@@ -35,7 +35,7 @@ Say **altium** to reach this bridge. Every verb returns `success`, a stable `err | Place, route, pour, silkscreen a board | `altium_move_parts`, `altium_route_net`, `altium_autoroute` (Situs), `altium_place_vias`, `altium_apply_pours`, `altium_apply_silkscreen`, `altium_set_design_rules`, `altium_layer_stack`, `altium_find_space` (free spots near a part; overlap check) (**altium-boards**) | | Manufacturing files, STEP, PDF | `altium_export_manufacturing` (checks the outline and copper layers in its Gerbers: `fabChecks`), `altium_board_outline`, `altium_export_step`, `altium_export_pdf`, `altium_run_outjob` (**altium-outputs**) | | Make or fix library parts | `altium_import_part`, `altium_build_symbol_modes`, `altium_build_footprint`, `altium_add_body`, `altium_set_params` (**altium-libraries**, **altium-part-import**) |-| Parts from the Adom wiki | `adom-wiki library export --format altium`, `altium_install_library_bundle` (**altium-wiki-import**) |+| Parts from the Adom wiki | one part onto a sheet: `altium-wiki-part place <owner/slug> --sheet ...`; a whole Cloud Library: `adom-wiki library export --format altium` + `altium_install_library_bundle` (**altium-wiki-import**) | | Parts from the Altium 365 workspace (shared cloud library) | `altium_workspace_search {query}`, `altium_workspace_place {sheet, query, pick, x, y}` (through Altium's own Components panel; no API access needed) (**altium-schematic-edit**) | | Altium 365 API | `altium_signin_status`, `altium_a365_*` (needs API access) (**altium-a365**) | | Anything else Altium can do | `altium_list_commands {query}` then `altium_run_command {command, params, filePath}` (1,100 processes); `altium_eval {text, filePath}` runs DelphiScript (a `Var` block is allowed; a failing script answers `script_error` with a shot of Altium's message) |
bridge-rs/src/main.rs+1−1
@@ -37,7 +37,7 @@ use tiny_http::{Header, Method, Response, Server}; /// Attached to every failed reply (see the /command handler). const REPORT_ISSUES: &str = "If this looks like an altium-bridge BUG (not a refusal you expected), report it: check for a duplicate with `adom-wiki issue list adom/altium-bridge`, then `adom-wiki issue create adom/altium-bridge --title \"<verb>: <one-line symptom>\" --category bug-report --body \"<what you ran, what you expected, what happened, the VERBATIM response JSON (and errorShot if present), bridgeVersion, the Altium version from altium_status, the OS>\"`. A relay, routing, toast or lifecycle problem belongs on adom/adom-bridge instead. Reports from real machines are how this bridge gets correct: three different Altium builds behaved three different ways in one weekend."; -pub const BRIDGE_VERSION: &str = "0.2.62";+pub const BRIDGE_VERSION: &str = "0.2.63";  fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {     let data = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
install.sh+3−2
@@ -49,7 +49,7 @@ chmod +x "$BINDIR/altium-dashboard" # and AI Flow's Altium library step run in the container. They were only in the source repo, so an AI # with just this package could not follow its own skills (found 2026-10-15). Install them as commands. mkdir -p "$APPDIR/tools"-for t in kicad2altium symbol_modes kicad_v10_to_v9 bom_tool; do+for t in kicad2altium symbol_modes kicad_v10_to_v9 bom_tool wiki_part; do   cp -f "$HERE/tools/$t.py" "$APPDIR/tools/$t.py" done mk() {  # <command> <tool>@@ -63,4 +63,5 @@ mk altium-kicad2altium kicad2altium mk altium-symbol-modes symbol_modes mk altium-kicad-downgrade kicad_v10_to_v9 mk altium-bom bom_tool-echo "OK: altium-bridge user skills installed + altium-dashboard + altium-kicad2altium, altium-symbol-modes, altium-kicad-downgrade, altium-bom"+mk altium-wiki-part wiki_part+echo "OK: altium-bridge user skills installed + altium-dashboard + altium-kicad2altium, altium-symbol-modes, altium-kicad-downgrade, altium-bom, altium-wiki-part"
package.json+2−1
@@ -1,6 +1,6 @@ {   "slug": "altium-bridge",-  "version": "0.2.62",+  "version": "0.2.63",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the Altium Designer bridge (altium_* verbs). The bridge runtime itself is the release zip; Adom Bridge loads that.",   "tags": [@@ -47,6 +47,7 @@     "skills/altium-uia/SKILL.md",     "skills/altium-wiki-import/SKILL.md",     "tools/bom_tool.py",+    "tools/wiki_part.py",     "tools/kicad2altium.py",     "tools/kicad_v10_to_v9.py",     "tools/symbol_modes.py",
page.json+1−1
@@ -4,7 +4,7 @@   "type": "app",   "title": "Altium - the Altium Bridge",   "brief": "Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.",-  "version": "0.2.62",+  "version": "0.2.63",   "hero": {     "type": "image",     "path": "screenshots/hero.png"
skills/altium-wiki-import/SKILL.md+18−1
@@ -3,13 +3,30 @@ name: altium-wiki-import description: >-   Import a component from the Adom wiki into Altium (one part page or a whole Cloud Library) with   symbol, footprint, 3D body and Mouser/DigiKey/LCSC identity, in the background. Use when asked to-  add this wiki part to Altium, install my cloud library into Altium, or prove an import worked.+  add or place this wiki part in Altium, install my cloud library into Altium, or prove an import worked. ---  Parent skill: **altium-bridge**  # altium-wiki-import +## One command: a wiki part on your sheet (0.2.63)++```bash+altium-wiki-part place adom/tps54202ddcr --sheet 'C:\proj\Main.SchDoc' --x 4000 --y 1000 --designator U7+altium-wiki-part import adom/tps54202ddcr          # into the library only+```++It clones the component page, converts its KiCad symbol and footprint (altium-symbol-modes,+altium-kicad2altium), stages them and its STEP on the PC, and runs `altium_import_part` with the page's+identity from page.json: MPN, Manufacturer, Package, Datasheet, Mouser / DigiKey / LCSC numbers and a link+back to the page (no parts-search quota spent). The default libraries are+`Documents\Adom\WikiParts\AdomWiki.SchLib/.PcbLib` (`--schlib` / `--pcblib` choose others), and both are+registered in Altium's Installed Libraries (`--no-install` skips that), so any project's change order finds+the footprint. `place` then calls `altium_sch_place_part`. Measured: TPS54202DDCR from its page to a placed+part in 12.5 s; on a project with no such footprint of its own, `altium_update_pcb` put it on the board with+the wiki footprint (6 pads). Find the page with `adom-wiki search "<MPN>"`.+ ## What ships today  `adom-wiki library` (Cloud Libraries, i.e. component carts) exports **KiCad, Fusion and Altium**.
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.2.62+0.2.63
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.2.62",+  "version": "0.2.63",   "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.",   "homepage": "https://wiki.adom.inc/adom/altium-bridge",   "docs": "https://wiki.adom.inc/adom/altium-bridge",
tools/wiki_part.pyadded+203
@@ -0,0 +1,203 @@+#!/usr/bin/env python3+"""altium-wiki-part: an Adom wiki component page into Altium in one command.++  altium-wiki-part import <page> [--schlib C:\\...\\Lib.SchLib] [--pcblib C:\\...\\Lib.PcbLib] [--target BOX]+      Clones the page (adom/tps54202ddcr), converts its KiCad symbol and footprint with altium-symbol-modes and+      altium-kicad2altium, stages them and its STEP model on the Windows machine, and imports the part with+      altium_import_part: symbol, footprint, 3D body, and the page's identity (MPN, manufacturer, package,+      datasheet, Mouser / DigiKey / LCSC numbers). Default libraries: Documents\\Adom\\WikiParts\\AdomWiki.*+  altium-wiki-part place <page> --sheet C:\\...\\Main.SchDoc [--x MIL --y MIL] [--designator U7] [--schlib ..] [--pcblib ..]+      The same import, then the part placed on the sheet with altium_sch_place_part.++Every command prints OK:/ERROR: first and Hint: lines after, like the other Adom CLIs.+"""+import argparse+import json+import os+import re+import shutil+import subprocess+import sys+import tempfile+from pathlib import Path++HERE = Path(__file__).resolve().parent+++def ok(msg, hints=()):+    print("OK: " + msg)+    for h in hints:+        if h:+            print("Hint: " + h)+++def err(msg, hints=()):+    print("ERROR: " + msg)+    for h in hints:+        if h:+            print("Hint: " + h)+    sys.exit(1)+++def bridge(verb, args, target=None, timeout=900):+    cmd = ["adom-bridge", "--ai-thread", os.environ.get("ADOM_AI_THREAD", "altium-wiki-part")]+    if target:+        cmd += ["--target", target]+    cmd += [verb, json.dumps(args)]+    try:+        out = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, stdin=subprocess.DEVNULL).stdout+    except subprocess.TimeoutExpired:+        err("%s did not answer within %d s" % (verb, timeout))+    except FileNotFoundError:+        err("adom-bridge is not on PATH", ["This runs in an Adom workspace with Adom Bridge connected to the Windows machine running Altium."])+    try:+        d = json.loads(out)+    except ValueError:+        err("%s answered something that is not JSON: %s" % (verb, out[:300]))+    o = d.get("output", d)+    if isinstance(o, str) and o.startswith("{"):+        try:+            o = json.loads(o)+        except ValueError:+            pass+    return o if isinstance(o, dict) else d+++def tool(name, script):+    """The converter: the installed command, else the script beside this one."""+    exe = shutil.which(name)+    return [exe] if exe else [sys.executable, str(HERE / script)]+++def run(cmd):+    r = subprocess.run(cmd, capture_output=True, text=True, stdin=subprocess.DEVNULL)+    if r.returncode != 0:+        err("%s failed: %s" % (" ".join(cmd[:2]), (r.stderr or r.stdout)[-400:]))+    return r.stdout+++def windows_home(target):+    r = bridge("shell_execute", {"command": "cmd /c echo %USERPROFILE%", "reason": "find the user's Documents folder for the wiki parts library"}, target)+    home = (r.get("stdout") or "").strip()+    if not home or "%" in home:+        err("could not read %USERPROFILE% on the Windows machine", ["Pass --schlib and --pcblib explicitly."])+    return home.replace("/", "\\")+++def fetch(page, work):+    """Clone the page; answer (folder, page.json's component)."""+    r = subprocess.run(["adom-wiki", "repo", "clone", page], cwd=work, capture_output=True, text=True, stdin=subprocess.DEVNULL)+    dirs = [d for d in Path(work).iterdir() if d.is_dir()]+    if r.returncode != 0 or not dirs:+        err("could not clone %s: %s" % (page, (r.stderr or r.stdout)[-300:]), ["Use the page reference, owner/slug (adom-wiki search \"<MPN>\" finds it)."])+    folder = dirs[0]+    try:+        meta = json.loads((folder / "page.json").read_text())+    except (OSError, ValueError):+        err("%s has no readable page.json" % page)+    comp = meta.get("component") or {}+    if not comp.get("parts"):+        err("%s is not a component page (no component.parts in page.json)" % page,+            ["Component pages carry a KiCad symbol and footprint; a Cloud Library exports whole with adom-wiki library export --format altium."])+    return folder, meta, comp+++def do_import(a):+    work = tempfile.mkdtemp(prefix="altium-wiki-part-")+    folder, meta, comp = fetch(a.page, work)+    parts = comp["parts"]+    mpn = (comp.get("mpn") or meta.get("title") or folder.name).strip()+    name = re.sub(r"[^A-Za-z0-9_.+-]", "_", mpn.upper())+    sym, fp = folder / parts.get("symbol", ""), folder / parts.get("footprint", "")+    step = folder / (parts.get("model_3d_step") or parts.get("model_3d_step_plain") or "")+    if not sym.is_file() or not fp.is_file():+        err("%s lacks its KiCad symbol or footprint (%s, %s)" % (a.page, sym.name, fp.name))+    desc = (meta.get("brief") or comp.get("description") or mpn)[:200].replace("|", "/")+    fp_spec, sym_spec = Path(work) / ("%s.fp.spec" % name), Path(work) / ("%s.sym.spec" % name)+    run(tool("altium-kicad2altium", "kicad2altium.py") + [str(fp), str(fp_spec), name, desc])+    run(tool("altium-symbol-modes", "symbol_modes.py") + [str(sym), str(sym_spec), name, desc])+    # the libraries and the staging folder on the Windows machine+    home = None+    if not a.schlib or not a.pcblib:+        home = windows_home(a.target)+    base = (home + "\\Documents\\Adom\\WikiParts") if home else None+    schlib = a.schlib or base + "\\AdomWiki.SchLib"+    pcblib = a.pcblib or base + "\\AdomWiki.PcbLib"+    stage = (base or str(Path(schlib.replace("\\", "/")).parent).replace("/", "\\")) + "\\staging\\" + name+    files = [str(fp_spec), str(sym_spec)] + ([str(step)] if step.is_file() else [])+    s = bridge("send_files", {"filePaths": files, "dest": stage, "destIsFolder": True, "reason": "stage a wiki part's converted symbol, footprint and STEP for Altium"}, a.target)+    if s.get("success") is False:+        err("could not stage the files on the Windows machine: %s" % (s.get("error") or s), ["send_files needs Adom Bridge connected; check adom-bridge targets."])+    remote = lambda p: stage + "\\" + Path(p).name+    links = comp.get("distributor_links") or {}+    params = {k: v for k, v in {+        "MPN": mpn, "Manufacturer Part Number": mpn, "Manufacturer": comp.get("manufacturer"), "Package": comp.get("package"),+        "Datasheet": comp.get("datasheet"), "Mouser Part #": links.get("mouser"), "DigiKey Part #": links.get("digikey"),+        "LCSC Part #": links.get("lcsc"), "Adom Wiki": "https://wiki.adom.inc/" + a.page,+    }.items() if v}+    args = {"schLib": schlib, "pcbLib": pcblib, "footprintSpec": remote(fp_spec), "symbolSpec": remote(sym_spec),+            "params": params, "comment": mpn, "description": desc, "timeout": 290}+    if step.is_file():+        args["step"] = remote(step)+    r = bridge("altium_import_part", args, a.target, timeout=330)+    if r.get("success") is not True:+        err("altium_import_part failed at %s: %s" % (r.get("failedAt") or "?", r.get("error") or r.get("errorCode") or json.dumps(r)[:300]),+            [r.get("_hint"), "altium_check_dialogs shows what Altium is waiting on."])+    # registered in Installed Libraries, so every project's change order finds the footprint (idempotent)+    installed = []+    if not a.no_install:+        for lib in (schlib, pcblib):+            i = bridge("altium_install_library", {"libraryPath": lib, "timeout": 90}, a.target, timeout=150)+            installed.append({"library": lib, "ok": i.get("success")})+    shutil.rmtree(work, ignore_errors=True)+    return {"installed": installed, "mpn": mpn, "symbol": r.get("symbol") or name, "footprint": r.get("footprint") or name, "schLib": schlib, "pcbLib": pcblib,+            "params": params, "step": step.is_file(), "steps": r.get("steps")}+++def cmd_import(a):+    p = do_import(a)+    ok("%s imported: symbol %s and footprint %s%s in %s" % (p["mpn"], p["symbol"], p["footprint"], " with its 3D body" if p["step"] else "", p["schLib"]),+       ["Parameters written: " + ", ".join(sorted(p["params"])),+        "Installed Libraries: " + ", ".join("%s %s" % (Path(i["library"].replace("\\", "/")).name, "ok" if i["ok"] else "FAILED") for i in p["installed"]) if p["installed"] else None,+        "Place it: altium-wiki-part place %s --sheet <C:\\...\\Main.SchDoc>, or altium_sch_place_part {sheet, library:%s, symbol:%s}." % (a.page, json.dumps(p["schLib"]), p["symbol"])])+++def cmd_place(a):+    if not a.sheet.lower().endswith(".schdoc"):+        err("--sheet must be a .SchDoc path on the Windows machine")+    p = do_import(a)+    args = {"sheet": a.sheet, "library": p["schLib"], "symbol": p["symbol"]}+    for k in ("x", "y"):+        if getattr(a, k) is not None:+            args[k] = getattr(a, k)+    if a.designator:+        args["designator"] = a.designator+    r = bridge("altium_sch_place_part", args, a.target, timeout=200)+    if r.get("success") is not True:+        err("the part was imported into %s but not placed: %s" % (p["schLib"], r.get("error") or r.get("errorCode") or json.dumps(r)[:300]), [r.get("_hint")])+    ok("%s placed on %s as %s at %s mil" % (p["mpn"], a.sheet, r.get("designator"), r.get("atMil")),+       ["It carries %s; its footprint %s is in %s." % (", ".join(sorted(p["params"])), p["footprint"], p["pcbLib"]),+        "Number it with altium_annotate (a '?' designator), wire it with altium_sch_connect, and carry it to the board with altium_update_pcb: the libraries are in Altium's Installed Libraries%s, so the change order finds the footprint." % ("" if all(i["ok"] for i in p["installed"]) and p["installed"] else " (NOT registered: pass the footprint library to the project)")])+++def main():+    ap = argparse.ArgumentParser(prog="altium-wiki-part", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)+    sub = ap.add_subparsers(dest="cmd", required=True)+    for n in ("import", "place"):+        s = sub.add_parser(n)+        s.add_argument("page", help="the component page, owner/slug (e.g. adom/tps54202ddcr)")+        s.add_argument("--schlib", help="target .SchLib on the Windows machine (default Documents\\Adom\\WikiParts\\AdomWiki.SchLib)")+        s.add_argument("--pcblib", help="target .PcbLib (default Documents\\Adom\\WikiParts\\AdomWiki.PcbLib)")+        s.add_argument("--target", help="the adom-bridge machine running Altium")+        s.add_argument("--no-install", action="store_true", help="do not register the libraries in Altium's Installed Libraries")+        if n == "place":+            s.add_argument("--sheet", required=True, help="the .SchDoc on the Windows machine")+            s.add_argument("--x", type=int, help="mils")+            s.add_argument("--y", type=int, help="mils")+            s.add_argument("--designator")+    a = ap.parse_args()+    {"import": cmd_import, "place": cmd_place}[a.cmd](a)+++if __name__ == "__main__":+    main()
uninstall.sh+1−1
@@ -4,5 +4,5 @@ for HOME_DIR in "$HOME/.claude/skills" "$HOME/.codex/skills"; do   rm -rf "$HOME_DIR/altium-bridge" "$HOME_DIR/altium-bridge-plan" "$HOME_DIR/altium-part-import" "$HOME_DIR/altium-kicad-import" "$HOME_DIR/altium-install" "$HOME_DIR/altium-libraries" "$HOME_DIR/altium-boards" "$HOME_DIR/altium-outputs" "$HOME_DIR/altium-instances" "$HOME_DIR/altium-licensing" "$HOME_DIR/altium-uia" "$HOME_DIR/altium-wiki-import" "$HOME_DIR/altium-a365" "$HOME_DIR/altium-bom" "$HOME_DIR/altium-schematic-edit" "$HOME_DIR/altium-bridge-dev" "$HOME_DIR/altium-bridge-publish" done rm -rf "$HOME/.local/share/adom/altium-bridge"-rm -f "$HOME/.local/bin/altium-dashboard" "$HOME/.local/bin/altium-kicad2altium" "$HOME/.local/bin/altium-symbol-modes" "$HOME/.local/bin/altium-kicad-downgrade" "$HOME/.local/bin/altium-bom"+rm -f "$HOME/.local/bin/altium-dashboard" "$HOME/.local/bin/altium-kicad2altium" "$HOME/.local/bin/altium-symbol-modes" "$HOME/.local/bin/altium-kicad-downgrade" "$HOME/.local/bin/altium-bom" "$HOME/.local/bin/altium-wiki-part" echo "OK: altium-bridge skills + dashboard removed"