@huaqiu/dsh-eda-host 0.3.22 → 0.3.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts CHANGED
@@ -1,17 +1,20 @@
1
1
  /**
2
- * Netlist transport for `@huaqiu/dsh-eda-host`.
2
+ * Transport for `@huaqiu/dsh-eda-host`.
3
3
  *
4
4
  * DSH → dsh-eda-host → hq-edge → EDA Host is the ONLY production path. This
5
- * module fetches the semantic netlist from the hq-edge netlist router and
6
- * maps the HTTP status back to the semantic gRPC error categories. It never
7
- * parses schematic files and never touches KiCad.
5
+ * module fetches the semantic netlist and the EDA-independent host information
6
+ * from hq-edge, maps HTTP statuses back to the semantic gRPC error categories,
7
+ * and enforces the single end-to-end request budget. It never parses schematic
8
+ * files and never touches KiCad.
8
9
  *
9
10
  * @module
10
11
  */
11
12
 
12
- import { netlistUrlOf, type EdaHostConfig, type NetlistScope } from './config.js'
13
+ import { DEFAULT_REQUEST_TIMEOUT_MS, hostUrlOf, netlistUrlOf, type EdaHostConfig, type NetlistScope } from './config.js'
13
14
  import {
14
15
  NetlistError,
16
+ type EdaHostCapability,
17
+ type EdaHostInfo,
15
18
  type SchematicNetlist,
16
19
  } from './types.js'
17
20
 
@@ -29,49 +32,201 @@ export interface EdaHostClientDeps {
29
32
  baseUrlResolver?: () => string | undefined
30
33
  }
31
34
 
35
+ /** Per-call options. `signal` lets DSH cancel a request (see §6 of the task). */
36
+ export interface EdaHostRequestOptions {
37
+ signal?: AbortSignal
38
+ }
39
+
32
40
  export interface EdaHostClient {
33
41
  /** Netlist of the currently selected components. */
34
- getSelectionNetlist(): Promise<SchematicNetlist>
42
+ getSelectionNetlist(options?: EdaHostRequestOptions): Promise<SchematicNetlist>
35
43
  /** Complete logical netlist for the current project. */
36
- getProjectNetlist(): Promise<SchematicNetlist>
44
+ getProjectNetlist(options?: EdaHostRequestOptions): Promise<SchematicNetlist>
37
45
  /** Netlist of the current active schematic page. */
38
- getActivePageNetlist(): Promise<SchematicNetlist>
46
+ getActivePageNetlist(options?: EdaHostRequestOptions): Promise<SchematicNetlist>
47
+ /**
48
+ * EDA-independent identity / installation of the host. Presence of a result
49
+ * is the availability signal — `hq.host.v1` has no availability field.
50
+ */
51
+ getEdaHostInfo(options?: EdaHostRequestOptions): Promise<EdaHostInfo>
52
+ /** Capabilities the host currently provides. */
53
+ getEdaHostCapabilities(options?: EdaHostRequestOptions): Promise<EdaHostCapability[]>
39
54
  }
40
55
 
41
- /** HTTP status → semantic error kind (see routes/netlist.ts on hq-edge). */
56
+ /** HTTP status → semantic error kind (see routes/edaHostStatus.ts on hq-edge). */
42
57
  function statusToKind(status: number): NetlistError['kind'] {
43
58
  if (status === 412) return 'FAILED_PRECONDITION'
44
59
  if (status === 501) return 'UNIMPLEMENTED'
45
60
  if (status === 503) return 'UNAVAILABLE'
61
+ if (status === 504) return 'DEADLINE_EXCEEDED'
46
62
  return 'INTERNAL'
47
63
  }
48
64
 
65
+ /** True when a thrown fetch error is an abort/timeout rather than a transport error. */
66
+ function isAbortError(err: unknown): boolean {
67
+ const name = (err as Error | undefined)?.name
68
+ return name === 'AbortError' || name === 'TimeoutError'
69
+ }
70
+
71
+ /**
72
+ * Combine an optional caller signal with the plugin's own budget.
73
+ *
74
+ * `AbortSignal.any` is not available on every Node version DSH may run on, so
75
+ * fall back to whichever signal exists — the budget is always present, which is
76
+ * what guarantees no request can wait indefinitely.
77
+ */
78
+ function resolveSignal(deadlineMs: number, caller?: AbortSignal): AbortSignal | undefined {
79
+ const budget = deadlineMs > 0 ? AbortSignal.timeout(deadlineMs) : undefined
80
+ const signals = [caller, budget].filter((s): s is AbortSignal => Boolean(s))
81
+ if (signals.length === 0) return undefined
82
+ if (signals.length === 1) return signals[0]
83
+ const anyFn = (AbortSignal as unknown as { any?: (s: AbortSignal[]) => AbortSignal }).any
84
+ return typeof anyFn === 'function' ? anyFn.call(AbortSignal, signals) : signals[0]
85
+ }
86
+
87
+ /**
88
+ * Extract the semantic `SchematicNetlist` from an hq-edge netlist body.
89
+ *
90
+ * hq-edge now emits a single-level body: `{ netlist: { components, nets } }`.
91
+ * Older hq-edge builds serialized the protobuf envelope
92
+ * (`GetNetListResponse.oneof result`), which produced a second `netlist` level
93
+ * and made every populated design look empty. That legacy shape is unwrapped
94
+ * EXPLICITLY — not silently — and anything else is a hard error, because
95
+ * "malformed" must never masquerade as "empty design".
96
+ */
97
+ export function parseNetlistBody(body: unknown): SchematicNetlist {
98
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
99
+ throw new NetlistError('INTERNAL', 'eda-host: malformed netlist response from hq-edge')
100
+ }
101
+
102
+ let candidate: unknown = (body as { netlist?: unknown }).netlist
103
+
104
+ if (
105
+ candidate &&
106
+ typeof candidate === 'object' &&
107
+ !Array.isArray(candidate) &&
108
+ (candidate as { components?: unknown }).components === undefined &&
109
+ typeof (candidate as { netlist?: unknown }).netlist === 'object'
110
+ ) {
111
+ // Legacy double-nested envelope — unwrap exactly one level.
112
+ candidate = (candidate as { netlist: unknown }).netlist
113
+ }
114
+
115
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
116
+ throw new NetlistError('INTERNAL', 'eda-host: malformed netlist response from hq-edge')
117
+ }
118
+
119
+ const { components, nets } = candidate as { components?: unknown; nets?: unknown }
120
+
121
+ // proto3 JSON omits empty arrays, so `undefined` is a legitimate empty list.
122
+ if (components !== undefined && !Array.isArray(components)) {
123
+ throw new NetlistError('INTERNAL', 'eda-host: netlist.components is not an array')
124
+ }
125
+ if (nets !== undefined && !Array.isArray(nets)) {
126
+ throw new NetlistError('INTERNAL', 'eda-host: netlist.nets is not an array')
127
+ }
128
+
129
+ return {
130
+ components: (components ?? []) as SchematicNetlist['components'],
131
+ nets: (nets ?? []) as SchematicNetlist['nets'],
132
+ }
133
+ }
134
+
135
+ function str(value: unknown): string {
136
+ return typeof value === 'string' ? value : ''
137
+ }
138
+
139
+ /**
140
+ * Extract a complete `EdaHostInfo` from an hq-edge host-info body.
141
+ *
142
+ * `hq.host.v1` has no availability flags, so the only way to distinguish "the
143
+ * host is here" from "the host is not" is whether this call succeeded at all.
144
+ * That makes the *shape* the contract: proto3 JSON omits default-valued fields,
145
+ * so a host that legitimately reports nothing arrives as `{}`. Rather than let
146
+ * a half-empty object reach the agent — where a missing field could be misread
147
+ * as "not available" — absent values are filled with their proto3 defaults.
148
+ *
149
+ * Values that are present are never reinterpreted; unknown `hostType` strings
150
+ * pass through so a newer host cannot be silently downgraded.
151
+ */
152
+ export function parseEdaHostInfo(value: unknown): EdaHostInfo {
153
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
154
+ throw new NetlistError('INTERNAL', 'eda-host: malformed host info response from hq-edge')
155
+ }
156
+
157
+ const raw = value as {
158
+ identity?: { hostType?: unknown; hostName?: unknown; version?: unknown }
159
+ installation?: { applicationPath?: unknown; executables?: unknown }
160
+ }
161
+
162
+ const executables = Array.isArray(raw.installation?.executables)
163
+ ? (raw.installation?.executables as unknown[])
164
+ .filter((e): e is Record<string, unknown> => Boolean(e) && typeof e === 'object')
165
+ .map((e) => ({ name: str(e.name), path: str(e.path) }))
166
+ : []
167
+
168
+ return {
169
+ identity: {
170
+ hostType:
171
+ typeof raw.identity?.hostType === 'string'
172
+ ? (raw.identity.hostType as EdaHostInfo['identity']['hostType'])
173
+ : 'EDA_HOST_TYPE_UNSPECIFIED',
174
+ hostName: str(raw.identity?.hostName),
175
+ version: str(raw.identity?.version),
176
+ },
177
+ installation: {
178
+ applicationPath: str(raw.installation?.applicationPath),
179
+ executables,
180
+ },
181
+ }
182
+ }
183
+
49
184
  export function createEdaHostClient(
50
185
  config: EdaHostConfig,
51
186
  deps: EdaHostClientDeps = {},
52
187
  ): EdaHostClient {
53
188
  const fetchImpl = deps.fetchImpl ?? globalThis.fetch
54
189
 
55
- async function fetchScope(scope: NetlistScope): Promise<SchematicNetlist> {
56
- // Resolve the host endpoint per request. A resolver (ctx.hqEdge) wins over
57
- // the static config/env value; if neither yields a URL we degrade to the
58
- // same clear FAILED_PRECONDITION the standalone install path uses.
59
- const baseUrl = deps.baseUrlResolver?.()?.trim()
60
- ?? config.hqEdgeBaseUrl?.trim()
61
- ?? ''
190
+ /**
191
+ * Resolve the host endpoint per request. A resolver (ctx.hqEdge) wins over
192
+ * the static config/env value; if neither yields a URL we degrade to the
193
+ * same clear FAILED_PRECONDITION the standalone install path uses.
194
+ */
195
+ function resolveConfig(): EdaHostConfig {
196
+ const baseUrl = deps.baseUrlResolver?.()?.trim() ?? config.hqEdgeBaseUrl?.trim() ?? ''
62
197
  if (baseUrl.length === 0) {
63
198
  throw new NetlistError(
64
199
  'FAILED_PRECONDITION',
65
- 'eda-host: no hq-edge base URL configured (hqEdgeBaseUrl / HQ_EDGE_BASE_URL) ' +
66
- 'netlist tools require the hq-edge EDA host bridge.',
200
+ 'eda-host: no hq-edge base URL configured (ctx.hqEdge.baseUrl / hqEdgeBaseUrl / ' +
201
+ 'HQ_EDGE_BASE_URL) — EDA host tools require the hq-edge bridge.',
67
202
  )
68
203
  }
69
- const url = netlistUrlOf({ ...config, hqEdgeBaseUrl: baseUrl }, scope)
204
+ return { ...config, hqEdgeBaseUrl: baseUrl }
205
+ }
206
+
207
+ /** Perform one GET and return the parsed JSON body, mapping failures. */
208
+ async function getJson(
209
+ url: string,
210
+ options: EdaHostRequestOptions | undefined,
211
+ what: string,
212
+ ): Promise<unknown> {
213
+ const signal = resolveSignal(config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, options?.signal)
70
214
 
71
215
  let response: Response
72
216
  try {
73
- response = await fetchImpl(url, { method: 'GET', headers: { Accept: 'application/json' } })
217
+ response = await fetchImpl(url, {
218
+ method: 'GET',
219
+ headers: { Accept: 'application/json' },
220
+ ...(signal ? { signal } : {}),
221
+ })
74
222
  } catch (err) {
223
+ // An aborted request is a timeout, never an empty result.
224
+ if (isAbortError(err)) {
225
+ throw new NetlistError(
226
+ 'DEADLINE_EXCEEDED',
227
+ `eda-host: ${what} request exceeded its time budget at ${url}`,
228
+ )
229
+ }
75
230
  // Connection-level failure: host not running / unreachable.
76
231
  throw new NetlistError(
77
232
  'UNAVAILABLE',
@@ -89,25 +244,54 @@ export function createEdaHostClient(
89
244
  }
90
245
  throw new NetlistError(
91
246
  statusToKind(response.status),
92
- `eda-host: netlist request failed (${response.status}${detail ? `: ${detail}` : ''})`,
247
+ `eda-host: ${what} request failed (${response.status}${detail ? `: ${detail}` : ''})`,
93
248
  )
94
249
  }
95
250
 
96
- // Valid (possibly empty) result: { netlist: { components, nets } }.
97
- const body = (await response.json()) as { netlist?: SchematicNetlist }
98
- if (!body || typeof body.netlist !== 'object' || body.netlist === null) {
99
- throw new NetlistError('INTERNAL', 'eda-host: malformed netlist response from hq-edge')
100
- }
251
+ return response.json()
252
+ }
101
253
 
102
- const netlist = body.netlist
103
- netlist.components ??= []
104
- netlist.nets ??= []
105
- return netlist
254
+ async function fetchScope(
255
+ scope: NetlistScope,
256
+ options?: EdaHostRequestOptions,
257
+ ): Promise<SchematicNetlist> {
258
+ const resolved = resolveConfig()
259
+ const url = netlistUrlOf(resolved, scope)
260
+ return parseNetlistBody(await getJson(url, options, 'netlist'))
106
261
  }
107
262
 
108
263
  return {
109
- getSelectionNetlist: () => fetchScope('selection'),
110
- getProjectNetlist: () => fetchScope('project'),
111
- getActivePageNetlist: () => fetchScope('active-page'),
264
+ getSelectionNetlist: (options) => fetchScope('selection', options),
265
+ getProjectNetlist: (options) => fetchScope('project', options),
266
+ getActivePageNetlist: (options) => fetchScope('active-page', options),
267
+
268
+ getEdaHostInfo: async (options) => {
269
+ const resolved = resolveConfig()
270
+ const url = hostUrlOf(resolved, 'info')
271
+ const body = (await getJson(url, options, 'host info')) as { info?: unknown }
272
+
273
+ if (!body || typeof body.info !== 'object' || body.info === null) {
274
+ throw new NetlistError('INTERNAL', 'eda-host: malformed host info response from hq-edge')
275
+ }
276
+ return parseEdaHostInfo(body.info)
277
+ },
278
+
279
+ getEdaHostCapabilities: async (options) => {
280
+ const resolved = resolveConfig()
281
+ const url = hostUrlOf(resolved, 'capabilities')
282
+ const body = (await getJson(url, options, 'host capabilities')) as {
283
+ capabilities?: unknown
284
+ }
285
+
286
+ if (!body || !Array.isArray(body.capabilities)) {
287
+ throw new NetlistError(
288
+ 'INTERNAL',
289
+ 'eda-host: malformed host capabilities response from hq-edge',
290
+ )
291
+ }
292
+ return body.capabilities.filter(
293
+ (c): c is EdaHostCapability => typeof c === 'string',
294
+ )
295
+ },
112
296
  }
113
297
  }
package/src/config.ts CHANGED
@@ -15,10 +15,25 @@ 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 host discovery; default "/api/v1/host". */
19
+ hostPathPrefix?: string
20
+ /**
21
+ * End-to-end budget for one EDA-host request, in milliseconds.
22
+ *
23
+ * This is the SINGLE request budget for the whole chain — the plugin does
24
+ * not define separate per-layer timeouts. It is enforced here (outermost)
25
+ * and mirrored by the KiCad UI-dispatch bound, so no agent request can wait
26
+ * indefinitely. See docs/tasks/expose-capability.md §6.
27
+ */
28
+ requestTimeoutMs?: number
18
29
  }
19
30
 
20
31
  export const DEFAULT_NETLIST_PATH_PREFIX = '/api/v1/netlist'
21
32
 
33
+ export const DEFAULT_HOST_PATH_PREFIX = '/api/v1/host'
34
+
35
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000
36
+
22
37
  /** Scope → route suffix on the host netlist router. */
23
38
  export type NetlistScope = 'project' | 'selection' | 'active-page'
24
39
 
@@ -35,9 +50,16 @@ export function resolveEdaHostConfig(
35
50
  const baseUrl = config?.hqEdgeBaseUrl ?? env.HQ_EDGE_BASE_URL ?? ''
36
51
  const pathPrefix =
37
52
  config?.netlistPathPrefix ?? env.HQ_EDGE_NETLIST_PATH ?? DEFAULT_NETLIST_PATH_PREFIX
53
+ const hostPrefix =
54
+ config?.hostPathPrefix ?? env.HQ_EDGE_HOST_PATH ?? DEFAULT_HOST_PATH_PREFIX
55
+ const timeoutRaw = config?.requestTimeoutMs ?? env.HQ_EDGE_REQUEST_TIMEOUT_MS
56
+ const timeout = Number.parseInt(String(timeoutRaw ?? ''), 10)
38
57
  return {
39
58
  hqEdgeBaseUrl: baseUrl,
40
59
  netlistPathPrefix: pathPrefix,
60
+ hostPathPrefix: hostPrefix,
61
+ requestTimeoutMs:
62
+ Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_REQUEST_TIMEOUT_MS,
41
63
  }
42
64
  }
43
65
 
@@ -52,3 +74,18 @@ export function netlistUrlOf(config: EdaHostConfig, scope: NetlistScope): string
52
74
  const prefix = (config.netlistPathPrefix ?? DEFAULT_NETLIST_PATH_PREFIX).replace(/^\/+|\/+$/g, '')
53
75
  return `${base}/${prefix}${SCOPE_ROUTE[scope]}`
54
76
  }
77
+
78
+ /** Host discovery route suffix. */
79
+ export type HostRoute = 'info' | 'capabilities'
80
+
81
+ const HOST_ROUTE: Record<HostRoute, string> = {
82
+ info: '/info',
83
+ capabilities: '/capabilities',
84
+ }
85
+
86
+ /** Build the absolute URL for one host discovery route. */
87
+ export function hostUrlOf(config: EdaHostConfig, route: HostRoute): string {
88
+ const base = (config.hqEdgeBaseUrl ?? '').replace(/\/+$/, '')
89
+ const prefix = (config.hostPathPrefix ?? DEFAULT_HOST_PATH_PREFIX).replace(/^\/+|\/+$/g, '')
90
+ return `${base}/${prefix}${HOST_ROUTE[route]}`
91
+ }
package/src/index.ts CHANGED
@@ -2,11 +2,13 @@
2
2
  * `@huaqiu/dsh-eda-host` — node plugin entry.
3
3
  *
4
4
  * Provides the `edaHost` service (semantic EDA-host capability) and registers
5
- * three agent tools:
5
+ * five 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_eda_host_info which EDA host, version, installation
11
+ * get_eda_host_capabilities what the current host can actually do
10
12
  *
11
13
  * ── Architectural boundary (task: add-dsh-eda-host) ─────────────────────────
12
14
  * The ONLY production request path is DSH → dsh-eda-host → hq-edge → EDA host.
@@ -26,7 +28,7 @@ import type { Context } from '@deepseek-ai/cordis'
26
28
  import { getLogger } from '@huaqiu/dsh-plugin-log'
27
29
  import { createEdaHostClient, type EdaHostClient } from './client.js'
28
30
  import { hasHost, resolveEdaHostConfig, type EdaHostConfig } from './config.js'
29
- import { createNetListTools } from './tools.js'
31
+ import { createEdaHostTools, createNetListTools } from './tools.js'
30
32
 
31
33
  /** Plugin id — matches package.json. */
32
34
  export const name = '@huaqiu/dsh-eda-host'
@@ -50,8 +52,14 @@ export const name = '@huaqiu/dsh-eda-host'
50
52
  export const inject = ['hqEdge', 'tools'] as const
51
53
 
52
54
  export type { EdaHostConfig } from './config.js'
53
- export type { EdaHostClient } from './client.js'
55
+ export type { EdaHostClient, EdaHostRequestOptions } from './client.js'
54
56
  export type {
57
+ EdaHostCapability,
58
+ EdaHostExecutable,
59
+ EdaHostIdentity,
60
+ EdaHostInfo,
61
+ EdaHostInstallation,
62
+ EdaHostType,
55
63
  ElectricalNet,
56
64
  ElectricalType,
57
65
  NetlistErrorKind,
@@ -61,6 +69,7 @@ export type {
61
69
  SchematicNetlist,
62
70
  } from './types.js'
63
71
  export { NetlistError } from './types.js'
72
+ export { parseNetlistBody } from './client.js'
64
73
 
65
74
  declare module '@deepseek-ai/cordis' {
66
75
  interface Context {
@@ -88,6 +97,12 @@ declare module '@deepseek-ai/cordis' {
88
97
  const COMPONENT = 'dsh-eda-host'
89
98
  const log = getLogger(COMPONENT)
90
99
 
100
+ // Emitted on import, before any Cordis dependency is resolved. Pairs with the
101
+ // "node half ready" marker in apply(): if this line is logged but that one is
102
+ // not, the edge-bridge never provided `hqEdge` and this plugin is still
103
+ // pending — which is otherwise completely silent.
104
+ log.info('dsh-eda-host: module loaded (waiting for the hqEdge + tools services)')
105
+
91
106
  /**
92
107
  * Host plugin body — provide `edaHost` and register the three netlist tools.
93
108
  *
@@ -113,30 +128,61 @@ export function apply(ctx: Context, config: Partial<EdaHostConfig> = {}): () =>
113
128
 
114
129
  const resolved = resolveEdaHostConfig(config)
115
130
 
131
+ // ── Explicit bridge dependency ───────────────────────────────────────────
132
+ // `hqEdge` is a REQUIRED inject, so Cordis only calls `apply()` once the
133
+ // edge-bridge has provided it. That means a missing bridge would otherwise
134
+ // leave this plugin pending forever with the tools never registered and
135
+ // nothing logged. Two things make that diagnosable:
136
+ //
137
+ // 1. `apply()` asserts the bridge actually gave us a usable endpoint and
138
+ // THROWS when it did not — a loud startup failure beats five tools that
139
+ // fail one by one at call time.
140
+ // 2. The module-level marker below is emitted on import. If
141
+ // "dsh-eda-host: module loaded" appears in the log but
142
+ // "dsh-eda-host: node half ready" never does, the bridge never provided
143
+ // `hqEdge` and this plugin is still pending.
144
+ //
145
+ // hq-edge is NOT optional here — this plugin must never become a standalone
146
+ // DSH plugin (docs/tasks/expose-capability.md §3, §7).
147
+ const hq = ctx.hqEdge
148
+ if (!hq || typeof hq.baseUrl !== 'string' || hq.baseUrl.trim().length === 0) {
149
+ throw new Error(
150
+ '@huaqiu/dsh-eda-host requires a usable hq-edge context: the edge-bridge ' +
151
+ 'plugin did not provide ctx.hqEdge.baseUrl. EDA host tools cannot work ' +
152
+ 'without hq-edge — check that the bridge started and that the HQ Edge ' +
153
+ 'port is valid.',
154
+ )
155
+ }
156
+
116
157
  // Late-bound host endpoint: prefer the edge-bridge service, then the overlay
117
158
  // config / env value. Consulted on every request (see client.ts).
118
159
  const getHqEdgeBaseUrl = (): string | undefined => {
119
- const hq = ctx.hqEdge
120
- return hq?.baseUrl && hq.baseUrl.trim().length > 0 ? hq.baseUrl : undefined
160
+ const current = ctx.hqEdge
161
+ return current?.baseUrl && current.baseUrl.trim().length > 0 ? current.baseUrl : undefined
121
162
  }
122
163
 
123
164
  log.info('applying dsh-eda-host node half', {
124
165
  hasConfigHost: hasHost(resolved),
125
166
  hqEdgeBaseUrlFromConfig: resolved.hqEdgeBaseUrl ?? null,
126
167
  netlistPathPrefix: resolved.netlistPathPrefix,
168
+ hostPathPrefix: resolved.hostPathPrefix,
169
+ requestTimeoutMs: resolved.requestTimeoutMs,
127
170
  })
128
171
 
129
172
  const client = createEdaHostClient(resolved, { baseUrlResolver: getHqEdgeBaseUrl })
130
173
 
131
174
  ctx.effect(() => ctx.provide('edaHost', client))
132
175
 
133
- const tools = createNetListTools({ client })
176
+ const tools = [...createNetListTools({ client }), ...createEdaHostTools({ client })]
134
177
  const disposers: Array<() => void> = []
135
178
  for (const tool of tools) {
136
179
  disposers.push(ctx.tools.register(tool))
137
180
  }
138
181
 
139
- log.info('dsh-eda-host node half ready', { tools: 3, configHostMode: hasHost(resolved) })
182
+ log.info('dsh-eda-host node half ready', {
183
+ tools: tools.length,
184
+ configHostMode: hasHost(resolved),
185
+ })
140
186
 
141
187
  return () => {
142
188
  for (const dispose of disposers) {