@huaqiu/dsh-kicad 0.4.12 → 0.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -5,19 +5,24 @@
5
5
  * This plugin ships TWO things that used to need two installations:
6
6
  *
7
7
  * 1. ten KiCad tools, registered with `ctx.tools.register(defineTool(...))`
8
- * 2. the `kicad-ipc` skill, registered with `ctx.skills.register(...)`
8
+ * 2. every bundled skill under `skills/`, registered with
9
+ * `ctx.skills.register(...)` — currently `kicad-ipc` (the reasoning around
10
+ * the tools) and `hardware-design-brief` (requirements → design inputs)
9
11
  *
10
12
  * Installing the Huaqiu DSH PCB/EDA bundle therefore makes KiCad agent
11
- * capabilities AND the skill that teaches the agent to use them available out of
13
+ * capabilities AND the skills that teach the agent to use them available out of
12
14
  * the box. There is no separate skill installation step, and this plugin adds no
13
15
  * second skill-registration mechanism — `ctx.skills.register()` is the DSH
14
16
  * runtime's own plugin-bundled skill channel.
15
17
  *
18
+ * The skill set is data, not code: `./skills.ts` owns the registry, so adding a
19
+ * skill from `kicad-agent` is a copy plus a registry entry.
20
+ *
16
21
  * ── Boundaries ──────────────────────────────────────────────────────────────
17
22
  * KiCad IPC is reached through the bundled Python scripts under
18
- * `skills/kicad-ipc/scripts/` (see `./ipc.ts`). This package holds no HQ Edge
19
- * dependency of any kind — no runtime, executable, service, port, config or
20
- * artifact dependency (§3, §21) — and no `@hqedge/*` import.
23
+ * `skills/<KICAD_SCRIPT_SKILL_ID>/scripts/` (see `./ipc.ts`). This package holds
24
+ * no HQ Edge dependency of any kind — no runtime, executable, service, port,
25
+ * config or artifact dependency (§3, §21) — and no `@hqedge/*` import.
21
26
  *
22
27
  * @module @huaqiu/dsh-kicad
23
28
  */
@@ -32,8 +37,8 @@ import {
32
37
  type KicadConfig,
33
38
  type KicadConfigInput,
34
39
  } from './config.js'
35
- import { requireSkillDir, resolveSkillDir, scriptsDir } from './paths.js'
36
- import { KICAD_SKILL_NAME } from './scripts.js'
40
+ import { requireSkillDir, resolveSkillDir, scriptsDir, skillsRoot } from './paths.js'
41
+ import { KICAD_SCRIPT_SKILL_ID, KICAD_SKILL_IDS, kicadSkill } from './skills.js'
37
42
  import { createKicadTools, kicadToolNames } from './tools.js'
38
43
 
39
44
  /** Plugin id — matches package.json. */
@@ -43,7 +48,7 @@ export const name = '@huaqiu/dsh-kicad'
43
48
  * Cordis services this half depends on.
44
49
  *
45
50
  * `skills` is REQUIRED: it is the DSH runtime's skill registry, and registering
46
- * the bundled `kicad-ipc` skill is this plugin's core job. Without the inject,
51
+ * the bundled skills is this plugin's core job. Without the inject,
47
52
  * `apply()`'s `ctx.skills` access would throw
48
53
  * `cannot get property "skills" without inject`.
49
54
  *
@@ -57,9 +62,16 @@ export const inject = ['skills', 'tools'] as const
57
62
  export type { KicadConfig, KicadConfigInput } from './config.js'
58
63
  export type { KicadError, KicadErrorKind, ScriptRun } from './ipc.js'
59
64
  export type { KicadScript, ScriptEffect } from './scripts.js'
60
- export { KICAD_SCRIPTS, KICAD_SCRIPT_IDS, KICAD_SKILL_NAME, kicadScript } from './scripts.js'
65
+ export type { KicadBundledSkill } from './skills.js'
66
+ export { KICAD_SCRIPTS, KICAD_SCRIPT_IDS, kicadScript } from './scripts.js'
67
+ export {
68
+ KICAD_SCRIPT_SKILL_ID,
69
+ KICAD_SKILL_IDS,
70
+ KICAD_SKILLS,
71
+ kicadSkill,
72
+ } from './skills.js'
61
73
  export { kicadToolNames } from './tools.js'
62
- export { resolveSkillDir, requireSkillDir, scriptsDir } from './paths.js'
74
+ export { resolveSkillDir, requireSkillDir, scriptsDir, skillsRoot } from './paths.js'
63
75
  export { runKicadScript, classifyRun, invokeKicadScript } from './ipc.js'
64
76
  export { createKicadTools } from './tools.js'
65
77
 
@@ -110,21 +122,11 @@ const log = getLogger(COMPONENT)
110
122
  // `@huaqiu/dsh-eda-host`.
111
123
  log.info('dsh-kicad: module loaded (waiting for the skills + tools services)')
112
124
 
113
- /**
114
- * Used only when the bundled SKILL.md has no parseable `description`
115
- * frontmatter. The real value always comes from the shipped file, so the skill
116
- * catalog cannot drift away from the skill body.
117
- */
118
- const FALLBACK_SKILL_DESCRIPTION =
119
- 'Operate a KiCad PCB through the official KiCad IPC API: inspect the live ' +
120
- 'board and create, modify or delete objects. Use for PCB automation and ' +
121
- 'autorouter-result import — not for editing .kicad_pcb files directly.'
122
-
123
125
  /**
124
126
  * Extract the `description` field from SKILL.md YAML frontmatter.
125
127
  *
126
128
  * Only the single-line form is supported (quoted or bare), which is what the
127
- * migrated skill uses. Returns `undefined` when absent so the caller can fall
129
+ * migrated skills use. Returns `undefined` when absent so the caller can fall
128
130
  * back rather than registering a skill with an empty description — DSH ignores
129
131
  * frontmatter-less skills entirely, so an empty description would silently
130
132
  * break discovery.
@@ -141,31 +143,48 @@ export function skillDescription(markdown: string): string | undefined {
141
143
  return unquoted.length > 0 ? unquoted : undefined
142
144
  }
143
145
 
144
- /**
145
- * Read the bundled `kicad-ipc` SKILL.md.
146
- *
147
- * Exposed for tests and for callers that want the skill body without loading
148
- * the plugin (e.g. a packaging check).
149
- */
150
- export function readBundledSkill(moduleUrl: string, override?: string): {
151
- dir: string
146
+ /** One bundled skill, as read from disk and ready for `ctx.skills.register()`. */
147
+ export interface BundledSkill {
148
+ /** Skill id — equals the directory name and the DSH-catalogued id. */
152
149
  name: string
150
+ /** Directory holding SKILL.md, `references/` and `scripts/`. */
151
+ dir: string
152
+ /** Catalogue description (from SKILL.md frontmatter, else the registry). */
153
153
  description: string
154
+ /** Full SKILL.md body. */
154
155
  content: string
155
- } {
156
- const dir = requireSkillDir(moduleUrl, override)
156
+ }
157
+
158
+ /**
159
+ * Read one bundled SKILL.md.
160
+ *
161
+ * Exposed for tests and for callers that want a skill body without loading the
162
+ * plugin (e.g. a packaging check).
163
+ *
164
+ * @param moduleUrl - `import.meta.url` of the calling module.
165
+ * @param skillId - bundled skill id (see `./skills.ts`).
166
+ * @param override - explicit skills root (plugin config or env var).
167
+ */
168
+ export function readBundledSkill(
169
+ moduleUrl: string,
170
+ skillId: string,
171
+ override?: string,
172
+ ): BundledSkill {
173
+ const dir = requireSkillDir(moduleUrl, skillId, override)
157
174
  const content = readFileSync(join(dir, 'SKILL.md'), 'utf8')
158
175
  return {
159
176
  dir,
160
- name: KICAD_SKILL_NAME,
161
- description: skillDescription(content) ?? FALLBACK_SKILL_DESCRIPTION,
177
+ name: skillId,
178
+ // The shipped frontmatter is authoritative; the registry summary is only a
179
+ // safety net so the catalog can never drift away from the skill body.
180
+ description: skillDescription(content) ?? kicadSkill(skillId).summary,
162
181
  content,
163
182
  }
164
183
  }
165
184
 
166
185
  /**
167
- * Read the bundled skill, or `null` when the installed package does not carry
168
- * one.
186
+ * Read one bundled skill, or `null` when the installed package does not carry
187
+ * it.
169
188
  *
170
189
  * ── Why this degrades instead of throwing ──────────────────────────────────
171
190
  * A missing `skills/` tree is a PACKAGING failure, and the honest signal for a
@@ -177,25 +196,29 @@ export function readBundledSkill(moduleUrl: string, override?: string): {
177
196
  * That actually happened: HQ Edge's builtin-plugin staging copied only
178
197
  * `package.json` + `lib/`, so `skills/` never reached the bundle and the
179
198
  * server could not start at all. The correct blast radius for "this plugin's
180
- * skill is missing" is "this plugin is degraded", so we log at error level
181
- * with the exact path and remedy, skip skill registration, and keep the tools
182
- * (which fail per-call with a typed `FAILED_PRECONDITION` rather than at load).
199
+ * skill is missing" is "this skill is unavailable", so we log at error level
200
+ * with the exact path and remedy and skip that skill — per skill, so a broken
201
+ * `kicad-ipc` never hides a perfectly good `hardware-design-brief`. The tools
202
+ * are always kept (they fail per-call with a typed `FAILED_PRECONDITION`
203
+ * rather than at load).
183
204
  *
184
205
  * @param moduleUrl - `import.meta.url` of the calling module.
185
- * @param override - explicit skill directory (plugin config or env var).
206
+ * @param skillId - bundled skill id (see `./skills.ts`).
207
+ * @param override - explicit skills root (plugin config or env var).
186
208
  */
187
209
  export function tryReadBundledSkill(
188
210
  moduleUrl: string,
211
+ skillId: string,
189
212
  override?: string,
190
- ): { dir: string; name: string; description: string; content: string } | null {
213
+ ): BundledSkill | null {
191
214
  try {
192
- return readBundledSkill(moduleUrl, override)
215
+ return readBundledSkill(moduleUrl, skillId, override)
193
216
  } catch (err) {
194
217
  log.error(
195
- 'dsh-kicad: bundled skill unavailable — continuing degraded, the KiCad ' +
196
- 'tools are registered but will fail until the package is reinstalled',
218
+ `dsh-kicad: bundled skill "${skillId}" unavailable — continuing degraded, ` +
219
+ 'the KiCad tools are registered but will fail until the package is reinstalled',
197
220
  {
198
- expectedDir: resolveSkillDir(moduleUrl, override),
221
+ expectedDir: resolveSkillDir(moduleUrl, skillId, override),
199
222
  error: String((err as Error)?.message ?? err),
200
223
  },
201
224
  )
@@ -204,16 +227,16 @@ export function tryReadBundledSkill(
204
227
  }
205
228
 
206
229
  /**
207
- * Host plugin body — register the `kicad-ipc` skill and the KiCad tools.
230
+ * Host plugin body — register every bundled skill and the KiCad tools.
208
231
  *
209
- * Both halves are registered here so that one installation delivers both. The
210
- * skill's `resourceBase` points at the bundled skill directory, which is how
211
- * the agent reaches `references/ipc-pcb-workflows.md` and `scripts/` as
212
- * progressive-disclosure resources.
232
+ * Both halves are registered here so that one installation delivers both. Each
233
+ * skill's `resourceBase` points at its own bundled directory, which is how the
234
+ * agent reaches `references/` and `scripts/` as progressive-disclosure
235
+ * resources.
213
236
  *
214
237
  * @param ctx - real cordis context (node side).
215
238
  * @param config - plugin overlay config (python interpreter, timeouts).
216
- * @returns disposer — unregisters the skill and the tools on plugin dispose.
239
+ * @returns disposer — unregisters every skill and tool on plugin dispose.
217
240
  */
218
241
  export function apply(ctx: Context, config: KicadConfigInput = {}): () => void {
219
242
  if (!ctx.tools || typeof ctx.tools.register !== 'function') {
@@ -225,26 +248,35 @@ export function apply(ctx: Context, config: KicadConfigInput = {}): () => void {
225
248
 
226
249
  const resolved = resolveKicadConfig(config)
227
250
 
228
- // Degraded rather than fatal: see tryReadBundledSkill. A null skill means
229
- // the installed package is incomplete, not that the host is unusable.
230
- const skill = tryReadBundledSkill(import.meta.url, config.skillsDir)
231
- // Tools resolve their Python scripts under the skill directory, so they use
232
- // the same path even when the skill itself is missing — a call then fails
233
- // with a typed FAILED_PRECONDITION naming the exact script it needed.
234
- const skillDir = skill?.dir ?? resolveSkillDir(import.meta.url, config.skillsDir)
251
+ // Degraded rather than fatal, per skill: see tryReadBundledSkill.
252
+ const skills = new Map<string, BundledSkill>()
253
+ for (const skillId of KICAD_SKILL_IDS) {
254
+ const skill = tryReadBundledSkill(import.meta.url, skillId, config.skillsDir)
255
+ if (skill) skills.set(skillId, skill)
256
+ }
257
+
258
+ // The tools resolve their Python scripts under the script-owning skill's
259
+ // directory, so they use the same path even when that skill itself failed to
260
+ // load — a call then fails with a typed FAILED_PRECONDITION naming the exact
261
+ // script it needed.
262
+ const scriptSkillDir =
263
+ skills.get(KICAD_SCRIPT_SKILL_ID)?.dir ??
264
+ resolveSkillDir(import.meta.url, KICAD_SCRIPT_SKILL_ID, config.skillsDir)
235
265
 
236
266
  log.info('applying dsh-kicad node half', {
237
267
  hasConfigHost: hasHostConfig(config),
238
268
  pythonPath: resolved.pythonPath,
239
- skillDir,
240
- skillPresent: skill !== null,
269
+ skillsRoot: skillsRoot(import.meta.url, config.skillsDir),
270
+ skillsRegistered: [...skills.keys()],
271
+ skillsMissing: KICAD_SKILL_IDS.filter((id) => !skills.has(id)),
272
+ scriptSkillDir,
241
273
  timeoutMs: resolved.timeoutMs,
242
274
  })
243
275
 
244
276
  const disposers: Array<() => void> = []
245
277
 
246
- // ── Skill (bundled, no separate installation) ────────────────────────────
247
- if (skill) {
278
+ // ── Skills (bundled, no separate installation) ───────────────────────────
279
+ for (const skill of skills.values()) {
248
280
  disposers.push(
249
281
  ctx.skills.register({
250
282
  name: skill.name,
@@ -258,7 +290,7 @@ export function apply(ctx: Context, config: KicadConfigInput = {}): () => void {
258
290
 
259
291
  // ── Tools ────────────────────────────────────────────────────────────────
260
292
  const tools = createKicadTools({
261
- scriptsDir: scriptsDir(skillDir),
293
+ scriptsDir: scriptsDir(scriptSkillDir),
262
294
  pythonPath: resolved.pythonPath,
263
295
  config: resolved,
264
296
  })
@@ -267,8 +299,8 @@ export function apply(ctx: Context, config: KicadConfigInput = {}): () => void {
267
299
  }
268
300
 
269
301
  log.info('dsh-kicad node half ready', {
270
- skill: skill?.name ?? null,
271
- degraded: skill === null,
302
+ skills: [...skills.keys()],
303
+ degraded: skills.size < KICAD_SKILL_IDS.length,
272
304
  tools: tools.length,
273
305
  expectedTools: kicadToolNames().length,
274
306
  })
package/src/paths.ts CHANGED
@@ -1,17 +1,23 @@
1
1
  /**
2
- * Resolution of the bundled KiCad skill directory at runtime.
2
+ * Resolution of the bundled skill directories at runtime.
3
3
  *
4
- * The skill ships inside the installed package (`<pkg>/skills/kicad-ipc/`), not
5
- * beside the source tree, so it is located relative to the loaded module. That
6
- * makes one resolver correct for every install shape:
4
+ * Every bundled skill ships inside the installed package
5
+ * (`<pkg>/skills/<skill-id>/`), not beside the source tree, so it is located
6
+ * relative to the loaded module. That makes one resolver correct for every
7
+ * install shape:
7
8
  *
8
- * - built artifact: `<pkg>/lib/index.mjs` -> `<pkg>/skills/kicad-ipc`
9
- * - source (vitest): `<pkg>/src/index.ts` -> `<pkg>/skills/kicad-ipc`
9
+ * - built artifact: `<pkg>/lib/index.mjs` -> `<pkg>/skills/<skill-id>`
10
+ * - source (vitest): `<pkg>/src/index.ts` -> `<pkg>/skills/<skill-id>`
10
11
  * - npm / git install: identical to the built artifact case
11
12
  *
12
13
  * `skills` must stay in `package.json` `files[]` or npm strips it and this
13
14
  * resolver fails loudly — which is the intended signal, because a `dsh-kicad`
14
- * without its skill is a broken delivery boundary.
15
+ * without its skills is a broken delivery boundary.
16
+ *
17
+ * The override (`skillsDir` config / `DSH_KICAD_SKILLS_DIR`) is the **skills
18
+ * root**, i.e. the directory that *contains* the skill directories — not one
19
+ * skill's directory. One override therefore relocates the whole set, and the
20
+ * per-skill layout below it is unchanged.
15
21
  *
16
22
  * @module
17
23
  */
@@ -19,19 +25,17 @@ import { existsSync } from 'node:fs'
19
25
  import { dirname, join, resolve } from 'node:path'
20
26
  import { fileURLToPath } from 'node:url'
21
27
 
22
- import { KICAD_SKILL_NAME } from './scripts.js'
23
-
24
28
  /**
25
- * Absolute path of the bundled `kicad-ipc` skill directory.
29
+ * Absolute path of the directory holding every bundled skill.
26
30
  *
27
31
  * Resolution order: an explicit override (config / `DSH_KICAD_SKILLS_DIR`),
28
32
  * then the package-relative location.
29
33
  *
30
34
  * @param moduleUrl - `import.meta.url` of the calling module.
31
- * @param override - explicit directory (plugin config or env var).
35
+ * @param override - explicit skills root (plugin config or env var).
32
36
  * @returns the resolved directory, whether or not it exists yet.
33
37
  */
34
- export function resolveSkillDir(moduleUrl: string, override?: string): string {
38
+ export function skillsRoot(moduleUrl: string, override?: string): string {
35
39
  if (override && override.trim().length > 0) return resolve(override)
36
40
  const envOverride = process.env['DSH_KICAD_SKILLS_DIR']
37
41
  if (envOverride && envOverride.trim().length > 0) return resolve(envOverride)
@@ -44,17 +48,29 @@ export function resolveSkillDir(moduleUrl: string, override?: string): string {
44
48
  const here = dirname(fileURLToPath(moduleUrl))
45
49
  // One level up is right for both `lib/` (built) and `src/` (vitest) because
46
50
  // both sit directly under the package root next to `skills/`.
47
- return resolve(here, '..', 'skills', KICAD_SKILL_NAME)
51
+ return resolve(here, '..', 'skills')
52
+ }
53
+
54
+ /**
55
+ * Absolute path of one bundled skill directory.
56
+ *
57
+ * @param moduleUrl - `import.meta.url` of the calling module.
58
+ * @param skillId - bundled skill id; MUST equal the directory name.
59
+ * @param override - explicit skills root (plugin config or env var).
60
+ * @returns the resolved directory, whether or not it exists yet.
61
+ */
62
+ export function resolveSkillDir(moduleUrl: string, skillId: string, override?: string): string {
63
+ return join(skillsRoot(moduleUrl, override), skillId)
48
64
  }
49
65
 
50
66
  /**
51
- * Resolve the skill directory and assert the skill is actually present.
67
+ * Resolve one skill directory and assert the skill is actually present.
52
68
  *
53
69
  * @throws when `SKILL.md` is missing — this is a packaging failure, not a
54
70
  * runtime condition, so it must be loud rather than silently degraded.
55
71
  */
56
- export function requireSkillDir(moduleUrl: string, override?: string): string {
57
- const dir = resolveSkillDir(moduleUrl, override)
72
+ export function requireSkillDir(moduleUrl: string, skillId: string, override?: string): string {
73
+ const dir = resolveSkillDir(moduleUrl, skillId, override)
58
74
  const skillFile = join(dir, 'SKILL.md')
59
75
  if (!existsSync(skillFile)) {
60
76
  throw new Error(
@@ -66,7 +82,7 @@ export function requireSkillDir(moduleUrl: string, override?: string): string {
66
82
  return dir
67
83
  }
68
84
 
69
- /** Absolute path of the bundled Python script directory. */
85
+ /** Absolute path of the bundled Python script directory inside a skill. */
70
86
  export function scriptsDir(skillDir: string): string {
71
87
  return join(skillDir, 'scripts')
72
88
  }
package/src/scripts.ts CHANGED
@@ -2,9 +2,10 @@
2
2
  * Registry of the KiCad IPC script templates bundled with this package.
3
3
  *
4
4
  * The scripts are the migrated `kicad-agent` executable surface. They live
5
- * under `skills/kicad-ipc/scripts/` and are shipped verbatim — this module only
6
- * describes them so the DSH tools and the skill stay in sync. Nothing here
7
- * reimplements KiCad IPC: the scripts own it (see `./ipc.ts`).
5
+ * under `skills/<KICAD_SCRIPT_SKILL_ID>/scripts/` — the `kicad-ipc` skill (see
6
+ * `./skills.ts`, which owns the skill ids) — and are shipped verbatim: this
7
+ * module only describes them so the DSH tools and the skill stay in sync.
8
+ * Nothing here reimplements KiCad IPC: the scripts own it (see `./ipc.ts`).
8
9
  *
9
10
  * @module
10
11
  */
@@ -126,6 +127,3 @@ export function kicadScript(id: string): KicadScript {
126
127
  }
127
128
  return script
128
129
  }
129
-
130
- /** The canonical skill directory name shipped by this package. */
131
- export const KICAD_SKILL_NAME = 'kicad-ipc'
package/src/skills.ts ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Registry of the skills bundled with this package.
3
+ *
4
+ * The skills under `skills/` are the migrated `kicad-agent` knowledge, shipped
5
+ * verbatim (see `README.md` and `docs/dsh-kicad-migration.md`) — this module
6
+ * only *describes* them, so `apply()` can register each one with
7
+ * `ctx.skills.register()` without hardcoding paths or names.
8
+ *
9
+ * The registry is the single source of truth for three things that must not
10
+ * drift apart:
11
+ *
12
+ * - the DSH skill id, which MUST equal the directory name under `skills/`
13
+ * (the bundling tests assert this, because a mismatch silently produces a
14
+ * skill whose `resourceBase` points somewhere else);
15
+ * - the fallback catalogue description, used only when SKILL.md frontmatter
16
+ * has no parseable `description` (the shipped file always wins);
17
+ * - which skill owns the Python script templates the KiCad tools run.
18
+ *
19
+ * Adding a skill from `kicad-agent` means: copy the directory, add an entry
20
+ * here, and register it — nothing else in `src/` is skill-specific.
21
+ *
22
+ * @module
23
+ */
24
+
25
+ /** A skill this package bundles and registers at load time. */
26
+ export interface KicadBundledSkill {
27
+ /** DSH skill id — must match the directory name under `skills/`. */
28
+ readonly id: string
29
+ /**
30
+ * Agent-facing catalogue description, used ONLY when the bundled SKILL.md has
31
+ * no parseable `description` frontmatter. The shipped file is authoritative,
32
+ * so this cannot be the normal path — DSH ignores a skill without a
33
+ * description, so an empty value here would silently break discovery.
34
+ */
35
+ readonly summary: string
36
+ /**
37
+ * Whether this skill owns `skills/<id>/scripts/` — the runnable Python
38
+ * templates that back the DSH tools. Exactly one bundled skill does; the
39
+ * tools resolve their interpreter scripts through it (`./paths.ts`).
40
+ */
41
+ readonly ownsScripts: boolean
42
+ }
43
+
44
+ /**
45
+ * The skill that owns the script templates behind the KiCad tools.
46
+ *
47
+ * The tools are the *executable* half of this skill: `kicad_pcb_*` shells out
48
+ * to `skills/kicad-ipc/scripts/*.py`. Kept as a named constant because the
49
+ * tools must keep resolving correctly even when that particular skill fails to
50
+ * load (they then fail per-call with a typed `FAILED_PRECONDITION` instead of
51
+ * silently pointing at another skill's directory).
52
+ */
53
+ export const KICAD_SCRIPT_SKILL_ID = 'kicad-ipc'
54
+
55
+ /**
56
+ * Every skill bundled with `@huaqiu/dsh-kicad`, keyed by id.
57
+ *
58
+ * Mirrors `skills/` 1:1 — the bundling test asserts that every entry exists on
59
+ * disk with a valid SKILL.md, and that no directory is left unregistered.
60
+ */
61
+ export const KICAD_SKILLS: Readonly<Record<string, KicadBundledSkill>> = {
62
+ [KICAD_SCRIPT_SKILL_ID]: {
63
+ id: KICAD_SCRIPT_SKILL_ID,
64
+ summary:
65
+ 'Operate a KiCad PCB through the official KiCad IPC API: inspect the live ' +
66
+ 'board and create, modify or delete objects. Use for PCB automation and ' +
67
+ 'autorouter-result import — not for editing .kicad_pcb files directly.',
68
+ ownsScripts: true,
69
+ },
70
+ 'hardware-design-brief': {
71
+ id: 'hardware-design-brief',
72
+ summary:
73
+ 'Turn a market need into verifiable hardware requirements: clarify them ' +
74
+ 'through staged questioning and write the market MRD, the hardware PRD ' +
75
+ 'and the schematic/PCB design inputs. Use when defining requirements for ' +
76
+ 'a new board project.',
77
+ ownsScripts: false,
78
+ },
79
+ }
80
+
81
+ /** The bundled skill ids, in registration order. */
82
+ export const KICAD_SKILL_IDS: readonly string[] = Object.keys(KICAD_SKILLS)
83
+
84
+ /**
85
+ * Look up one bundled skill by id.
86
+ * @throws when the id is not part of the bundled set.
87
+ */
88
+ export function kicadSkill(id: string): KicadBundledSkill {
89
+ const skill = KICAD_SKILLS[id]
90
+ if (!skill) {
91
+ throw new Error(`@huaqiu/dsh-kicad: unknown bundled skill "${id}"`)
92
+ }
93
+ return skill
94
+ }