@huaqiu/dsh-eda-host 0.3.25 → 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/lib/index.d.mts CHANGED
@@ -16,6 +16,8 @@ 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;
19
21
  /** Path prefix for host discovery; default "/api/v1/host". */
20
22
  hostPathPrefix?: string;
21
23
  /**
@@ -68,6 +70,123 @@ interface SchematicNetlist {
68
70
  components: SchematicComponent[];
69
71
  nets: ElectricalNet[];
70
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
+ }
71
190
  /**
72
191
  * Semantic error categories for netlist retrieval. Mirrors the gRPC status
73
192
  * contract of `hq.ir.schematic.v1.NetListService` so an agent can distinguish
@@ -174,6 +293,12 @@ interface EdaHostClient {
174
293
  getEdaHostInfo(options?: EdaHostRequestOptions): Promise<EdaHostInfo>;
175
294
  /** Capabilities the host currently provides. */
176
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>;
177
302
  }
178
303
  /**
179
304
  * Extract the semantic `SchematicNetlist` from an hq-edge netlist body.
@@ -248,4 +373,4 @@ declare module '@deepseek-ai/cordis' {
248
373
  */
249
374
  declare function apply(ctx: Context, config?: Partial<EdaHostConfig>): () => void;
250
375
  //#endregion
251
- 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 PinDefinition, type PinReference, type SchematicComponent, type SchematicNetlist, apply, inject, name, parseNetlistBody };
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 };
package/lib/index.mjs CHANGED
@@ -9,12 +9,14 @@ const SCOPE_ROUTE = {
9
9
  function resolveEdaHostConfig(config, env = process.env) {
10
10
  const baseUrl = config?.hqEdgeBaseUrl ?? env.HQ_EDGE_BASE_URL ?? "";
11
11
  const pathPrefix = config?.netlistPathPrefix ?? env.HQ_EDGE_NETLIST_PATH ?? "/api/v1/netlist";
12
+ const pcbSelectionPrefix = config?.pcbSelectionPathPrefix ?? env.HQ_EDGE_PCB_SELECTION_PATH ?? "/api/v1/pcb-selection";
12
13
  const hostPrefix = config?.hostPathPrefix ?? env.HQ_EDGE_HOST_PATH ?? "/api/v1/host";
13
14
  const timeoutRaw = config?.requestTimeoutMs ?? env.HQ_EDGE_REQUEST_TIMEOUT_MS;
14
15
  const timeout = Number.parseInt(String(timeoutRaw ?? ""), 10);
15
16
  return {
16
17
  hqEdgeBaseUrl: baseUrl,
17
18
  netlistPathPrefix: pathPrefix,
19
+ pcbSelectionPathPrefix: pcbSelectionPrefix,
18
20
  hostPathPrefix: hostPrefix,
19
21
  requestTimeoutMs: Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_REQUEST_TIMEOUT_MS
20
22
  };
@@ -27,6 +29,10 @@ function hasHost(config) {
27
29
  function netlistUrlOf(config, scope) {
28
30
  return `${(config.hqEdgeBaseUrl ?? "").replace(/\/+$/, "")}/${(config.netlistPathPrefix ?? "/api/v1/netlist").replace(/^\/+|\/+$/g, "")}${SCOPE_ROUTE[scope]}`;
29
31
  }
32
+ /** Build the absolute URL for the PCB selection route. */
33
+ function pcbSelectionUrlOf(config) {
34
+ return `${(config.hqEdgeBaseUrl ?? "").replace(/\/+$/, "")}/${(config.pcbSelectionPathPrefix ?? "/api/v1/pcb-selection").replace(/^\/+|\/+$/g, "")}`;
35
+ }
30
36
  const HOST_ROUTE = {
31
37
  info: "/info",
32
38
  capabilities: "/capabilities"
@@ -185,6 +191,23 @@ function createEdaHostClient(config, deps = {}) {
185
191
  async function fetchScope(scope, options) {
186
192
  return parseNetlistBody(await getJson(netlistUrlOf(resolveConfig(), scope), options, "netlist"));
187
193
  }
194
+ function parsePcbSelection(value) {
195
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new NetlistError("INTERNAL", "eda-host: malformed PCB selection response from hq-edge");
196
+ const asArray = (v) => Array.isArray(v) ? v : [];
197
+ return {
198
+ footprints: asArray(value.footprints),
199
+ pads: asArray(value.pads),
200
+ tracks: asArray(value.tracks),
201
+ arcs: asArray(value.arcs),
202
+ vias: asArray(value.vias),
203
+ zones: asArray(value.zones),
204
+ shapes: asArray(value.shapes),
205
+ texts: asArray(value.texts),
206
+ dimensions: asArray(value.dimensions),
207
+ groups: asArray(value.groups),
208
+ nets: asArray(value.nets)
209
+ };
210
+ }
188
211
  return {
189
212
  getSelectionNetlist: (options) => fetchScope("selection", options),
190
213
  getProjectNetlist: (options) => fetchScope("project", options),
@@ -198,6 +221,9 @@ function createEdaHostClient(config, deps = {}) {
198
221
  const body = await getJson(hostUrlOf(resolveConfig(), "capabilities"), options, "host capabilities");
199
222
  if (!body || !Array.isArray(body.capabilities)) throw new NetlistError("INTERNAL", "eda-host: malformed host capabilities response from hq-edge");
200
223
  return body.capabilities.filter((c) => typeof c === "string");
224
+ },
225
+ getPcbSelection: async (options) => {
226
+ return parsePcbSelection(await getJson(pcbSelectionUrlOf(resolveConfig()), options, "pcb selection"));
201
227
  }
202
228
  };
203
229
  }
@@ -298,51 +324,77 @@ function createNetListTools(env) {
298
324
  * a claim that the host can provide it, nothing more.
299
325
  */
300
326
  function createEdaHostTools(env) {
301
- return [defineTool({
302
- name: "get_eda_host_info",
303
- description: "Describe the EDA host currently connected through hq-edge (DSH → dsh-eda-host → hq-edge → EDA host). Returns { ok, info: { identity: { hostType, hostName, version }, installation: { applicationPath, executables[]: { name, path } } } }. Use it when you need factual information about the current EDA environment, such as \"what EDA host am I connected to\", \"what version is it\", \"where is it installed\", or \"where is kicad-cli\". The returned information is authoritative host-provided ground truth. " + ERROR_SEMANTICS,
304
- parameters: {},
305
- output: {
306
- schema: { type: "json" },
307
- render: renderJson
308
- },
309
- async execute(_args, exec) {
310
- try {
311
- const options = exec?.signal ? { signal: exec.signal } : {};
312
- return asJson({
313
- ok: true,
314
- info: await env.client.getEdaHostInfo(options)
315
- });
316
- } catch (err) {
317
- return asJson({
318
- ok: false,
319
- error: failureOf(err)
320
- });
327
+ return [
328
+ defineTool({
329
+ name: "get_eda_host_info",
330
+ description: "Describe the EDA host currently connected through hq-edge (DSH → dsh-eda-host → hq-edge → EDA host). Returns { ok, info: { identity: { hostType, hostName, version }, installation: { applicationPath, executables[]: { name, path } } } }. Use it when you need factual information about the current EDA environment, such as \"what EDA host am I connected to\", \"what version is it\", \"where is it installed\", or \"where is kicad-cli\". The returned information is authoritative host-provided ground truth. " + ERROR_SEMANTICS,
331
+ parameters: {},
332
+ output: {
333
+ schema: { type: "json" },
334
+ render: renderJson
335
+ },
336
+ async execute(_args, exec) {
337
+ try {
338
+ const options = exec?.signal ? { signal: exec.signal } : {};
339
+ return asJson({
340
+ ok: true,
341
+ info: await env.client.getEdaHostInfo(options)
342
+ });
343
+ } catch (err) {
344
+ return asJson({
345
+ ok: false,
346
+ error: failureOf(err)
347
+ });
348
+ }
321
349
  }
322
- }
323
- }), defineTool({
324
- name: "get_eda_host_capabilities",
325
- description: "List the capabilities the CURRENT EDA host provides, using EDA-independent capability identifiers such as EDA_HOST_CAPABILITY_NETLIST, EDA_HOST_CAPABILITY_PCB or EDA_HOST_CAPABILITY_BOM. Returns { ok, capabilities: string[] }. This is DISCOVERY, not execution: it reports capabilities already provided by the connected EDA host. Treat the returned capability list as the authoritative runtime contract for the current host. Do not assume a capability is available merely because the EDA application is generally known to support it. Unknown capability identifiers MUST be treated as unsupported. " + ERROR_SEMANTICS,
326
- parameters: {},
327
- output: {
328
- schema: { type: "json" },
329
- render: renderJson
330
- },
331
- async execute(_args, exec) {
332
- try {
333
- const options = exec?.signal ? { signal: exec.signal } : {};
334
- return asJson({
335
- ok: true,
336
- capabilities: await env.client.getEdaHostCapabilities(options)
337
- });
338
- } catch (err) {
339
- return asJson({
340
- ok: false,
341
- error: failureOf(err)
342
- });
350
+ }),
351
+ defineTool({
352
+ name: "get_pcb_selection",
353
+ description: "Read the semantic PCB selection from the current PCB editor through hq-edge (DSH dsh-eda-host hq-edge EDA host). Returns { ok, selection: { footprints[], pads[], tracks[], arcs[], vias[], zones[], shapes[], texts[], dimensions[], groups[], nets[] } }. Each footprint has reference, value, footprint, position {x,y} in mm, rotationDeg and pads[] (pin, type, shape, position, widthMm, heightMm, rotationDeg, layer, net{name, code}); tracks have layer, start/end in mm, widthMm, lengthMm and net; vias have layers[], drillMm, viaType and start/end; zones have layer, net and outline segments. Every object carries id — the EDA-host native object identity. IMPORTANT: ok:true with all-empty arrays is a VALID empty selection (nothing selected) do not treat it as a failure. " + ERROR_SEMANTICS,
354
+ parameters: {},
355
+ output: {
356
+ schema: { type: "json" },
357
+ render: renderJson
358
+ },
359
+ async execute(_args, exec) {
360
+ try {
361
+ const options = exec?.signal ? { signal: exec.signal } : {};
362
+ return asJson({
363
+ ok: true,
364
+ selection: await env.client.getPcbSelection(options)
365
+ });
366
+ } catch (err) {
367
+ return asJson({
368
+ ok: false,
369
+ error: failureOf(err)
370
+ });
371
+ }
343
372
  }
344
- }
345
- })];
373
+ }),
374
+ defineTool({
375
+ name: "get_eda_host_capabilities",
376
+ description: "List the capabilities the CURRENT EDA host provides, using EDA-independent capability identifiers such as EDA_HOST_CAPABILITY_NETLIST, EDA_HOST_CAPABILITY_PCB or EDA_HOST_CAPABILITY_BOM. Returns { ok, capabilities: string[] }. This is DISCOVERY, not execution: it reports capabilities already provided by the connected EDA host. Treat the returned capability list as the authoritative runtime contract for the current host. Do not assume a capability is available merely because the EDA application is generally known to support it. Unknown capability identifiers MUST be treated as unsupported. " + ERROR_SEMANTICS,
377
+ parameters: {},
378
+ output: {
379
+ schema: { type: "json" },
380
+ render: renderJson
381
+ },
382
+ async execute(_args, exec) {
383
+ try {
384
+ const options = exec?.signal ? { signal: exec.signal } : {};
385
+ return asJson({
386
+ ok: true,
387
+ capabilities: await env.client.getEdaHostCapabilities(options)
388
+ });
389
+ } catch (err) {
390
+ return asJson({
391
+ ok: false,
392
+ error: failureOf(err)
393
+ });
394
+ }
395
+ }
396
+ })
397
+ ];
346
398
  }
347
399
  //#endregion
348
400
  //#region src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huaqiu/dsh-eda-host",
3
- "version": "0.3.25",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "main": "./lib/index.mjs",
6
6
  "types": "./lib/index.d.mts",
@@ -22,7 +22,7 @@
22
22
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.0"
23
23
  },
24
24
  "dependencies": {
25
- "@huaqiu/dsh-plugin-log": "0.3.25"
25
+ "@huaqiu/dsh-plugin-log": "0.4.2"
26
26
  },
27
27
  "files": [
28
28
  "lib",
package/src/client.ts CHANGED
@@ -10,11 +10,12 @@
10
10
  * @module
11
11
  */
12
12
 
13
- import { DEFAULT_REQUEST_TIMEOUT_MS, hostUrlOf, netlistUrlOf, type EdaHostConfig, type NetlistScope } from './config.js'
13
+ import { DEFAULT_REQUEST_TIMEOUT_MS, hostUrlOf, netlistUrlOf, pcbSelectionUrlOf, type EdaHostConfig, type NetlistScope } from './config.js'
14
14
  import {
15
15
  NetlistError,
16
16
  type EdaHostCapability,
17
17
  type EdaHostInfo,
18
+ type PcbSelection,
18
19
  type SchematicNetlist,
19
20
  } from './types.js'
20
21
 
@@ -51,6 +52,12 @@ export interface EdaHostClient {
51
52
  getEdaHostInfo(options?: EdaHostRequestOptions): Promise<EdaHostInfo>
52
53
  /** Capabilities the host currently provides. */
53
54
  getEdaHostCapabilities(options?: EdaHostRequestOptions): Promise<EdaHostCapability[]>
55
+ /**
56
+ * Semantic PCB selection of the current PCB editor (hq.pcb.v1
57
+ * PcbSelectionService.GetSelection bridged through hq-edge). An empty
58
+ * selection resolves to an all-empty `PcbSelection` — never an error.
59
+ */
60
+ getPcbSelection(options?: EdaHostRequestOptions): Promise<PcbSelection>
54
61
  }
55
62
 
56
63
  /** HTTP status → semantic error kind (see routes/edaHostStatus.ts on hq-edge). */
@@ -260,6 +267,32 @@ export function createEdaHostClient(
260
267
  return parseNetlistBody(await getJson(url, options, 'netlist'))
261
268
  }
262
269
 
270
+ function parsePcbSelection(value: unknown): PcbSelection {
271
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
272
+ throw new NetlistError(
273
+ 'INTERNAL',
274
+ 'eda-host: malformed PCB selection response from hq-edge',
275
+ )
276
+ }
277
+
278
+ const asArray = (v: unknown): unknown[] => (Array.isArray(v) ? v : [])
279
+
280
+ // proto3 JSON omits empty arrays; every field is a repeated list.
281
+ return {
282
+ footprints: asArray((value as Record<string, unknown>).footprints),
283
+ pads: asArray((value as Record<string, unknown>).pads),
284
+ tracks: asArray((value as Record<string, unknown>).tracks),
285
+ arcs: asArray((value as Record<string, unknown>).arcs),
286
+ vias: asArray((value as Record<string, unknown>).vias),
287
+ zones: asArray((value as Record<string, unknown>).zones),
288
+ shapes: asArray((value as Record<string, unknown>).shapes),
289
+ texts: asArray((value as Record<string, unknown>).texts),
290
+ dimensions: asArray((value as Record<string, unknown>).dimensions),
291
+ groups: asArray((value as Record<string, unknown>).groups),
292
+ nets: asArray((value as Record<string, unknown>).nets),
293
+ } as PcbSelection
294
+ }
295
+
263
296
  return {
264
297
  getSelectionNetlist: (options) => fetchScope('selection', options),
265
298
  getProjectNetlist: (options) => fetchScope('project', options),
@@ -293,5 +326,12 @@ export function createEdaHostClient(
293
326
  (c): c is EdaHostCapability => typeof c === 'string',
294
327
  )
295
328
  },
329
+
330
+ getPcbSelection: async (options) => {
331
+ const resolved = resolveConfig()
332
+ const url = pcbSelectionUrlOf(resolved)
333
+ const body = (await getJson(url, options, 'pcb selection')) as unknown
334
+ return parsePcbSelection(body)
335
+ },
296
336
  }
297
337
  }
package/src/config.ts CHANGED
@@ -15,6 +15,8 @@ export interface EdaHostConfig {
15
15
  hqEdgeBaseUrl?: string
16
16
  /** Path prefix on the host; default "/api/v1/netlist". */
17
17
  netlistPathPrefix?: string
18
+ /** Path prefix for PCB selection; default "/api/v1/pcb-selection". */
19
+ pcbSelectionPathPrefix?: string
18
20
  /** Path prefix for host discovery; default "/api/v1/host". */
19
21
  hostPathPrefix?: string
20
22
  /**
@@ -30,6 +32,8 @@ export interface EdaHostConfig {
30
32
 
31
33
  export const DEFAULT_NETLIST_PATH_PREFIX = '/api/v1/netlist'
32
34
 
35
+ export const DEFAULT_PCB_SELECTION_PATH_PREFIX = '/api/v1/pcb-selection'
36
+
33
37
  export const DEFAULT_HOST_PATH_PREFIX = '/api/v1/host'
34
38
 
35
39
  export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000
@@ -50,6 +54,10 @@ export function resolveEdaHostConfig(
50
54
  const baseUrl = config?.hqEdgeBaseUrl ?? env.HQ_EDGE_BASE_URL ?? ''
51
55
  const pathPrefix =
52
56
  config?.netlistPathPrefix ?? env.HQ_EDGE_NETLIST_PATH ?? DEFAULT_NETLIST_PATH_PREFIX
57
+ const pcbSelectionPrefix =
58
+ config?.pcbSelectionPathPrefix ??
59
+ env.HQ_EDGE_PCB_SELECTION_PATH ??
60
+ DEFAULT_PCB_SELECTION_PATH_PREFIX
53
61
  const hostPrefix =
54
62
  config?.hostPathPrefix ?? env.HQ_EDGE_HOST_PATH ?? DEFAULT_HOST_PATH_PREFIX
55
63
  const timeoutRaw = config?.requestTimeoutMs ?? env.HQ_EDGE_REQUEST_TIMEOUT_MS
@@ -57,6 +65,7 @@ export function resolveEdaHostConfig(
57
65
  return {
58
66
  hqEdgeBaseUrl: baseUrl,
59
67
  netlistPathPrefix: pathPrefix,
68
+ pcbSelectionPathPrefix: pcbSelectionPrefix,
60
69
  hostPathPrefix: hostPrefix,
61
70
  requestTimeoutMs:
62
71
  Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_REQUEST_TIMEOUT_MS,
@@ -75,6 +84,16 @@ export function netlistUrlOf(config: EdaHostConfig, scope: NetlistScope): string
75
84
  return `${base}/${prefix}${SCOPE_ROUTE[scope]}`
76
85
  }
77
86
 
87
+ /** Build the absolute URL for the PCB selection route. */
88
+ export function pcbSelectionUrlOf(config: EdaHostConfig): string {
89
+ const base = (config.hqEdgeBaseUrl ?? '').replace(/\/+$/, '')
90
+ const prefix = (config.pcbSelectionPathPrefix ?? DEFAULT_PCB_SELECTION_PATH_PREFIX).replace(
91
+ /^\/+|\/+$/g,
92
+ '',
93
+ )
94
+ return `${base}/${prefix}`
95
+ }
96
+
78
97
  /** Host discovery route suffix. */
79
98
  export type HostRoute = 'info' | 'capabilities'
80
99
 
package/src/index.ts CHANGED
@@ -2,11 +2,12 @@
2
2
  * `@huaqiu/dsh-eda-host` — node plugin entry.
3
3
  *
4
4
  * Provides the `edaHost` service (semantic EDA-host capability) and registers
5
- * five agent tools:
5
+ * six agent tools:
6
6
  *
7
7
  * get_project_netlist complete project netlist
8
8
  * get_selection_netlist currently selected components netlist
9
9
  * get_active_page_netlist active schematic page netlist
10
+ * get_pcb_selection semantic PCB selection of the current PCB editor
10
11
  * get_eda_host_info which EDA host, version, installation
11
12
  * get_eda_host_capabilities what the current host can actually do
12
13
  *
@@ -63,6 +64,20 @@ export type {
63
64
  ElectricalNet,
64
65
  ElectricalType,
65
66
  NetlistErrorKind,
67
+ PcbArc,
68
+ PcbDimension,
69
+ PcbFootprint,
70
+ PcbGroup,
71
+ PcbNetRef,
72
+ PcbPad,
73
+ PcbPoint,
74
+ PcbSegment,
75
+ PcbSelection,
76
+ PcbShape,
77
+ PcbText,
78
+ PcbTrack,
79
+ PcbVia,
80
+ PcbZone,
66
81
  PinDefinition,
67
82
  PinReference,
68
83
  SchematicComponent,
package/src/tools.ts CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  import { defineTool } from '@deepseek-ai/dsh-tools'
26
26
  import type { EdaHostClient, EdaHostRequestOptions } from './client.js'
27
- import { NetlistError, type EdaHostCapability, type EdaHostInfo, type SchematicNetlist } from './types.js'
27
+ import { NetlistError, type EdaHostCapability, type EdaHostInfo, type PcbSelection, type SchematicNetlist } from './types.js'
28
28
 
29
29
  /** Structural alias of the DSH `JsonValue`. */
30
30
  type Json = string | number | boolean | null | Json[] | { [key: string]: Json }
@@ -62,6 +62,10 @@ type HostCapabilitiesResult =
62
62
  | { ok: true; capabilities: EdaHostCapability[] }
63
63
  | { ok: false; error: { kind: string; message: string } }
64
64
 
65
+ type PcbSelectionResult =
66
+ | { ok: true; selection: PcbSelection }
67
+ | { ok: false; error: { kind: string; message: string } }
68
+
65
69
  /**
66
70
  * Every failure kind, and what the agent should do about it.
67
71
  *
@@ -192,6 +196,36 @@ export function createNetListTools(env: NetlistToolEnv) {
192
196
  },
193
197
  }),
194
198
 
199
+ defineTool({
200
+ name: 'get_pcb_selection',
201
+ description:
202
+ `Read the semantic PCB selection from the current PCB editor through hq-edge ` +
203
+ `(DSH → dsh-eda-host → hq-edge → EDA host). Returns { ok, selection: { footprints[], ` +
204
+ `pads[], tracks[], arcs[], vias[], zones[], shapes[], texts[], dimensions[], groups[], ` +
205
+ `nets[] } }. ` +
206
+ `Each footprint has reference, value, footprint, position {x,y} in mm, rotationDeg and ` +
207
+ `pads[] (pin, type, shape, position, widthMm, heightMm, rotationDeg, layer, net{name, ` +
208
+ `code}); tracks have layer, start/end in mm, widthMm, lengthMm and net; vias have ` +
209
+ `layers[], drillMm, viaType and start/end; zones have layer, net and outline segments. ` +
210
+ `Every object carries id — the EDA-host native object identity. ` +
211
+ `IMPORTANT: ok:true with all-empty arrays is a VALID empty selection (nothing selected) ` +
212
+ `— do not treat it as a failure. ` +
213
+ ERROR_SEMANTICS,
214
+ parameters: {},
215
+ output: { schema: { type: 'json' }, render: renderJson },
216
+ async execute(_args: unknown, exec: ToolExecLike): Promise<Json> {
217
+ try {
218
+ const options: EdaHostRequestOptions = exec?.signal
219
+ ? { signal: exec.signal }
220
+ : {}
221
+ const selection = await env.client.getPcbSelection(options)
222
+ return asJson<PcbSelectionResult>({ ok: true, selection })
223
+ } catch (err) {
224
+ return asJson<PcbSelectionResult>({ ok: false, error: failureOf(err) })
225
+ }
226
+ },
227
+ }),
228
+
195
229
  defineTool({
196
230
  name: 'get_eda_host_capabilities',
197
231
  description:
package/src/types.ts CHANGED
@@ -54,6 +54,138 @@ export interface SchematicNetlist {
54
54
  nets: ElectricalNet[]
55
55
  }
56
56
 
57
+ /**
58
+ * Semantic PCB selection types.
59
+ *
60
+ * Self-contained structural mirror of `hq.pcb.v1` (the hq-edge-owned semantic
61
+ * protobuf contract for `PcbSelectionService.GetSelection`). Units follow the
62
+ * hq.pcb.v1 convention: positions/sizes/lengths in mm, rotations in degrees,
63
+ * `id` is the KiCad native object identity.
64
+ */
65
+
66
+ export interface PcbPoint {
67
+ x: number
68
+ y: number
69
+ }
70
+
71
+ export interface PcbNetRef {
72
+ name: string
73
+ code: number
74
+ }
75
+
76
+ export interface PcbPad {
77
+ id: string
78
+ pin: string
79
+ type: string
80
+ shape: string
81
+ position?: PcbPoint
82
+ widthMm: number
83
+ heightMm: number
84
+ rotationDeg: number
85
+ layer: string
86
+ net?: PcbNetRef
87
+ }
88
+
89
+ export interface PcbFootprint {
90
+ id: string
91
+ reference: string
92
+ value: string
93
+ footprint: string
94
+ position?: PcbPoint
95
+ rotationDeg: number
96
+ pads: PcbPad[]
97
+ }
98
+
99
+ export interface PcbTrack {
100
+ id: string
101
+ layer: string
102
+ start?: PcbPoint
103
+ end?: PcbPoint
104
+ widthMm: number
105
+ lengthMm: number
106
+ net?: PcbNetRef
107
+ }
108
+
109
+ export interface PcbArc {
110
+ id: string
111
+ layer: string
112
+ start?: PcbPoint
113
+ end?: PcbPoint
114
+ mid?: PcbPoint
115
+ net?: PcbNetRef
116
+ }
117
+
118
+ export interface PcbVia {
119
+ id: string
120
+ layers: string[]
121
+ drillMm: number
122
+ viaType: string
123
+ start?: PcbPoint
124
+ end?: PcbPoint
125
+ }
126
+
127
+ export interface PcbSegment {
128
+ start?: PcbPoint
129
+ end?: PcbPoint
130
+ }
131
+
132
+ export interface PcbZone {
133
+ id: string
134
+ layer: string
135
+ net?: PcbNetRef
136
+ outline: PcbSegment[]
137
+ }
138
+
139
+ export interface PcbShape {
140
+ id: string
141
+ layer: string
142
+ shapeType: string
143
+ start?: PcbPoint
144
+ end?: PcbPoint
145
+ center?: PcbPoint
146
+ radiusMm: number
147
+ mid?: PcbPoint
148
+ widthMm: number
149
+ }
150
+
151
+ export interface PcbText {
152
+ id: string
153
+ layer: string
154
+ text: string
155
+ position?: PcbPoint
156
+ rotationDeg: number
157
+ hJustify: string
158
+ vJustify: string
159
+ }
160
+
161
+ export interface PcbDimension {
162
+ id: string
163
+ layer: string
164
+ start?: PcbPoint
165
+ end?: PcbPoint
166
+ value: string
167
+ dimType: string
168
+ }
169
+
170
+ export interface PcbGroup {
171
+ id: string
172
+ itemIds: string[]
173
+ }
174
+
175
+ export interface PcbSelection {
176
+ footprints: PcbFootprint[]
177
+ pads: PcbPad[]
178
+ tracks: PcbTrack[]
179
+ arcs: PcbArc[]
180
+ vias: PcbVia[]
181
+ zones: PcbZone[]
182
+ shapes: PcbShape[]
183
+ texts: PcbText[]
184
+ dimensions: PcbDimension[]
185
+ groups: PcbGroup[]
186
+ nets: PcbNetRef[]
187
+ }
188
+
57
189
  /**
58
190
  * Semantic error categories for netlist retrieval. Mirrors the gRPC status
59
191
  * contract of `hq.ir.schematic.v1.NetListService` so an agent can distinguish