@huaqiu/dsh-eda-host 0.3.24 → 0.4.1
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/README.md +61 -6
- package/lib/index.d.mts +231 -5
- package/lib/index.mjs +287 -46
- package/package.json +2 -2
- package/src/client.ts +256 -32
- package/src/config.ts +56 -0
- package/src/index.ts +68 -7
- package/src/tools.ts +152 -27
- package/src/types.ts +224 -0
package/README.md
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
# @huaqiu/dsh-eda-host
|
|
2
2
|
|
|
3
|
-
DSH plugin exposing the current EDA host
|
|
4
|
-
|
|
3
|
+
DSH plugin exposing the current EDA host to agents through hq-edge: its
|
|
4
|
+
schematic netlist, and — without reimplementing anything — the environment and
|
|
5
|
+
capability facts the host already knows.
|
|
5
6
|
|
|
6
7
|
## What it provides
|
|
7
8
|
|
|
9
|
+
Netlist:
|
|
10
|
+
|
|
8
11
|
| Tool | Scope |
|
|
9
12
|
|---|---|
|
|
10
13
|
| `get_project_netlist` | complete logical netlist of the current project |
|
|
11
14
|
| `get_selection_netlist` | netlist of the currently selected components |
|
|
12
15
|
| `get_active_page_netlist` | netlist of the active schematic page |
|
|
13
16
|
|
|
17
|
+
EDA host discovery:
|
|
18
|
+
|
|
19
|
+
| Tool | Returns |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `get_eda_host_info` | host identity, version, installation paths, `kicad-cli` path |
|
|
22
|
+
| `get_eda_host_capabilities` | capabilities the *current* host can actually provide |
|
|
23
|
+
|
|
24
|
+
Discovery is not implementation: a capability is advertised only when the host
|
|
25
|
+
can already provide it, and never because the EDA application is generally
|
|
26
|
+
known for it.
|
|
27
|
+
|
|
14
28
|
Each tool returns lossless JSON:
|
|
15
29
|
|
|
16
30
|
```json
|
|
@@ -42,7 +56,12 @@ NOT an error. On failure the tool returns `ok: false` with a semantic
|
|
|
42
56
|
- `UNIMPLEMENTED` — the host does not support this scope (e.g. active page on
|
|
43
57
|
KiCad). Do not retry.
|
|
44
58
|
- `UNAVAILABLE` — hq-edge host unreachable.
|
|
45
|
-
- `
|
|
59
|
+
- `DEADLINE_EXCEEDED` — the host did not answer within the request budget.
|
|
60
|
+
- `INTERNAL` — host-side failure or a malformed response.
|
|
61
|
+
|
|
62
|
+
`ok:true` with empty `components`/`nets` means exactly one thing: a genuinely
|
|
63
|
+
empty design. It never means "the parse failed" or "the host is unavailable" —
|
|
64
|
+
those are always `ok:false` with a `kind`.
|
|
46
65
|
|
|
47
66
|
## Architecture
|
|
48
67
|
|
|
@@ -58,9 +77,45 @@ EDA Host (hq.ir.schematic.v1.NetListService — KiCad / HQ EDA)
|
|
|
58
77
|
native EDA model
|
|
59
78
|
```
|
|
60
79
|
|
|
61
|
-
- No `@hqedge/*` dependency: the base URL
|
|
62
|
-
(
|
|
63
|
-
|
|
80
|
+
- No `@hqedge/*` dependency: the base URL is read from `ctx.hqEdge.baseUrl`
|
|
81
|
+
(provided by the edge-bridge), falling back to overlay config
|
|
82
|
+
(`hqEdgeBaseUrl`) or `HQ_EDGE_BASE_URL`.
|
|
64
83
|
- No KiCad code, no schematic parsing, no host IPC in this package.
|
|
65
84
|
- Errors are propagated with semantic kinds — never converted into fake empty
|
|
66
85
|
netlists.
|
|
86
|
+
|
|
87
|
+
## EDA host discovery
|
|
88
|
+
|
|
89
|
+
Host info crosses the same boundary as the netlist, using the EDA-independent
|
|
90
|
+
`hq.host.v1` contract — not KiCad-shaped messages:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
get_eda_host_info ─┐
|
|
94
|
+
├─→ hq-edge /api/v1/host/* ─→ hq.host.v1.EdaHostInfoService ─→ host
|
|
95
|
+
get_eda_host_capabilities ─┘
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The host adapts its native facts (version, install path, `kicad-cli` location,
|
|
99
|
+
which editors are open) into that contract. hq-edge performs no host-specific
|
|
100
|
+
translation, so KiCad and HQ EDA implement the same service.
|
|
101
|
+
|
|
102
|
+
**`hq.host.v1` has no availability fields.** A host that answers is available,
|
|
103
|
+
and every executable it lists is one it can run — so unavailability is never a
|
|
104
|
+
flag to check, it is a failed request (`ok:false` with `UNAVAILABLE` /
|
|
105
|
+
`FAILED_PRECONDITION` / `DEADLINE_EXCEEDED`). Because proto3 omits default
|
|
106
|
+
values, a host reporting nothing arrives as `{}`; the client fills the proto3
|
|
107
|
+
defaults so a missing field can never be misread as "unavailable". An empty
|
|
108
|
+
executable `path` means the host did not resolve an absolute location, not that
|
|
109
|
+
the tool is missing.
|
|
110
|
+
|
|
111
|
+
## Request budget
|
|
112
|
+
|
|
113
|
+
Every request has a single bounded budget (`requestTimeoutMs`, default 30 s),
|
|
114
|
+
enforced here and mirrored by the KiCad UI-dispatch bound. A request that
|
|
115
|
+
exceeds it fails with `DEADLINE_EXCEEDED`; it never returns an empty result.
|
|
116
|
+
|
|
117
|
+
## Dependency boundary
|
|
118
|
+
|
|
119
|
+
This plugin **requires hq-edge**. It is not a standalone DSH plugin: `hqEdge` is
|
|
120
|
+
a required inject, and `apply()` throws if the bridge did not provide a usable
|
|
121
|
+
endpoint. See `docs/tasks/expose-capability.md` in hq-edge.
|
package/lib/index.d.mts
CHANGED
|
@@ -16,6 +16,19 @@ interface EdaHostConfig {
|
|
|
16
16
|
hqEdgeBaseUrl?: string;
|
|
17
17
|
/** Path prefix on the host; default "/api/v1/netlist". */
|
|
18
18
|
netlistPathPrefix?: string;
|
|
19
|
+
/** Path prefix for PCB selection; default "/api/v1/pcb-selection". */
|
|
20
|
+
pcbSelectionPathPrefix?: string;
|
|
21
|
+
/** Path prefix for host discovery; default "/api/v1/host". */
|
|
22
|
+
hostPathPrefix?: string;
|
|
23
|
+
/**
|
|
24
|
+
* End-to-end budget for one EDA-host request, in milliseconds.
|
|
25
|
+
*
|
|
26
|
+
* This is the SINGLE request budget for the whole chain — the plugin does
|
|
27
|
+
* not define separate per-layer timeouts. It is enforced here (outermost)
|
|
28
|
+
* and mirrored by the KiCad UI-dispatch bound, so no agent request can wait
|
|
29
|
+
* indefinitely. See docs/tasks/expose-capability.md §6.
|
|
30
|
+
*/
|
|
31
|
+
requestTimeoutMs?: number;
|
|
19
32
|
}
|
|
20
33
|
//#endregion
|
|
21
34
|
//#region src/types.d.ts
|
|
@@ -57,6 +70,123 @@ interface SchematicNetlist {
|
|
|
57
70
|
components: SchematicComponent[];
|
|
58
71
|
nets: ElectricalNet[];
|
|
59
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Semantic PCB selection types.
|
|
75
|
+
*
|
|
76
|
+
* Self-contained structural mirror of `hq.pcb.v1` (the hq-edge-owned semantic
|
|
77
|
+
* protobuf contract for `PcbSelectionService.GetSelection`). Units follow the
|
|
78
|
+
* hq.pcb.v1 convention: positions/sizes/lengths in mm, rotations in degrees,
|
|
79
|
+
* `id` is the KiCad native object identity.
|
|
80
|
+
*/
|
|
81
|
+
interface PcbPoint {
|
|
82
|
+
x: number;
|
|
83
|
+
y: number;
|
|
84
|
+
}
|
|
85
|
+
interface PcbNetRef {
|
|
86
|
+
name: string;
|
|
87
|
+
code: number;
|
|
88
|
+
}
|
|
89
|
+
interface PcbPad {
|
|
90
|
+
id: string;
|
|
91
|
+
pin: string;
|
|
92
|
+
type: string;
|
|
93
|
+
shape: string;
|
|
94
|
+
position?: PcbPoint;
|
|
95
|
+
widthMm: number;
|
|
96
|
+
heightMm: number;
|
|
97
|
+
rotationDeg: number;
|
|
98
|
+
layer: string;
|
|
99
|
+
net?: PcbNetRef;
|
|
100
|
+
}
|
|
101
|
+
interface PcbFootprint {
|
|
102
|
+
id: string;
|
|
103
|
+
reference: string;
|
|
104
|
+
value: string;
|
|
105
|
+
footprint: string;
|
|
106
|
+
position?: PcbPoint;
|
|
107
|
+
rotationDeg: number;
|
|
108
|
+
pads: PcbPad[];
|
|
109
|
+
}
|
|
110
|
+
interface PcbTrack {
|
|
111
|
+
id: string;
|
|
112
|
+
layer: string;
|
|
113
|
+
start?: PcbPoint;
|
|
114
|
+
end?: PcbPoint;
|
|
115
|
+
widthMm: number;
|
|
116
|
+
lengthMm: number;
|
|
117
|
+
net?: PcbNetRef;
|
|
118
|
+
}
|
|
119
|
+
interface PcbArc {
|
|
120
|
+
id: string;
|
|
121
|
+
layer: string;
|
|
122
|
+
start?: PcbPoint;
|
|
123
|
+
end?: PcbPoint;
|
|
124
|
+
mid?: PcbPoint;
|
|
125
|
+
net?: PcbNetRef;
|
|
126
|
+
}
|
|
127
|
+
interface PcbVia {
|
|
128
|
+
id: string;
|
|
129
|
+
layers: string[];
|
|
130
|
+
drillMm: number;
|
|
131
|
+
viaType: string;
|
|
132
|
+
start?: PcbPoint;
|
|
133
|
+
end?: PcbPoint;
|
|
134
|
+
}
|
|
135
|
+
interface PcbSegment {
|
|
136
|
+
start?: PcbPoint;
|
|
137
|
+
end?: PcbPoint;
|
|
138
|
+
}
|
|
139
|
+
interface PcbZone {
|
|
140
|
+
id: string;
|
|
141
|
+
layer: string;
|
|
142
|
+
net?: PcbNetRef;
|
|
143
|
+
outline: PcbSegment[];
|
|
144
|
+
}
|
|
145
|
+
interface PcbShape {
|
|
146
|
+
id: string;
|
|
147
|
+
layer: string;
|
|
148
|
+
shapeType: string;
|
|
149
|
+
start?: PcbPoint;
|
|
150
|
+
end?: PcbPoint;
|
|
151
|
+
center?: PcbPoint;
|
|
152
|
+
radiusMm: number;
|
|
153
|
+
mid?: PcbPoint;
|
|
154
|
+
widthMm: number;
|
|
155
|
+
}
|
|
156
|
+
interface PcbText {
|
|
157
|
+
id: string;
|
|
158
|
+
layer: string;
|
|
159
|
+
text: string;
|
|
160
|
+
position?: PcbPoint;
|
|
161
|
+
rotationDeg: number;
|
|
162
|
+
hJustify: string;
|
|
163
|
+
vJustify: string;
|
|
164
|
+
}
|
|
165
|
+
interface PcbDimension {
|
|
166
|
+
id: string;
|
|
167
|
+
layer: string;
|
|
168
|
+
start?: PcbPoint;
|
|
169
|
+
end?: PcbPoint;
|
|
170
|
+
value: string;
|
|
171
|
+
dimType: string;
|
|
172
|
+
}
|
|
173
|
+
interface PcbGroup {
|
|
174
|
+
id: string;
|
|
175
|
+
itemIds: string[];
|
|
176
|
+
}
|
|
177
|
+
interface PcbSelection {
|
|
178
|
+
footprints: PcbFootprint[];
|
|
179
|
+
pads: PcbPad[];
|
|
180
|
+
tracks: PcbTrack[];
|
|
181
|
+
arcs: PcbArc[];
|
|
182
|
+
vias: PcbVia[];
|
|
183
|
+
zones: PcbZone[];
|
|
184
|
+
shapes: PcbShape[];
|
|
185
|
+
texts: PcbText[];
|
|
186
|
+
dimensions: PcbDimension[];
|
|
187
|
+
groups: PcbGroup[];
|
|
188
|
+
nets: PcbNetRef[];
|
|
189
|
+
}
|
|
60
190
|
/**
|
|
61
191
|
* Semantic error categories for netlist retrieval. Mirrors the gRPC status
|
|
62
192
|
* contract of `hq.ir.schematic.v1.NetListService` so an agent can distinguish
|
|
@@ -70,21 +200,117 @@ type NetlistErrorKind =
|
|
|
70
200
|
/** Host-side runtime failure. */
|
|
71
201
|
'INTERNAL' |
|
|
72
202
|
/** Host unavailable (connection refused). */
|
|
73
|
-
'UNAVAILABLE'
|
|
203
|
+
'UNAVAILABLE' |
|
|
204
|
+
/** The host did not answer within the request budget. */
|
|
205
|
+
'DEADLINE_EXCEEDED';
|
|
74
206
|
declare class NetlistError extends Error {
|
|
75
207
|
readonly kind: NetlistErrorKind;
|
|
76
208
|
constructor(kind: NetlistErrorKind, message: string);
|
|
77
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* `hq.host.v1` carries **no availability flags**.
|
|
212
|
+
*
|
|
213
|
+
* A host that answers `GetEdaHostInfo` is, by definition, available, and every
|
|
214
|
+
* executable it lists is one it can actually run. Unavailability is therefore
|
|
215
|
+
* never a field to check — it is a failed request, surfaced as `ok:false` with
|
|
216
|
+
* `error.kind` `UNAVAILABLE` / `FAILED_PRECONDITION` / `DEADLINE_EXCEEDED`.
|
|
217
|
+
*
|
|
218
|
+
* Consequence for consumers: never infer "unavailable" from a missing or empty
|
|
219
|
+
* value. Absence of `identity`/`installation` just means proto3 omitted
|
|
220
|
+
* defaults (see `parseEdaHostInfo`), and empty `path` only means the host could
|
|
221
|
+
* not resolve an absolute location — the tool is still runnable by name.
|
|
222
|
+
*/
|
|
223
|
+
/**
|
|
224
|
+
* Which EDA application sits behind the semantic host boundary.
|
|
225
|
+
*
|
|
226
|
+
* Deliberately EDA-independent: a new host adds a value here rather than
|
|
227
|
+
* introducing host-specific messages or tools.
|
|
228
|
+
*/
|
|
229
|
+
type EdaHostType = 'EDA_HOST_TYPE_UNSPECIFIED' | 'EDA_HOST_TYPE_KICAD' | 'EDA_HOST_TYPE_HQ_EDA';
|
|
230
|
+
/**
|
|
231
|
+
* A capability an EDA host may provide.
|
|
232
|
+
*
|
|
233
|
+
* A capability is advertised only when the host can actually provide it —
|
|
234
|
+
* discovering a capability is not the same as implementing it. Unknown values
|
|
235
|
+
* MUST be treated as "not supported" so a newer host cannot confuse an older
|
|
236
|
+
* plugin.
|
|
237
|
+
*/
|
|
238
|
+
type EdaHostCapability = 'EDA_HOST_CAPABILITY_UNSPECIFIED' | 'EDA_HOST_CAPABILITY_SCHEMATIC' | 'EDA_HOST_CAPABILITY_PCB' | 'EDA_HOST_CAPABILITY_NETLIST' | 'EDA_HOST_CAPABILITY_NETLIST_SELECTION' | 'EDA_HOST_CAPABILITY_NETLIST_ACTIVE_PAGE' | 'EDA_HOST_CAPABILITY_ERC' | 'EDA_HOST_CAPABILITY_DRC' | 'EDA_HOST_CAPABILITY_BOM' | 'EDA_HOST_CAPABILITY_PLACEMENT';
|
|
239
|
+
/** A host-provided command line tool. */
|
|
240
|
+
interface EdaHostExecutable {
|
|
241
|
+
/** Stable tool name, e.g. "kicad-cli". */
|
|
242
|
+
name: string;
|
|
243
|
+
/**
|
|
244
|
+
* Absolute path when the host could resolve one.
|
|
245
|
+
*
|
|
246
|
+
* Empty means "resolvable by name only" (e.g. found on `PATH` but the host
|
|
247
|
+
* did not report a location) — never "not installed" or "unusable".
|
|
248
|
+
*/
|
|
249
|
+
path: string;
|
|
250
|
+
}
|
|
251
|
+
/** Where the host application lives on disk. */
|
|
252
|
+
interface EdaHostInstallation {
|
|
253
|
+
applicationPath: string;
|
|
254
|
+
/**
|
|
255
|
+
* Executables the host can run. Being listed here IS the availability
|
|
256
|
+
* signal: there is no per-executable availability flag.
|
|
257
|
+
*/
|
|
258
|
+
executables: EdaHostExecutable[];
|
|
259
|
+
}
|
|
260
|
+
/** Which EDA host is connected. */
|
|
261
|
+
interface EdaHostIdentity {
|
|
262
|
+
hostType: EdaHostType;
|
|
263
|
+
hostName: string;
|
|
264
|
+
version: string;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* EDA-independent description of the connected host.
|
|
268
|
+
*
|
|
269
|
+
* Answering this message is the host's way of saying it is available — there is
|
|
270
|
+
* no `available` field to inspect.
|
|
271
|
+
*/
|
|
272
|
+
interface EdaHostInfo {
|
|
273
|
+
identity: EdaHostIdentity;
|
|
274
|
+
installation: EdaHostInstallation;
|
|
275
|
+
}
|
|
78
276
|
//#endregion
|
|
79
277
|
//#region src/client.d.ts
|
|
278
|
+
/** Per-call options. `signal` lets DSH cancel a request (see §6 of the task). */
|
|
279
|
+
interface EdaHostRequestOptions {
|
|
280
|
+
signal?: AbortSignal;
|
|
281
|
+
}
|
|
80
282
|
interface EdaHostClient {
|
|
81
283
|
/** Netlist of the currently selected components. */
|
|
82
|
-
getSelectionNetlist(): Promise<SchematicNetlist>;
|
|
284
|
+
getSelectionNetlist(options?: EdaHostRequestOptions): Promise<SchematicNetlist>;
|
|
83
285
|
/** Complete logical netlist for the current project. */
|
|
84
|
-
getProjectNetlist(): Promise<SchematicNetlist>;
|
|
286
|
+
getProjectNetlist(options?: EdaHostRequestOptions): Promise<SchematicNetlist>;
|
|
85
287
|
/** Netlist of the current active schematic page. */
|
|
86
|
-
getActivePageNetlist(): Promise<SchematicNetlist>;
|
|
288
|
+
getActivePageNetlist(options?: EdaHostRequestOptions): Promise<SchematicNetlist>;
|
|
289
|
+
/**
|
|
290
|
+
* EDA-independent identity / installation of the host. Presence of a result
|
|
291
|
+
* is the availability signal — `hq.host.v1` has no availability field.
|
|
292
|
+
*/
|
|
293
|
+
getEdaHostInfo(options?: EdaHostRequestOptions): Promise<EdaHostInfo>;
|
|
294
|
+
/** Capabilities the host currently provides. */
|
|
295
|
+
getEdaHostCapabilities(options?: EdaHostRequestOptions): Promise<EdaHostCapability[]>;
|
|
296
|
+
/**
|
|
297
|
+
* Semantic PCB selection of the current PCB editor (hq.pcb.v1
|
|
298
|
+
* PcbSelectionService.GetSelection bridged through hq-edge). An empty
|
|
299
|
+
* selection resolves to an all-empty `PcbSelection` — never an error.
|
|
300
|
+
*/
|
|
301
|
+
getPcbSelection(options?: EdaHostRequestOptions): Promise<PcbSelection>;
|
|
87
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* Extract the semantic `SchematicNetlist` from an hq-edge netlist body.
|
|
305
|
+
*
|
|
306
|
+
* hq-edge now emits a single-level body: `{ netlist: { components, nets } }`.
|
|
307
|
+
* Older hq-edge builds serialized the protobuf envelope
|
|
308
|
+
* (`GetNetListResponse.oneof result`), which produced a second `netlist` level
|
|
309
|
+
* and made every populated design look empty. That legacy shape is unwrapped
|
|
310
|
+
* EXPLICITLY — not silently — and anything else is a hard error, because
|
|
311
|
+
* "malformed" must never masquerade as "empty design".
|
|
312
|
+
*/
|
|
313
|
+
declare function parseNetlistBody(body: unknown): SchematicNetlist;
|
|
88
314
|
//#endregion
|
|
89
315
|
//#region src/index.d.ts
|
|
90
316
|
/** Plugin id — matches package.json. */
|
|
@@ -147,4 +373,4 @@ declare module '@deepseek-ai/cordis' {
|
|
|
147
373
|
*/
|
|
148
374
|
declare function apply(ctx: Context, config?: Partial<EdaHostConfig>): () => void;
|
|
149
375
|
//#endregion
|
|
150
|
-
export { type EdaHostClient, type EdaHostConfig, type ElectricalNet, type ElectricalType, NetlistError, type NetlistErrorKind, type PinDefinition, type PinReference, type SchematicComponent, type SchematicNetlist, apply, inject, name };
|
|
376
|
+
export { type EdaHostCapability, type EdaHostClient, type EdaHostConfig, type EdaHostExecutable, type EdaHostIdentity, type EdaHostInfo, type EdaHostInstallation, type EdaHostRequestOptions, type EdaHostType, type ElectricalNet, type ElectricalType, NetlistError, type NetlistErrorKind, type PcbArc, type PcbDimension, type PcbFootprint, type PcbGroup, type PcbNetRef, type PcbPad, type PcbPoint, type PcbSegment, type PcbSelection, type PcbShape, type PcbText, type PcbTrack, type PcbVia, type PcbZone, type PinDefinition, type PinReference, type SchematicComponent, type SchematicNetlist, apply, inject, name, parseNetlistBody };
|