@huaqiu/dsh-eda-host 0.4.5 → 0.4.7

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
@@ -18,6 +18,8 @@ interface EdaHostConfig {
18
18
  netlistPathPrefix?: string;
19
19
  /** Path prefix for PCB selection; default "/api/v1/pcb-selection". */
20
20
  pcbSelectionPathPrefix?: string;
21
+ /** Path prefix for the complete-board query; default "/api/v1/pcb-board". */
22
+ pcbBoardPathPrefix?: string;
21
23
  /** Path prefix for host discovery; default "/api/v1/host". */
22
24
  hostPathPrefix?: string;
23
25
  /**
@@ -71,12 +73,13 @@ interface SchematicNetlist {
71
73
  nets: ElectricalNet[];
72
74
  }
73
75
  /**
74
- * Semantic PCB selection types.
76
+ * Semantic PCB snapshot types.
75
77
  *
76
78
  * 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.
79
+ * protobuf contract for `PcbQueryService.GetSelection` / `GetBoard`). The
80
+ * same PcbSnapshot shape backs both the current selection and the complete
81
+ * board. Units follow the hq.pcb.v1 convention: positions/sizes/lengths in mm,
82
+ * rotations in degrees, `id` is the KiCad native object identity.
80
83
  */
81
84
  interface PcbPoint {
82
85
  x: number;
@@ -174,7 +177,7 @@ interface PcbGroup {
174
177
  id: string;
175
178
  itemIds: string[];
176
179
  }
177
- interface PcbSelection {
180
+ interface PcbSnapshot {
178
181
  footprints: PcbFootprint[];
179
182
  pads: PcbPad[];
180
183
  tracks: PcbTrack[];
@@ -294,11 +297,17 @@ interface EdaHostClient {
294
297
  /** Capabilities the host currently provides. */
295
298
  getEdaHostCapabilities(options?: EdaHostRequestOptions): Promise<EdaHostCapability[]>;
296
299
  /**
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
+ * Semantic PCB snapshot of the current PCB editor selection (hq.pcb.v1
301
+ * PcbQueryService.GetSelection bridged through hq-edge). An empty
302
+ * selection resolves to an all-empty `PcbSnapshot` — never an error.
300
303
  */
301
- getPcbSelection(options?: EdaHostRequestOptions): Promise<PcbSelection>;
304
+ getPcbSelection(options?: EdaHostRequestOptions): Promise<PcbSnapshot>;
305
+ /**
306
+ * Semantic PCB snapshot of the complete board (hq.pcb.v1
307
+ * PcbQueryService.GetBoard bridged through hq-edge). An empty board
308
+ * resolves to an all-empty `PcbSnapshot` — never an error.
309
+ */
310
+ getPcbBoard(options?: EdaHostRequestOptions): Promise<PcbSnapshot>;
302
311
  }
303
312
  /**
304
313
  * Extract the semantic `SchematicNetlist` from an hq-edge netlist body.
@@ -373,4 +382,4 @@ declare module '@deepseek-ai/cordis' {
373
382
  */
374
383
  declare function apply(ctx: Context, config?: Partial<EdaHostConfig>): () => void;
375
384
  //#endregion
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 };
385
+ 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 PcbShape, type PcbSnapshot, 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
@@ -10,6 +10,7 @@ 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
12
  const pcbSelectionPrefix = config?.pcbSelectionPathPrefix ?? env.HQ_EDGE_PCB_SELECTION_PATH ?? "/api/v1/pcb-selection";
13
+ const pcbBoardPrefix = config?.pcbBoardPathPrefix ?? env.HQ_EDGE_PCB_BOARD_PATH ?? "/api/v1/pcb-board";
13
14
  const hostPrefix = config?.hostPathPrefix ?? env.HQ_EDGE_HOST_PATH ?? "/api/v1/host";
14
15
  const timeoutRaw = config?.requestTimeoutMs ?? env.HQ_EDGE_REQUEST_TIMEOUT_MS;
15
16
  const timeout = Number.parseInt(String(timeoutRaw ?? ""), 10);
@@ -17,6 +18,7 @@ function resolveEdaHostConfig(config, env = process.env) {
17
18
  hqEdgeBaseUrl: baseUrl,
18
19
  netlistPathPrefix: pathPrefix,
19
20
  pcbSelectionPathPrefix: pcbSelectionPrefix,
21
+ pcbBoardPathPrefix: pcbBoardPrefix,
20
22
  hostPathPrefix: hostPrefix,
21
23
  requestTimeoutMs: Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_REQUEST_TIMEOUT_MS
22
24
  };
@@ -33,6 +35,10 @@ function netlistUrlOf(config, scope) {
33
35
  function pcbSelectionUrlOf(config) {
34
36
  return `${(config.hqEdgeBaseUrl ?? "").replace(/\/+$/, "")}/${(config.pcbSelectionPathPrefix ?? "/api/v1/pcb-selection").replace(/^\/+|\/+$/g, "")}`;
35
37
  }
38
+ /** Build the absolute URL for the complete-board query route. */
39
+ function pcbBoardUrlOf(config) {
40
+ return `${(config.hqEdgeBaseUrl ?? "").replace(/\/+$/, "")}/${(config.pcbBoardPathPrefix ?? "/api/v1/pcb-board").replace(/^\/+|\/+$/g, "")}`;
41
+ }
36
42
  const HOST_ROUTE = {
37
43
  info: "/info",
38
44
  capabilities: "/capabilities"
@@ -191,8 +197,8 @@ function createEdaHostClient(config, deps = {}) {
191
197
  async function fetchScope(scope, options) {
192
198
  return parseNetlistBody(await getJson(netlistUrlOf(resolveConfig(), scope), options, "netlist"));
193
199
  }
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");
200
+ function parsePcbSnapshot(value, what) {
201
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new NetlistError("INTERNAL", `eda-host: malformed ${what} response from hq-edge`);
196
202
  const asArray = (v) => Array.isArray(v) ? v : [];
197
203
  return {
198
204
  footprints: asArray(value.footprints),
@@ -223,7 +229,10 @@ function createEdaHostClient(config, deps = {}) {
223
229
  return body.capabilities.filter((c) => typeof c === "string");
224
230
  },
225
231
  getPcbSelection: async (options) => {
226
- return parsePcbSelection(await getJson(pcbSelectionUrlOf(resolveConfig()), options, "pcb selection"));
232
+ return parsePcbSnapshot(await getJson(pcbSelectionUrlOf(resolveConfig()), options, "pcb selection"), "PCB selection");
233
+ },
234
+ getPcbBoard: async (options) => {
235
+ return parsePcbSnapshot(await getJson(pcbBoardUrlOf(resolveConfig()), options, "pcb board"), "PCB board");
227
236
  }
228
237
  };
229
238
  }
@@ -327,7 +336,7 @@ function createEdaHostTools(env) {
327
336
  return [
328
337
  defineTool({
329
338
  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,
339
+ 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\". When the host is KiCad, the executable list also carries \"kicad-python\" — the interpreter bundled with KiCad that owns the official kicad-python package (kipy); the dsh-kicad skills already run with it automatically. The returned information is authoritative host-provided ground truth. " + ERROR_SEMANTICS,
331
340
  parameters: {},
332
341
  output: {
333
342
  schema: { type: "json" },
@@ -371,6 +380,29 @@ function createEdaHostTools(env) {
371
380
  }
372
381
  }
373
382
  }),
383
+ defineTool({
384
+ name: "get_pcb_board",
385
+ description: "Read the COMPLETE semantic PCB board from the current PCB editor through hq-edge (DSH → dsh-eda-host → hq-edge → EDA host). Returns { ok, snapshot: { footprints[], pads[], tracks[], arcs[], vias[], zones[], shapes[], texts[], dimensions[], groups[], nets[] } } — the same shape as get_pcb_selection, but covering every object on the board regardless of selection. 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 board — do not treat it as a failure. " + ERROR_SEMANTICS,
386
+ parameters: {},
387
+ output: {
388
+ schema: { type: "json" },
389
+ render: renderJson
390
+ },
391
+ async execute(_args, exec) {
392
+ try {
393
+ const options = exec?.signal ? { signal: exec.signal } : {};
394
+ return asJson({
395
+ ok: true,
396
+ snapshot: await env.client.getPcbBoard(options)
397
+ });
398
+ } catch (err) {
399
+ return asJson({
400
+ ok: false,
401
+ error: failureOf(err)
402
+ });
403
+ }
404
+ }
405
+ }),
374
406
  defineTool({
375
407
  name: "get_eda_host_capabilities",
376
408
  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,
@@ -418,6 +450,30 @@ const name = "@huaqiu/dsh-eda-host";
418
450
  */
419
451
  const inject = ["hqEdge", "tools"];
420
452
  const log = getLogger("dsh-eda-host");
453
+ /**
454
+ * Env vars relevant to the hq-edge / DSH / KiCad integration, captured at
455
+ * plugin apply() time so startup issues are diagnosable from the log.
456
+ *
457
+ * Values are logged verbatim EXCEPT `KICAD_API_TOKEN`, which is a live
458
+ * credential KiCad injects for the IPC socket — only its presence is logged
459
+ * (never the token itself).
460
+ */
461
+ const DSH_ENV_LOG_KEYS = [
462
+ "DSH_KICAD_PYTHON",
463
+ "DSH_KICAD_SKILLS_DIR",
464
+ "DSH_HOME",
465
+ "HQ_EDGE_BASE_URL",
466
+ "KICAD_API_SOCKET",
467
+ "KICAD_API_TOKEN"
468
+ ];
469
+ function envSnapshot() {
470
+ const snapshot = {};
471
+ for (const key of DSH_ENV_LOG_KEYS) {
472
+ const value = process.env[key];
473
+ snapshot[key] = key === "KICAD_API_TOKEN" ? value ? "<set>" : null : value ?? null;
474
+ }
475
+ return snapshot;
476
+ }
421
477
  log.info("dsh-eda-host: module loaded (waiting for the hqEdge + tools services)");
422
478
  /**
423
479
  * Host plugin body — provide `edaHost` and register the three netlist tools.
@@ -449,7 +505,8 @@ function apply(ctx, config = {}) {
449
505
  hqEdgeBaseUrlFromConfig: resolved.hqEdgeBaseUrl ?? null,
450
506
  netlistPathPrefix: resolved.netlistPathPrefix,
451
507
  hostPathPrefix: resolved.hostPathPrefix,
452
- requestTimeoutMs: resolved.requestTimeoutMs
508
+ requestTimeoutMs: resolved.requestTimeoutMs,
509
+ env: envSnapshot()
453
510
  });
454
511
  const client = createEdaHostClient(resolved, { baseUrlResolver: getHqEdgeBaseUrl });
455
512
  ctx.effect(() => ctx.provide("edaHost", client));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huaqiu/dsh-eda-host",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
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.4.5"
25
+ "@huaqiu/dsh-plugin-log": "0.4.7"
26
26
  },
27
27
  "files": [
28
28
  "lib",
package/src/client.ts CHANGED
@@ -10,12 +10,12 @@
10
10
  * @module
11
11
  */
12
12
 
13
- import { DEFAULT_REQUEST_TIMEOUT_MS, hostUrlOf, netlistUrlOf, pcbSelectionUrlOf, type EdaHostConfig, type NetlistScope } from './config.js'
13
+ import { DEFAULT_REQUEST_TIMEOUT_MS, hostUrlOf, netlistUrlOf, pcbBoardUrlOf, 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
+ type PcbSnapshot,
19
19
  type SchematicNetlist,
20
20
  } from './types.js'
21
21
 
@@ -53,11 +53,17 @@ export interface EdaHostClient {
53
53
  /** Capabilities the host currently provides. */
54
54
  getEdaHostCapabilities(options?: EdaHostRequestOptions): Promise<EdaHostCapability[]>
55
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.
56
+ * Semantic PCB snapshot of the current PCB editor selection (hq.pcb.v1
57
+ * PcbQueryService.GetSelection bridged through hq-edge). An empty
58
+ * selection resolves to an all-empty `PcbSnapshot` — never an error.
59
59
  */
60
- getPcbSelection(options?: EdaHostRequestOptions): Promise<PcbSelection>
60
+ getPcbSelection(options?: EdaHostRequestOptions): Promise<PcbSnapshot>
61
+ /**
62
+ * Semantic PCB snapshot of the complete board (hq.pcb.v1
63
+ * PcbQueryService.GetBoard bridged through hq-edge). An empty board
64
+ * resolves to an all-empty `PcbSnapshot` — never an error.
65
+ */
66
+ getPcbBoard(options?: EdaHostRequestOptions): Promise<PcbSnapshot>
61
67
  }
62
68
 
63
69
  /** HTTP status → semantic error kind (see routes/edaHostStatus.ts on hq-edge). */
@@ -267,11 +273,11 @@ export function createEdaHostClient(
267
273
  return parseNetlistBody(await getJson(url, options, 'netlist'))
268
274
  }
269
275
 
270
- function parsePcbSelection(value: unknown): PcbSelection {
276
+ function parsePcbSnapshot(value: unknown, what: string): PcbSnapshot {
271
277
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
272
278
  throw new NetlistError(
273
279
  'INTERNAL',
274
- 'eda-host: malformed PCB selection response from hq-edge',
280
+ `eda-host: malformed ${what} response from hq-edge`,
275
281
  )
276
282
  }
277
283
 
@@ -290,7 +296,7 @@ export function createEdaHostClient(
290
296
  dimensions: asArray((value as Record<string, unknown>).dimensions),
291
297
  groups: asArray((value as Record<string, unknown>).groups),
292
298
  nets: asArray((value as Record<string, unknown>).nets),
293
- } as PcbSelection
299
+ } as PcbSnapshot
294
300
  }
295
301
 
296
302
  return {
@@ -331,7 +337,14 @@ export function createEdaHostClient(
331
337
  const resolved = resolveConfig()
332
338
  const url = pcbSelectionUrlOf(resolved)
333
339
  const body = (await getJson(url, options, 'pcb selection')) as unknown
334
- return parsePcbSelection(body)
340
+ return parsePcbSnapshot(body, 'PCB selection')
341
+ },
342
+
343
+ getPcbBoard: async (options) => {
344
+ const resolved = resolveConfig()
345
+ const url = pcbBoardUrlOf(resolved)
346
+ const body = (await getJson(url, options, 'pcb board')) as unknown
347
+ return parsePcbSnapshot(body, 'PCB board')
335
348
  },
336
349
  }
337
350
  }
package/src/config.ts CHANGED
@@ -17,6 +17,8 @@ export interface EdaHostConfig {
17
17
  netlistPathPrefix?: string
18
18
  /** Path prefix for PCB selection; default "/api/v1/pcb-selection". */
19
19
  pcbSelectionPathPrefix?: string
20
+ /** Path prefix for the complete-board query; default "/api/v1/pcb-board". */
21
+ pcbBoardPathPrefix?: string
20
22
  /** Path prefix for host discovery; default "/api/v1/host". */
21
23
  hostPathPrefix?: string
22
24
  /**
@@ -34,6 +36,8 @@ export const DEFAULT_NETLIST_PATH_PREFIX = '/api/v1/netlist'
34
36
 
35
37
  export const DEFAULT_PCB_SELECTION_PATH_PREFIX = '/api/v1/pcb-selection'
36
38
 
39
+ export const DEFAULT_PCB_BOARD_PATH_PREFIX = '/api/v1/pcb-board'
40
+
37
41
  export const DEFAULT_HOST_PATH_PREFIX = '/api/v1/host'
38
42
 
39
43
  export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000
@@ -58,6 +62,10 @@ export function resolveEdaHostConfig(
58
62
  config?.pcbSelectionPathPrefix ??
59
63
  env.HQ_EDGE_PCB_SELECTION_PATH ??
60
64
  DEFAULT_PCB_SELECTION_PATH_PREFIX
65
+ const pcbBoardPrefix =
66
+ config?.pcbBoardPathPrefix ??
67
+ env.HQ_EDGE_PCB_BOARD_PATH ??
68
+ DEFAULT_PCB_BOARD_PATH_PREFIX
61
69
  const hostPrefix =
62
70
  config?.hostPathPrefix ?? env.HQ_EDGE_HOST_PATH ?? DEFAULT_HOST_PATH_PREFIX
63
71
  const timeoutRaw = config?.requestTimeoutMs ?? env.HQ_EDGE_REQUEST_TIMEOUT_MS
@@ -66,6 +74,7 @@ export function resolveEdaHostConfig(
66
74
  hqEdgeBaseUrl: baseUrl,
67
75
  netlistPathPrefix: pathPrefix,
68
76
  pcbSelectionPathPrefix: pcbSelectionPrefix,
77
+ pcbBoardPathPrefix: pcbBoardPrefix,
69
78
  hostPathPrefix: hostPrefix,
70
79
  requestTimeoutMs:
71
80
  Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_REQUEST_TIMEOUT_MS,
@@ -94,6 +103,16 @@ export function pcbSelectionUrlOf(config: EdaHostConfig): string {
94
103
  return `${base}/${prefix}`
95
104
  }
96
105
 
106
+ /** Build the absolute URL for the complete-board query route. */
107
+ export function pcbBoardUrlOf(config: EdaHostConfig): string {
108
+ const base = (config.hqEdgeBaseUrl ?? '').replace(/\/+$/, '')
109
+ const prefix = (config.pcbBoardPathPrefix ?? DEFAULT_PCB_BOARD_PATH_PREFIX).replace(
110
+ /^\/+|\/+$/g,
111
+ '',
112
+ )
113
+ return `${base}/${prefix}`
114
+ }
115
+
97
116
  /** Host discovery route suffix. */
98
117
  export type HostRoute = 'info' | 'capabilities'
99
118
 
package/src/index.ts CHANGED
@@ -72,7 +72,7 @@ export type {
72
72
  PcbPad,
73
73
  PcbPoint,
74
74
  PcbSegment,
75
- PcbSelection,
75
+ PcbSnapshot,
76
76
  PcbShape,
77
77
  PcbText,
78
78
  PcbTrack,
@@ -112,6 +112,32 @@ declare module '@deepseek-ai/cordis' {
112
112
  const COMPONENT = 'dsh-eda-host'
113
113
  const log = getLogger(COMPONENT)
114
114
 
115
+ /**
116
+ * Env vars relevant to the hq-edge / DSH / KiCad integration, captured at
117
+ * plugin apply() time so startup issues are diagnosable from the log.
118
+ *
119
+ * Values are logged verbatim EXCEPT `KICAD_API_TOKEN`, which is a live
120
+ * credential KiCad injects for the IPC socket — only its presence is logged
121
+ * (never the token itself).
122
+ */
123
+ const DSH_ENV_LOG_KEYS = [
124
+ 'DSH_KICAD_PYTHON',
125
+ 'DSH_KICAD_SKILLS_DIR',
126
+ 'DSH_HOME',
127
+ 'HQ_EDGE_BASE_URL',
128
+ 'KICAD_API_SOCKET',
129
+ 'KICAD_API_TOKEN',
130
+ ] as const
131
+
132
+ function envSnapshot(): Record<string, string | null> {
133
+ const snapshot: Record<string, string | null> = {}
134
+ for (const key of DSH_ENV_LOG_KEYS) {
135
+ const value = process.env[key]
136
+ snapshot[key] = key === 'KICAD_API_TOKEN' ? (value ? '<set>' : null) : (value ?? null)
137
+ }
138
+ return snapshot
139
+ }
140
+
115
141
  // Emitted on import, before any Cordis dependency is resolved. Pairs with the
116
142
  // "node half ready" marker in apply(): if this line is logged but that one is
117
143
  // not, the edge-bridge never provided `hqEdge` and this plugin is still
@@ -182,6 +208,7 @@ export function apply(ctx: Context, config: Partial<EdaHostConfig> = {}): () =>
182
208
  netlistPathPrefix: resolved.netlistPathPrefix,
183
209
  hostPathPrefix: resolved.hostPathPrefix,
184
210
  requestTimeoutMs: resolved.requestTimeoutMs,
211
+ env: envSnapshot(),
185
212
  })
186
213
 
187
214
  const client = createEdaHostClient(resolved, { baseUrlResolver: getHqEdgeBaseUrl })
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 PcbSelection, type SchematicNetlist } from './types.js'
27
+ import { NetlistError, type EdaHostCapability, type EdaHostInfo, type PcbSnapshot, 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 }
@@ -63,7 +63,11 @@ type HostCapabilitiesResult =
63
63
  | { ok: false; error: { kind: string; message: string } }
64
64
 
65
65
  type PcbSelectionResult =
66
- | { ok: true; selection: PcbSelection }
66
+ | { ok: true; selection: PcbSnapshot }
67
+ | { ok: false; error: { kind: string; message: string } }
68
+
69
+ type PcbBoardResult =
70
+ | { ok: true; snapshot: PcbSnapshot }
67
71
  | { ok: false; error: { kind: string; message: string } }
68
72
 
69
73
  /**
@@ -179,6 +183,9 @@ export function createNetListTools(env: NetlistToolEnv) {
179
183
  `Use it when you need factual information about the current EDA environment, such as ` +
180
184
  `"what EDA host am I connected to", "what version is it", "where is it installed", ` +
181
185
  `or "where is kicad-cli". ` +
186
+ `When the host is KiCad, the executable list also carries "kicad-python" — the ` +
187
+ `interpreter bundled with KiCad that owns the official kicad-python package ` +
188
+ `(kipy); the dsh-kicad skills already run with it automatically. ` +
182
189
  `The returned information is authoritative host-provided ground truth. ` +
183
190
  ERROR_SEMANTICS,
184
191
  parameters: {},
@@ -226,6 +233,37 @@ export function createNetListTools(env: NetlistToolEnv) {
226
233
  },
227
234
  }),
228
235
 
236
+ defineTool({
237
+ name: 'get_pcb_board',
238
+ description:
239
+ `Read the COMPLETE semantic PCB board from the current PCB editor through hq-edge ` +
240
+ `(DSH → dsh-eda-host → hq-edge → EDA host). Returns { ok, snapshot: { footprints[], ` +
241
+ `pads[], tracks[], arcs[], vias[], zones[], shapes[], texts[], dimensions[], groups[], ` +
242
+ `nets[] } } — the same shape as get_pcb_selection, but covering every object on the ` +
243
+ `board regardless of selection. ` +
244
+ `Each footprint has reference, value, footprint, position {x,y} in mm, rotationDeg and ` +
245
+ `pads[] (pin, type, shape, position, widthMm, heightMm, rotationDeg, layer, net{name, ` +
246
+ `code}); tracks have layer, start/end in mm, widthMm, lengthMm and net; vias have ` +
247
+ `layers[], drillMm, viaType and start/end; zones have layer, net and outline segments. ` +
248
+ `Every object carries id — the EDA-host native object identity. ` +
249
+ `IMPORTANT: ok:true with all-empty arrays is a VALID empty board — do not treat it as ` +
250
+ `a failure. ` +
251
+ ERROR_SEMANTICS,
252
+ parameters: {},
253
+ output: { schema: { type: 'json' }, render: renderJson },
254
+ async execute(_args: unknown, exec: ToolExecLike): Promise<Json> {
255
+ try {
256
+ const options: EdaHostRequestOptions = exec?.signal
257
+ ? { signal: exec.signal }
258
+ : {}
259
+ const snapshot = await env.client.getPcbBoard(options)
260
+ return asJson<PcbBoardResult>({ ok: true, snapshot })
261
+ } catch (err) {
262
+ return asJson<PcbBoardResult>({ ok: false, error: failureOf(err) })
263
+ }
264
+ },
265
+ }),
266
+
229
267
  defineTool({
230
268
  name: 'get_eda_host_capabilities',
231
269
  description:
package/src/types.ts CHANGED
@@ -55,12 +55,13 @@ export interface SchematicNetlist {
55
55
  }
56
56
 
57
57
  /**
58
- * Semantic PCB selection types.
58
+ * Semantic PCB snapshot types.
59
59
  *
60
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.
61
+ * protobuf contract for `PcbQueryService.GetSelection` / `GetBoard`). The
62
+ * same PcbSnapshot shape backs both the current selection and the complete
63
+ * board. Units follow the hq.pcb.v1 convention: positions/sizes/lengths in mm,
64
+ * rotations in degrees, `id` is the KiCad native object identity.
64
65
  */
65
66
 
66
67
  export interface PcbPoint {
@@ -172,7 +173,7 @@ export interface PcbGroup {
172
173
  itemIds: string[]
173
174
  }
174
175
 
175
- export interface PcbSelection {
176
+ export interface PcbSnapshot {
176
177
  footprints: PcbFootprint[]
177
178
  pads: PcbPad[]
178
179
  tracks: PcbTrack[]