skill
kicad10-to-altium
Public Unreviewedby barrett-land
Import KiCad 10 symbols into Altium Designer correctly. Altium 26 supports KiCad through 9 only, so a raw KiCad 10 import silently loses property visibility and every hidden field lands visible. Downgrades v10 to v9 first, then imports headlessly, links the footprint, and repairs visibility.
← Commit history
Publish 1.1.0
6 files changed
+594
SKILL.mdadded+224@@ -0,0 +1,224 @@+---+name: kicad10-to-altium+description: Import KiCad symbols into Altium Designer correctly, especially from KiCad 10. Altium 26's importer supports KiCad through 9 only, so a raw KiCad 10 import silently drops property visibility and every hidden field arrives visible, piled on top of the symbol. It also imports text in its own font and colour rather than Altium's. Downgrades the library to KiCad 9 first, imports headlessly via the AdomBridge extension, links a real footprint model, normalises font/size/colour to Altium's defaults, and repairs visibility on already-broken symbols. Use when importing KiCad parts into Altium, when an imported symbol shows overlapping text, when imported parts look different from hand-made ones, or when converting a KiCad 10 library. Trigger words - kicad to altium, kicad 10 altium import, altium import overlapping fields, hidden fields visible after import, imported symbol wrong font, downgrade kicad 10 library, kicad symbol into altium library.+---++# kicad10-to-altium++Altium 26's KiCad importer understands the format **through KiCad 9**. KiCad 10 files still+import, but silently lose things — most visibly **property visibility**, so fields that are hidden+in KiCad land visible in Altium and stack into an unreadable pile on the symbol.++This skill gets KiCad 10 parts in cleanly.++## Measured behaviour — not guesswork++The same generic `Device:R` symbol extracted from each installed KiCad, imported through+`AdomBridge:ImportKiCad`, on Altium 26.8/26.9:++| Source | Importer warnings | Params | **Hidden preserved** |+|---|---|---|---|+| KiCad 8 (`20231120`) | 1 | 9 | 5 ✓ |+| KiCad 9 (`20241209`) | 2 | 9 | 5 ✓ |+| **KiCad 10 (`20251024`)** | **7** | 9 | **0 ✗** |+| KiCad 10 → 9 (this skill) | 2 | 9 | **5 ✓** |++Pins (2) and body geometry survive in every case. It is the metadata that degrades.++Repeated on a real IC — `STM32F103C8Tx`, LQFP-48, pulled from the wiki as KiCad 10, a *derived*+symbol extending a base:++| | Result |+|---|---|+| Raw v10 → KiCad 9 | `Unable to load library` |+| Converted, symbols left in source order | **still** `Unable to load library` (see below) |+| Converted + reordered (this skill) | loads in KiCad **9 and 10** |+| → imported to Altium | 2 symbols, **48 pins**, 5 hidden fields preserved each |++The importer log is honest about what it skipped: 238 × `alternate of pin` (alternate pin+functions — Altium has no equivalent), 4 × `hide of pin`, 3 × `embedded_fonts`, 2 ×+`exclude_from_sim`, 1 × `generator_version`. **Alternate pin functions are the real loss on an+MCU** — pin *names* survive, the alternate-function list does not.++Also measured: **KiCad 9 cannot open a KiCad 10 library at all** (`Unable to load library`), so+there is no "just re-save it in KiCad 9" escape hatch for a v10-only third-party library. That is+why this converter exists.++## Step 1 — downgrade, if the source is KiCad 10++```bash+python3 kicad_v10_to_v9.py in.kicad_sym -o out_v9.kicad_sym+# hide relocated into (effects ...): 5+# dropped do_not_autoplace: 7+# dropped show_name: 7+# dropped in_pos_files: 1+# dropped duplicate_pin_numbers_are_jumpers: 1+```++Validate with KiCad itself, the only authority that matters:++```bash+kicad-cli sym export svg out_v9.kicad_sym -o /tmp/svg # must exit 0+```++### Derived symbols: order matters, and only KiCad 9 cares++The failure that survives a token-perfect conversion. KiCad 9 resolves `(extends "Base")` in a+**single forward pass**, so a derived symbol placed *before* its base makes the whole library fail+to load. KiCad 10 resolves it regardless of order — so v10 files happily store the derived symbol+first, and real ones do.++The symptom is maximally unhelpful: `kicad-cli` says only `Unable to load library`, naming neither+symbol, and the file is valid S-expression that **KiCad 10 opens fine**. Use that asymmetry to+diagnose — *loads in 10, fails in 9* means a v9-only rule, not a broken file.++The converter topologically reorders symbols so every base precedes its dependants:++```+ reordered 2 symbol(s) so each (extends ...) base comes first+```++This is not an edge case. Derived symbols are how KiCad ships most ICs — every part in+`MCU_ST_STM32F1` is one — so a converter that ignores order works on passives and fails on+practically every real IC.++**Do not attempt this with regex.** `hide` is *relocated* between versions, not renamed: v9 nests+`(hide yes)` **inside `(effects ...)`**; v10 moved it **out** to be a direct child of `property`. A+token substitution in place produces a file that is neither version — KiCad 9 rejects it and Altium+silently ignores the orphan token, so visibility is still lost while the warning count *drops*,+which looks like success. That failure mode is why this ships a real S-expression parser.++### What downgrading costs++| Dropped | Real cost |+|---|---|+| `show_name`, `do_not_autoplace` | Cosmetic — field-label display and auto-placement pinning |+| `in_pos_files` | Low — v9 expresses this on the **footprint** as `(attr exclude_from_pos_files)`, which is what pcbnew actually reads when generating position files |+| `duplicate_pin_numbers_are_jumpers` | Low — `no` on all 3609 stdlib symbols, matching v9's implicit behaviour. The tool **warns** if it sees `yes`, which has no v9 equivalent |+| `corner_radius` | Cosmetic — rounded rectangles become square |+| stacked pins | v9 cannot represent them; rare |++Nothing electrical is lost for ordinary parts.++## Step 2 — import headlessly++Requires the **AdomBridge extension** (`altium_install_extension`, then restart Altium).++```+# Request file at a SPACE-FREE path: Altium's -R parser splits on whitespace.+# Mode=Import+# File=<path to the v9 .kicad_sym>+X2.exe -RAdomBridge:ImportKiCad(Req=C:\Users\Public\AdomAltium\importkicad_req.txt)+```++`Mode=List` instead enumerates every importer controller and its supported document kinds — useful+when you are unsure a format is handled.++Output lands in `Imported <stem>\<stem>.SchLib` beside the source, with a `.LOG` listing every+field the importer did not understand. **Read that log** — it is the honest record of what was+dropped.++## Step 3 — link a real footprint++The import creates the PCB model slot but leaves it **empty**: `ModelType=PCBLIB` with no+`ModelName`. The `Footprint` value survives only as a text parameter, which Altium does not use for+placement. Populate it while copying into the destination library:++```bash+adom-desktop --ai-thread "<thread>" altium_copy_sch_component '{+ "src": "...\\Imported out_v9\\out_v9.SchLib",+ "dst": "...\\Libraries\\Symbols\\Resistors.SchLib",+ "component": "R", "newname": "R_10k_0402",+ "footprint": "R0402", "designator": "R?"+}'+```++Prefer a footprint that already exists in the destination `.PcbLib`: the importer does **not**+bring footprints across at all (`.kicad_mod` / `.pretty` are unsupported), and an in-house+footprint is usually better than anything the import would produce.++## Step 4 — normalise font, size and colour++The importer **does not reuse the document's default font**. It appends its own entries to the+library's font table and carries KiCad's field colour across, so an imported part renders in a+different face, size *and* hue from every hand-made part in the same library — obvious the moment+both sit on one sheet.++Measured by reading the `.SchLib` binary directly (`RECORD=34` designator, `RECORD=41`+comment/parameters) for a hand-made part and an imported one in the *same* library:++| | Font | Face / size | `Color` |+|---|---|---|---|+| Hand-made Altium part | `FontID=1` | Times New Roman 10 | `8388608` |+| **KiCad-imported part** | `FontID=3` / `4` | **Yu Gothic UI Light 8** | **`8650884`** |++Altium stores colour as **BGR**, not RGB, so `8388608` = `$800000` = **navy RGB(0,0,128)**. The+imported `8650884` = `$840204` = RGB(4,2,132), a slightly purple blue — close enough to look like a+rendering artefact rather than a data difference, which is why it goes unnoticed.++Note the importer *adds* fonts rather than reusing existing ones, so a library imported into+repeatedly accumulates near-duplicate table entries.++```bash+adom-desktop --ai-thread "<thread>" altium_set_text_style '{+ "schlib": "...\\Libraries\\Symbols\\Resistors.SchLib",+ "component": "R_10k_0402"+}'+# -> {"components":1,"textObjects":11,"fontId":1,"color":8388608,"saved":true}+```++Defaults are `fontid=1` and `color=8388608`; pass `"component": "*"` to restyle the whole library.+It is idempotent, so running it over already-correct parts is safe.++⚠️ `fontid=1` is right because Altium seeds a new library's font table with its default face at+index 1 — it is **not** guaranteed in a library assembled from elsewhere. Confirm before trusting+it, by reading the `FontIdCount` / `FontName<n>` / `Size<n>` keys out of the `FileHeader` stream:++```python+import olefile, re+txt = olefile.OleFileIO('X.SchLib').openstream('FileHeader').read().decode('latin-1')+for m in re.finditer(r'\|Size(\d+)=(\d+)', txt):+ i = m.group(1)+ print(i, m.group(2), re.search(r'\|FontName%s=([^|]*)' % i, txt).group(1))+```++## Step 5 — repair visibility on symbols already imported badly++The verb takes a **whitelist of parameters to show**; everything else is hidden.++```bash+adom-desktop --ai-thread "<thread>" altium_set_visibility '{+ "schlib": "...\\Resistors.SchLib",+ "component": "R_10k_0402",+ "visible": "Value"+}'+```++## Gotchas that will cost you time++- **The bridge is single-threaded — fire verbs SERIALLY.** Two overlapping calls wedge the bridge's+ Python module-lock: `request.txt` stops updating and every later verb times out. Recover by+ killing the bridge's `python.exe` (Adom Desktop respawns it); restarting Altium is not needed.+- **Verbs report "Request timed out" while succeeding.** Adom Desktop caps a call around 60s and+ slow library saves outrun it. Read `C:\Users\Public\AdomAltium\response.json` before retrying, or+ you will create duplicates (`Name`, `Name_1`).+- **Paths with spaces break `-R` dispatch and plain file-open alike.** Altium splits the argument+ and treats the tail as files to open, launching a *second* instance+ (`ERROR_INVALID_STATE 0x8007139F`). Stage sources at space-free paths.+- **BOMs break the parse.** PowerShell's `Set-Content -Encoding UTF8` writes one; use+ `[IO.File]::WriteAllText($p, $t, (New-Object Text.UTF8Encoding($false)))`.+- **Back up the destination library first** — these verbs save in place.+- 🔴 **If every verb times out but Altium works fine, count the `X2.EXE` processes.** A `-R` call+ should hand off to the running instance and exit immediately. When the handoff breaks, each+ attempt leaves an orphan launcher — **~9 threads, no window** — and the command never reaches the+ real instance (~90 threads, has a window). The oldest orphan blocks the handoff and every later+ call queues behind it, so the app stays perfectly usable while automation is dead. That looks+ exactly like a wedged script engine and is **not** one. Kill the windowless orphans, keep the+ real instance, and dispatch recovers instantly. A stray instance launched from a path with+ spaces is a common way to start the pile-up.++## Where the version really sits++`KICAD_LIB_V6` in the controller's doc-kind list is legacy naming for the S-expression era, **not**+a version ceiling: the parser tracks through KiCad 9. That list also **understates** what is+accepted — `.kicad_sym` is absent from it yet imports fine.
__pycache__/kicad_v10_to_v9.cpython-312.pycadded⋯ 1 unchanged line ⋯
install.shadded+8@@ -0,0 +1,8 @@+#!/bin/bash+set -e+PKG_DIR="$(cd "$(dirname "$0")" && pwd)"+DEST="$HOME/.claude/skills/kicad10-to-altium"+mkdir -p "$DEST"+ln -sfn "$PKG_DIR/SKILL.md" "$DEST/SKILL.md"+ln -sfn "$PKG_DIR/kicad_v10_to_v9.py" "$DEST/kicad_v10_to_v9.py"+echo "[kicad10-to-altium] installed (symlinked from $PKG_DIR)"
kicad_v10_to_v9.pyadded+309@@ -0,0 +1,309 @@+#!/usr/bin/env python3+"""Downgrade a KiCad 10 .kicad_sym symbol library to KiCad 9 format.++WHY THIS EXISTS+KiCad 9 cannot open KiCad 10 libraries at all ("Unable to load library"), and Altium's+importer understands KiCad through 9 but degrades on 10 -- most importantly it drops property+VISIBILITY, so fields hidden in KiCad arrive visible in Altium. For a third-party library that+ships v10 only, there is no "just re-save in KiCad 9" escape hatch. Hence this.++WHAT ACTUALLY CHANGES (measured by diffing the same symbol out of KiCad 8/9/10):++ hide RELOCATED, not renamed. v9 nests (hide yes) INSIDE (effects ...);+ v10 moved it OUT to be a direct child of (property ...). A regex that+ rewrites the token in place produces a file that is NEITHER version --+ KiCad 9 rejects it and Altium silently ignores the orphan token. This is+ the whole reason a real parser is needed.+ show_name v10 only -> dropped (cosmetic: show the field name beside its value)+ do_not_autoplace v10 only -> dropped (cosmetic: pin a field against auto-placement)+ in_pos_files v10 only -> dropped. NOT a real loss: v9 expresses this on the+ FOOTPRINT as (attr exclude_from_pos_files), which is what pcbnew+ actually reads when generating position files.+ duplicate_pin_numbers_are_jumpers+ v10 only -> dropped. Always "no" across all 3609 stdlib symbols,+ which matches v9's implicit behaviour, so normally a no-op. If a+ library sets it to "yes" that ERC semantic has no v9 equivalent --+ the tool warns rather than pretending otherwise.+ corner_radius v10 only -> dropped (rounded rectangles become square)+ version 20251024 -> 20241209++ SYMBOL ORDER Not a token change at all, and the one that actually bites. KiCad 9+ resolves (extends "Base") in a SINGLE FORWARD PASS while parsing, so a+ derived symbol placed before its base makes the entire library fail to+ load. KiCad 10 resolves the reference regardless of order, so v10 files+ are free to store the derived symbol first -- and real ones do. Convert+ such a file token-by-token and every token is correct, yet KiCad 9 still+ says only "Unable to load library", naming neither symbol. This tool+ topologically reorders symbols so each base precedes its dependants.+ Derived symbols are everywhere in practice (KiCad's own STM32 libraries+ are built on them), so a converter that ignores order works on simple+ passives and fails on almost every real IC.++Usage:+ python3 kicad_v10_to_v9.py in.kicad_sym [-o out.kicad_sym] [--quiet]++Validate the result with KiCad 9 itself -- that is the only authority that matters:+ kicad-cli sym export svg out.kicad_sym -o /tmp/svg+"""++from __future__ import annotations++import argparse+import sys+from pathlib import Path++V10 = "20251024"+V9 = "20241209"++# Direct children of (property ...) that v9 has no grammar for.+PROPERTY_DROP = {"show_name", "do_not_autoplace"}+# Direct children of (symbol ...) that v9 has no grammar for.+SYMBOL_DROP = {"in_pos_files", "duplicate_pin_numbers_are_jumpers"}+# Anywhere.+GLOBAL_DROP = {"corner_radius"}+++class Node(list):+ """An S-expression list. Atoms are plain str (quoted strings keep their quotes)."""+++def parse(text: str) -> Node:+ """Parse S-expressions. Handles quoted strings with backslash escapes."""+ i, n = 0, len(text)+ stack: list[Node] = [Node()]+ while i < n:+ c = text[i]+ if c.isspace():+ i += 1+ elif c == "(":+ node = Node()+ stack[-1].append(node)+ stack.append(node)+ i += 1+ elif c == ")":+ if len(stack) == 1:+ raise ValueError(f"unbalanced ')' at offset {i}")+ stack.pop()+ i += 1+ elif c == '"':+ j = i + 1+ while j < n:+ if text[j] == "\\":+ j += 2+ continue+ if text[j] == '"':+ break+ j += 1+ stack[-1].append(text[i : j + 1])+ i = j + 1+ else:+ j = i+ while j < n and not text[j].isspace() and text[j] not in "()":+ j += 1+ stack[-1].append(text[i:j])+ i = j+ if len(stack) != 1:+ raise ValueError("unbalanced '(' -- unexpected end of input")+ return stack[0]+++def head(node) -> str | None:+ """The tag of an S-expression, e.g. 'property' for (property "Foo" ...)."""+ if isinstance(node, Node) and node and isinstance(node[0], str):+ return node[0]+ return None+++def dump(node, indent: int = 0) -> str:+ """Serialise back to KiCad's tab-indented style."""+ if not isinstance(node, Node):+ return str(node)+ tag = head(node)+ # Keep short leaf forms on one line, which is what KiCad does.+ if all(not isinstance(c, Node) for c in node):+ return "(" + " ".join(str(c) for c in node) + ")"+ pad = "\t" * indent+ out = ["(" + (tag or "")]+ inline = []+ rest = []+ for child in node[1:]:+ (inline if not isinstance(child, Node) and not rest else rest).append(child)+ if inline:+ out[0] += " " + " ".join(str(c) for c in inline)+ for child in rest:+ out.append(pad + "\t" + dump(child, indent + 1))+ return ("\n".join(out)) + "\n" + pad + ")"+++class Stats:+ def __init__(self):+ self.hide_moved = 0+ self.hide_dropped = 0+ self.dropped: dict[str, int] = {}+ self.jumpers_yes = 0+ self.reordered = 0++ def drop(self, tag: str):+ self.dropped[tag] = self.dropped.get(tag, 0) + 1+++def convert_property(prop: Node, st: Stats) -> Node:+ """Move (hide yes) into (effects ...) and drop v10-only children."""+ hide_yes = False+ kept: list = []+ for child in prop:+ tag = head(child)+ if tag == "hide":+ val = child[1] if len(child) > 1 else "yes"+ if val == "yes":+ hide_yes = True+ st.hide_moved += 1+ else:+ st.hide_dropped += 1+ continue+ if tag in PROPERTY_DROP:+ st.drop(tag)+ continue+ kept.append(child)++ out = Node(kept)+ if hide_yes:+ effects = next((c for c in out if head(c) == "effects"), None)+ if effects is None:+ effects = Node(["effects"])+ out.append(effects)+ # v9 puts (hide yes) inside (effects ...), after (font ...).+ if not any(head(c) == "hide" for c in effects):+ effects.append(Node(["hide", "yes"]))+ return out+++def walk(node, st: Stats, parent_tag: str | None = None):+ if not isinstance(node, Node):+ return node+ tag = head(node)++ if tag == "property":+ node = convert_property(node, st)++ kept = Node()+ for child in node:+ ctag = head(child)+ if ctag in GLOBAL_DROP:+ st.drop(ctag)+ continue+ if tag == "symbol" and ctag in SYMBOL_DROP:+ if ctag == "duplicate_pin_numbers_are_jumpers":+ val = child[1] if len(child) > 1 else "no"+ if val == "yes":+ st.jumpers_yes += 1+ st.drop(ctag)+ continue+ if ctag == "version" and len(child) > 1 and child[1] == V10:+ child = Node(["version", V9])+ kept.append(walk(child, st, tag) if isinstance(child, Node) else child)+ return kept+++def order_symbols(root: Node, st: Stats) -> Node:+ """Emit base symbols BEFORE the symbols that (extends ...) them.++ KiCad 9 resolves `extends` in a single forward pass while parsing, so a derived+ symbol that appears before its base makes the whole library fail to load --+ `kicad-cli` reports only "Unable to load library", naming neither symbol. KiCad 10+ resolves the reference independently of order, so a v10 library is free to store+ the derived symbol first, and many do. Converting such a file token-by-token+ therefore produces valid-looking v9 that KiCad 9 still refuses.++ Stable: symbols that need no move keep their original relative order.+ """+ lib = root[0] if len(root) == 1 and head(root[0]) == "kicad_symbol_lib" else None+ if lib is None:+ return root++ syms = [c for c in lib[1:] if head(c) == "symbol"]+ if len(syms) < 2:+ return root++ def name_of(s):+ return s[1] if len(s) > 1 and isinstance(s[1], str) else None++ def base_of(s):+ for c in s:+ if head(c) == "extends" and len(c) > 1:+ return c[1]+ return None++ by_name = {name_of(s): s for s in syms}+ ordered, placed = [], set()++ def place(s, seen):+ sid = id(s)+ if sid in placed:+ return+ base = base_of(s)+ parent = by_name.get(base)+ # Missing base or a cycle: emit in place rather than guess.+ if parent is not None and id(parent) not in placed and id(parent) not in seen:+ place(parent, seen | {sid})+ if sid not in placed:+ placed.add(sid)+ ordered.append(s)++ for s in syms:+ place(s, set())++ if [id(s) for s in ordered] != [id(s) for s in syms]:+ st.reordered = sum(1 for a, b in zip(ordered, syms) if id(a) != id(b))+ out = Node([lib[0]])+ it = iter(ordered)+ for c in lib[1:]:+ out.append(next(it) if head(c) == "symbol" else c)+ return Node([out])+ return root+++def main() -> int:+ ap = argparse.ArgumentParser(description="Downgrade a KiCad 10 .kicad_sym to KiCad 9.")+ ap.add_argument("input")+ ap.add_argument("-o", "--output")+ ap.add_argument("--quiet", action="store_true")+ args = ap.parse_args()++ src = Path(args.input)+ text = src.read_text(encoding="utf-8-sig") # tolerate a BOM; never write one back++ if f"(version {V10})" not in text.replace("\n", " ").replace("\t", " "):+ found = "unknown"+ for line in text.splitlines()[:8]:+ if "(version" in line:+ found = line.strip()+ break+ print(f"warning: input does not declare version {V10} (saw: {found})", file=sys.stderr)++ tree = parse(text)+ st = Stats()+ out = order_symbols(walk(tree, st), st)++ body = "\n".join(dump(child) for child in out) + "\n"+ dst = Path(args.output) if args.output else src.with_name(src.stem + "_v9.kicad_sym")+ dst.write_text(body, encoding="utf-8")++ if not args.quiet:+ print(f"wrote {dst} ({dst.stat().st_size} bytes, from {src.stat().st_size})")+ print(f" hide relocated into (effects ...): {st.hide_moved}")+ if st.hide_dropped:+ print(f" (hide no) dropped: {st.hide_dropped}")+ for tag, count in sorted(st.dropped.items()):+ print(f" dropped {tag}: {count}")+ if st.reordered:+ print(f" reordered {st.reordered} symbol(s) so each (extends ...) base "+ "comes first -- KiCad 9 resolves extends in one forward pass")+ if st.jumpers_yes:+ print(f" !! {st.jumpers_yes} symbol(s) had duplicate_pin_numbers_are_jumpers=yes -- "+ "that ERC semantic has NO v9 equivalent and is genuinely lost")+ return 0+++if __name__ == "__main__":+ sys.exit(main())
package.jsonadded+49@@ -0,0 +1,49 @@+{+ "slug": "kicad10-to-altium",+ "version": "1.1.0",+ "type": "skill",+ "description": "Import KiCad 10 symbols into Altium Designer correctly. Altium 26 supports KiCad through 9 only, so a raw KiCad 10 import silently loses property visibility and every hidden field lands visible. Downgrades v10 to v9 first, then imports headlessly, links the footprint, and repairs visibility.",+ "tags": [+ "skill",+ "kicad",+ "altium",+ "import",+ "library",+ "eda"+ ],+ "dependencies": {+ "barrett-land/adom-desktop-altium-bridge": "*"+ },+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh"+ },+ "needs_sudo": false,+ "discovery_triggers": [+ "import kicad symbol into altium",+ "kicad 10 to altium",+ "kicad symbols show all fields in altium",+ "altium import lost hidden fields",+ "downgrade kicad 10 library",+ "kicad to altium library"+ ],+ "discovery_pitch": "Use when bringing KiCad symbols into Altium, especially from KiCad 10, or when an imported symbol shows a pile of overlapping text fields.",+ "sample_prompts": [+ {+ "label": "Import a symbol",+ "prompt": "Import this KiCad symbol into my Altium resistors library"+ },+ {+ "label": "Fix a bad import",+ "prompt": "My imported Altium symbol shows all its fields overlapping, fix it"+ },+ {+ "label": "Downgrade a library",+ "prompt": "Convert this KiCad 10 library to KiCad 9"+ }+ ],+ "hero": {+ "type": "image",+ "path": "docs/hero.png"+ }+}
uninstall.shadded+4@@ -0,0 +1,4 @@+#!/bin/bash+set -e+rm -rf "$HOME/.claude/skills/kicad10-to-altium"+echo "[kicad10-to-altium] removed"