@huaqiu/dsh-kicad 0.4.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 深圳华秋智联股份有限公司
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # @huaqiu/dsh-kicad
2
+
3
+ KiCad agent capabilities for DeepSeek Harness, shipped as **one** DSH plugin: the
4
+ executable KiCad tools *and* the `kicad-ipc` skill that teaches the agent how to use
5
+ them. Installing the Huaqiu DSH PCB/EDA bundle makes both available out of the box —
6
+ there is no separate skill installation step.
7
+
8
+ Migrated from the standalone `kicad-agent` repository.
9
+
10
+ ## What you get
11
+
12
+ ```
13
+ @huaqiu/dsh-kicad
14
+ ├── Tools 10 KiCad tools, registered with ctx.tools.register(defineTool(...))
15
+ └── Skills kicad-ipc — SKILL.md + references/ + 11 python script templates
16
+ ```
17
+
18
+ ### Tools
19
+
20
+ | Tool | Script | Effect |
21
+ |---|---|---|
22
+ | `kicad_ipc_diagnose` | `diagnose_ipc_connection.py` | read |
23
+ | `kicad_ipc_verify_live` | `verify_live_ipc.py` | probe (commit dropped) |
24
+ | `kicad_pcb_create_track` | `create_track.py` | mutate |
25
+ | `kicad_pcb_create_via` | `create_via.py` | mutate |
26
+ | `kicad_pcb_create_copper_zone` | `create_copper_zone.py` | mutate |
27
+ | `kicad_pcb_add_footprint_from_template` | `add_footprint_from_board_template.py` | mutate |
28
+ | `kicad_pcb_move_rotate_footprint` | `move_rotate_footprint.py` | mutate |
29
+ | `kicad_pcb_update_selected_track_width` | `update_selected_track_width.py` | mutate |
30
+ | `kicad_pcb_refill_zones` | `refill_zones.py` | mutate |
31
+ | `kicad_pcb_remove_selected_items` | `remove_selected_items.py` | mutate (needs `confirm`) |
32
+
33
+ Every tool returns the same envelope:
34
+
35
+ ```jsonc
36
+ { "ok": true, "script": "create_track", "effect": "mutate", "output": "…" }
37
+ { "ok": false, "script": "create_track", "effect": "mutate",
38
+ "error": { "kind": "FAILED_PRECONDITION", "message": "…" } }
39
+ ```
40
+
41
+ `error.kind` is one of `FAILED_PRECONDITION` (KiCad IPC cannot run at all — do not
42
+ retry), `UNAVAILABLE` (KiCad unreachable — retry once after checking the environment),
43
+ `DEADLINE_EXCEEDED` (timed out), `INVALID_ARGUMENT` (board untouched), `INTERNAL`
44
+ (script failed; the commit was dropped).
45
+
46
+ ### Skill
47
+
48
+ `skills/kicad-ipc/SKILL.md` is the agent's operating guidance: when to use KiCad, how to
49
+ inspect current state, how to verify mutations, and when to stop and ask. It is *not* an
50
+ IPC API specification — the tool schemas are the executable interface, the skill is the
51
+ reasoning around them.
52
+
53
+ Progressive disclosure: `references/ipc-pcb-workflows.md` holds version matrices, the
54
+ public CRUD table and failure handling; `scripts/` holds runnable templates. Both are
55
+ reachable by the agent because the skill is registered with a `resourceBase` pointing at
56
+ its directory.
57
+
58
+ ## How the plugin is wired
59
+
60
+ ```yaml
61
+ # cordis.patch.yml
62
+ - insert:
63
+ - id: huaqiu-dsh-kicad
64
+ name: '@huaqiu/dsh-kicad'
65
+ inject: ['skills', 'tools']
66
+ ```
67
+
68
+ `inject` must match `export const inject` in `src/index.ts`. `skills` is required —
69
+ that is the DSH runtime's own plugin-bundled skill channel (`ctx.skills.register`).
70
+ `apply()` registers the skill and the ten tools and returns a single disposer.
71
+
72
+ The skill is located relative to the **loaded module**, not the source tree, so one
73
+ resolver works for `src/` (tests), `lib/` (built) and an npm install:
74
+
75
+ ```
76
+ <package>/lib/index.mjs -> <package>/skills/kicad-ipc
77
+ ```
78
+
79
+ That is why `files[]` must contain `skills` — otherwise npm strips it and `apply()`
80
+ throws loudly instead of silently degrading.
81
+
82
+ ## Runtime requirements
83
+
84
+ KiCad IPC is not bundled; these are host requirements, the same ones `kicad-agent` had:
85
+
86
+ - KiCad **9.0+** with the KiCad API service enabled (Preferences → Plugins) and PCB
87
+ Editor restarted, with a `.kicad_pcb` open.
88
+ - A Python interpreter with the official `kicad-python` package (`kipy`) whose version
89
+ matches the running KiCad. Configure it with plugin config `pythonPath` or
90
+ `$DSH_KICAD_PYTHON` (default `python3`).
91
+ - DSH running with **Full Access** — a non-Full-Access sandbox cannot reach KiCad's
92
+ named pipe on Windows.
93
+
94
+ `KICAD_API_SOCKET` / `KICAD_API_TOKEN` are injected by KiCad and read by `kipy`. This
95
+ package never guesses, hardcodes or scans for them.
96
+
97
+ ### Configuration
98
+
99
+ | Setting | Env | Default |
100
+ |---|---|---|
101
+ | `pythonPath` | `DSH_KICAD_PYTHON` | `python3` |
102
+ | `skillsDir` | `DSH_KICAD_SKILLS_DIR` | bundled `skills/kicad-ipc` |
103
+ | `timeoutMs` | — | `30000` |
104
+ | `diagnosticTimeoutMs` | — | `15000` |
105
+ | `refillTimeoutMs` | — | `150000` |
106
+
107
+ ## Boundaries
108
+
109
+ - **No HQ Edge dependency.** Unlike `@huaqiu/dsh-eda-host`, this plugin talks to KiCad
110
+ directly: no runtime, executable, service, port, config or artifact dependency, and no
111
+ `@hqedge/*` import.
112
+ - **KiCad is the source of truth.** The adapter returns what KiCad said, verbatim. There
113
+ is no second authoritative representation of board state in this package.
114
+ - **The scripts own the protocol.** The adapter shells out to the migrated Python
115
+ templates rather than re-implementing KiCad IPC in TypeScript.
116
+
117
+ ## Layout
118
+
119
+ ```
120
+ packages/dsh-kicad/
121
+ src/index.ts # plugin entry: apply() registers skill + tools
122
+ src/tools.ts # 10 defineTool definitions
123
+ src/ipc.ts # KiCad IPC adapter (script runner + error classification)
124
+ src/scripts.ts # bundled script registry
125
+ src/paths.ts # runtime skill-directory resolution
126
+ src/config.ts # python interpreter / timeouts
127
+ skills/kicad-ipc/
128
+ SKILL.md
129
+ agents/openai.yaml
130
+ references/ipc-pcb-workflows.md
131
+ scripts/*.py
132
+ test/ # plugin loading, tool registration, skill discovery, bundling
133
+ ```
134
+
135
+ ## Tests
136
+
137
+ ```bash
138
+ pnpm --filter @huaqiu/dsh-kicad test
139
+ ```
140
+
141
+ 46 tests covering plugin loading, the disposer, skill discovery, tool registration, argv
142
+ construction, pre-flight validation, semantic error mapping and packaging. Execution is
143
+ exercised against a fake interpreter, so no KiCad or `kipy` is needed.
@@ -0,0 +1,15 @@
1
+ # DSH bundle patch: inserts the Huaqiu KiCad plugin into a profile's layer stack.
2
+ #
3
+ # This one row delivers BOTH halves of `dsh-kicad` (task: dsh-kicad-skill-plugin):
4
+ # the KiCad tools and the bundled `kicad-ipc` skill. The plugin registers the
5
+ # skill itself at load time via `ctx.skills.register()`, so there is no separate
6
+ # skill installation step and no second skill registration mechanism.
7
+ #
8
+ # The entry `inject` MUST match the node-half module contract (`src/index.ts`
9
+ # `export const inject`): `skills` + `tools`. `skills` is REQUIRED — without it
10
+ # `apply()`'s `ctx.skills` access throws `cannot get property "skills" without
11
+ # inject`.
12
+ - insert:
13
+ - id: huaqiu-dsh-kicad
14
+ name: '@huaqiu/dsh-kicad'
15
+ inject: ['skills', 'tools']
@@ -0,0 +1,284 @@
1
+ import { defineTool } from "@deepseek-ai/dsh-tools";
2
+ import { Context } from "@deepseek-ai/cordis";
3
+ //#region src/config.d.ts
4
+ /**
5
+ * Configuration for `@huaqiu/dsh-kicad`.
6
+ *
7
+ * Deliberately small. Everything KiCad-specific (socket, token, board state)
8
+ * belongs to KiCad and the bundled scripts — the scripts read
9
+ * `KICAD_API_SOCKET` / `KICAD_API_TOKEN` that KiCad injects, and this package
10
+ * never guesses them. The only host configuration here is *how to reach the
11
+ * Python environment that owns `kipy`*, which is a machine concern, not a
12
+ * design concern.
13
+ *
14
+ * @module
15
+ */
16
+ /** Configuration accepted by the plugin's `apply()`. */
17
+ interface KicadConfig {
18
+ /**
19
+ * Python interpreter used to run the bundled KiCad scripts. It must have the
20
+ * official `kicad-python` package (`kipy`) installed.
21
+ */
22
+ pythonPath: string;
23
+ /** Scripts directory override (defaults to the bundled skill's `scripts/`). */
24
+ skillsDir?: string;
25
+ /** Per-script timeout in milliseconds. */
26
+ timeoutMs: number;
27
+ /** Timeout for the two diagnostic scripts, which are fast but must be prompt. */
28
+ diagnosticTimeoutMs: number;
29
+ /** Timeout for `refill_zones`, which can legitimately block for ~2 minutes. */
30
+ refillTimeoutMs: number;
31
+ }
32
+ type KicadConfigInput = Partial<KicadConfig>;
33
+ //#endregion
34
+ //#region src/scripts.d.ts
35
+ /**
36
+ * Registry of the KiCad IPC script templates bundled with this package.
37
+ *
38
+ * The scripts are the migrated `kicad-agent` executable surface. They live
39
+ * under `skills/kicad-ipc/scripts/` and are shipped verbatim — this module only
40
+ * describes them so the DSH tools and the skill stay in sync. Nothing here
41
+ * reimplements KiCad IPC: the scripts own it (see `./ipc.ts`).
42
+ *
43
+ * @module
44
+ */
45
+ /** How one bundled script changes (or does not change) the KiCad board. */
46
+ type ScriptEffect =
47
+ /** Reads only. Safe to run at any time. */
48
+ 'read' |
49
+ /** Mutates the board inside a dropped commit — nothing is persisted. */
50
+ 'probe' |
51
+ /** Mutates the board and pushes the commit. Persists only with `save`. */
52
+ 'mutate';
53
+ interface KicadScript {
54
+ /** Stable id — the script's file stem. */
55
+ readonly id: string;
56
+ /** File name inside `skills/kicad-ipc/scripts/`. */
57
+ readonly file: string;
58
+ /** One-line, agent-facing summary of what the script does. */
59
+ readonly summary: string;
60
+ /** What the script does to the board. */
61
+ readonly effect: ScriptEffect;
62
+ /** Whether the script accepts the shared `--save` flag. */
63
+ readonly supportsSave: boolean;
64
+ }
65
+ /**
66
+ * Every bundled script, keyed by id.
67
+ *
68
+ * Mirrors `skills/kicad-ipc/scripts/` 1:1. Adding a script to the skill means
69
+ * adding it here (and exposing it in `./tools.ts`) — the bundling test asserts
70
+ * that all three stay consistent.
71
+ */
72
+ declare const KICAD_SCRIPTS: Readonly<Record<string, KicadScript>>;
73
+ /** The bundled script ids, in registration order. */
74
+ declare const KICAD_SCRIPT_IDS: readonly string[];
75
+ /**
76
+ * Look up one script by id.
77
+ * @throws when the id is not part of the bundled set.
78
+ */
79
+ declare function kicadScript(id: string): KicadScript;
80
+ /** The canonical skill directory name shipped by this package. */
81
+ declare const KICAD_SKILL_NAME = "kicad-ipc";
82
+ //#endregion
83
+ //#region src/ipc.d.ts
84
+ /**
85
+ * Semantic failure kinds, shared by every KiCad tool.
86
+ *
87
+ * Same vocabulary as `@huaqiu/dsh-eda-host` so the agent's error handling does
88
+ * not have to differ per EDA plugin.
89
+ */
90
+ type KicadErrorKind =
91
+ /** The environment cannot run KiCad IPC at all (no python, no kipy, no board). */
92
+ 'FAILED_PRECONDITION' |
93
+ /** KiCad is installed but unreachable right now — retryable. */
94
+ 'UNAVAILABLE' |
95
+ /** The script did not finish inside its timeout. */
96
+ 'DEADLINE_EXCEEDED' |
97
+ /** The arguments were rejected before KiCad was touched. */
98
+ 'INVALID_ARGUMENT' |
99
+ /** KiCad or the script failed after the connection was established. */
100
+ 'INTERNAL';
101
+ interface KicadError {
102
+ kind: KicadErrorKind;
103
+ message: string;
104
+ }
105
+ /** Raw result of one script invocation. */
106
+ interface ScriptRun {
107
+ /** Script identifier (file stem). */
108
+ script: string;
109
+ /** Process exit code; `null` when it never started or was killed. */
110
+ exitCode: number | null;
111
+ stdout: string;
112
+ stderr: string;
113
+ /** Exact argv handed to the interpreter, for auditability. */
114
+ argv: string[];
115
+ /** `true` when the process was killed because the timeout elapsed. */
116
+ timedOut: boolean;
117
+ }
118
+ interface RunScriptOptions {
119
+ /** Directory holding the bundled scripts. */
120
+ scriptsDir: string;
121
+ /** Python interpreter with `kipy` available. */
122
+ pythonPath: string;
123
+ /** Script to run. */
124
+ script: KicadScript;
125
+ /** CLI flags, already stringified (e.g. `['--net', 'GND']`). */
126
+ args?: readonly string[];
127
+ /** Timeout in milliseconds. */
128
+ timeoutMs: number;
129
+ /** Optional cancellation from the tool call. */
130
+ signal?: AbortSignal;
131
+ }
132
+ /**
133
+ * Run one bundled KiCad script.
134
+ *
135
+ * Never throws for script-level failures — the outcome is reported, so a
136
+ * missing `kipy` or a closed KiCad degrades into a typed error the agent can
137
+ * act on instead of crashing the plugin. It only throws when the script itself
138
+ * is not part of the package.
139
+ */
140
+ declare function runKicadScript(options: RunScriptOptions): Promise<ScriptRun>;
141
+ /**
142
+ * Translate a raw run into a semantic error, or `undefined` on success.
143
+ *
144
+ * Exit codes are part of the scripts' contract:
145
+ *
146
+ * - `diagnose_ipc_connection.py`: 0 ok, 1 unreachable, 2 `kipy` missing,
147
+ * 3 API version mismatch, 4 no board open.
148
+ * - every argparse script: 2 = rejected arguments (KiCad untouched).
149
+ * - anything else non-zero: a real KiCad/script failure.
150
+ */
151
+ declare function classifyRun(run: ScriptRun): KicadError | undefined;
152
+ /** Convenience: run a script and return its semantic outcome. */
153
+ declare function invokeKicadScript(options: RunScriptOptions): Promise<{
154
+ ok: boolean;
155
+ run: ScriptRun;
156
+ error?: KicadError;
157
+ }>;
158
+ //#endregion
159
+ //#region src/tools.d.ts
160
+ /** Everything a tool needs to reach the bundled scripts. */
161
+ interface KicadToolEnv {
162
+ /** Directory holding the bundled Python scripts. */
163
+ scriptsDir: string;
164
+ /** Python interpreter with `kipy`. */
165
+ pythonPath: string;
166
+ config: KicadConfig;
167
+ }
168
+ /**
169
+ * All KiCad tools contributed by this plugin.
170
+ */
171
+ declare function createKicadTools(env: KicadToolEnv): ReturnType<typeof defineTool>[];
172
+ /**
173
+ * Names of every tool this plugin registers — asserted by the tests and used
174
+ * for startup logging.
175
+ */
176
+ declare function kicadToolNames(): string[];
177
+ //#endregion
178
+ //#region src/paths.d.ts
179
+ /**
180
+ * Absolute path of the bundled `kicad-ipc` skill directory.
181
+ *
182
+ * Resolution order: an explicit override (config / `DSH_KICAD_SKILLS_DIR`),
183
+ * then the package-relative location.
184
+ *
185
+ * @param moduleUrl - `import.meta.url` of the calling module.
186
+ * @param override - explicit directory (plugin config or env var).
187
+ * @returns the resolved directory, whether or not it exists yet.
188
+ */
189
+ declare function resolveSkillDir(moduleUrl: string, override?: string): string;
190
+ /**
191
+ * Resolve the skill directory and assert the skill is actually present.
192
+ *
193
+ * @throws when `SKILL.md` is missing — this is a packaging failure, not a
194
+ * runtime condition, so it must be loud rather than silently degraded.
195
+ */
196
+ declare function requireSkillDir(moduleUrl: string, override?: string): string;
197
+ /** Absolute path of the bundled Python script directory. */
198
+ declare function scriptsDir(skillDir: string): string;
199
+ //#endregion
200
+ //#region src/index.d.ts
201
+ /** Plugin id — matches package.json. */
202
+ declare const name = "@huaqiu/dsh-kicad";
203
+ /**
204
+ * Cordis services this half depends on.
205
+ *
206
+ * `skills` is REQUIRED: it is the DSH runtime's skill registry, and registering
207
+ * the bundled `kicad-ipc` skill is this plugin's core job. Without the inject,
208
+ * `apply()`'s `ctx.skills` access would throw
209
+ * `cannot get property "skills" without inject`.
210
+ *
211
+ * `tools` is the DSH node runtime tool registry used for the KiCad tools.
212
+ *
213
+ * Note what is NOT here: `hqEdge`. Unlike `@huaqiu/dsh-eda-host`, this plugin
214
+ * talks to KiCad directly, so it must never depend on the edge bridge.
215
+ */
216
+ declare const inject: readonly ["skills", "tools"];
217
+ /**
218
+ * The `skills` service as this plugin uses it.
219
+ *
220
+ * `@deepseek-ai/dsh-tools` already augments `Context` with `tools`, but nothing
221
+ * in this workspace augments `skills` — `@deepseek-ai/dsh-skill` is a runtime
222
+ * dependency of the harness, not of this package (we never import it; the
223
+ * service arrives through Cordis injection). Declaring the shape we consume is
224
+ * the same technique `@huaqiu/dsh-eda-host` uses for `hqEdge`.
225
+ */
226
+ interface SkillRegistration {
227
+ /** Skill id — must match `/^[a-z0-9]+(?:-[a-z0-9]+)*$/` to be discoverable. */
228
+ name: string;
229
+ /** Catalog description shown to the model. */
230
+ description: string;
231
+ /** Full SKILL.md body. */
232
+ content: string;
233
+ /** Where `references/` and `scripts/` live for progressive disclosure. */
234
+ resourceBase: {
235
+ kind: 'directory';
236
+ path: string;
237
+ };
238
+ }
239
+ declare module '@deepseek-ai/cordis' {
240
+ interface Context {
241
+ /** DSH skill registry, provided by the `@deepseek-ai/dsh-skill` service. */
242
+ skills: {
243
+ /** Register a plugin-bundled skill; returns its unregister disposer. */
244
+ register(skill: SkillRegistration): () => void;
245
+ };
246
+ }
247
+ }
248
+ /**
249
+ * Extract the `description` field from SKILL.md YAML frontmatter.
250
+ *
251
+ * Only the single-line form is supported (quoted or bare), which is what the
252
+ * migrated skill uses. Returns `undefined` when absent so the caller can fall
253
+ * back rather than registering a skill with an empty description — DSH ignores
254
+ * frontmatter-less skills entirely, so an empty description would silently
255
+ * break discovery.
256
+ */
257
+ declare function skillDescription(markdown: string): string | undefined;
258
+ /**
259
+ * Read the bundled `kicad-ipc` SKILL.md.
260
+ *
261
+ * Exposed for tests and for callers that want the skill body without loading
262
+ * the plugin (e.g. a packaging check).
263
+ */
264
+ declare function readBundledSkill(moduleUrl: string, override?: string): {
265
+ dir: string;
266
+ name: string;
267
+ description: string;
268
+ content: string;
269
+ };
270
+ /**
271
+ * Host plugin body — register the `kicad-ipc` skill and the KiCad tools.
272
+ *
273
+ * Both halves are registered here so that one installation delivers both. The
274
+ * skill's `resourceBase` points at the bundled skill directory, which is how
275
+ * the agent reaches `references/ipc-pcb-workflows.md` and `scripts/` as
276
+ * progressive-disclosure resources.
277
+ *
278
+ * @param ctx - real cordis context (node side).
279
+ * @param config - plugin overlay config (python interpreter, timeouts).
280
+ * @returns disposer — unregisters the skill and the tools on plugin dispose.
281
+ */
282
+ declare function apply(ctx: Context, config?: KicadConfigInput): () => void;
283
+ //#endregion
284
+ export { KICAD_SCRIPTS, KICAD_SCRIPT_IDS, KICAD_SKILL_NAME, type KicadConfig, type KicadConfigInput, type KicadError, type KicadErrorKind, type KicadScript, type ScriptEffect, type ScriptRun, SkillRegistration, apply, classifyRun, createKicadTools, inject, invokeKicadScript, kicadScript, kicadToolNames, name, readBundledSkill, requireSkillDir, resolveSkillDir, runKicadScript, scriptsDir, skillDescription };