@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/lib/index.mjs
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import { getLogger } from "@huaqiu/dsh-plugin-log";
|
|
2
2
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
3
4
|
const SCOPE_ROUTE = {
|
|
4
5
|
project: "/project",
|
|
5
6
|
selection: "/selection",
|
|
6
7
|
"active-page": "/active-page"
|
|
7
8
|
};
|
|
8
9
|
function resolveEdaHostConfig(config, env = process.env) {
|
|
10
|
+
const baseUrl = config?.hqEdgeBaseUrl ?? env.HQ_EDGE_BASE_URL ?? "";
|
|
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";
|
|
13
|
+
const hostPrefix = config?.hostPathPrefix ?? env.HQ_EDGE_HOST_PATH ?? "/api/v1/host";
|
|
14
|
+
const timeoutRaw = config?.requestTimeoutMs ?? env.HQ_EDGE_REQUEST_TIMEOUT_MS;
|
|
15
|
+
const timeout = Number.parseInt(String(timeoutRaw ?? ""), 10);
|
|
9
16
|
return {
|
|
10
|
-
hqEdgeBaseUrl:
|
|
11
|
-
netlistPathPrefix:
|
|
17
|
+
hqEdgeBaseUrl: baseUrl,
|
|
18
|
+
netlistPathPrefix: pathPrefix,
|
|
19
|
+
pcbSelectionPathPrefix: pcbSelectionPrefix,
|
|
20
|
+
hostPathPrefix: hostPrefix,
|
|
21
|
+
requestTimeoutMs: Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_REQUEST_TIMEOUT_MS
|
|
12
22
|
};
|
|
13
23
|
}
|
|
14
24
|
/** True when a host base URL is available (host mode). */
|
|
@@ -19,6 +29,18 @@ function hasHost(config) {
|
|
|
19
29
|
function netlistUrlOf(config, scope) {
|
|
20
30
|
return `${(config.hqEdgeBaseUrl ?? "").replace(/\/+$/, "")}/${(config.netlistPathPrefix ?? "/api/v1/netlist").replace(/^\/+|\/+$/g, "")}${SCOPE_ROUTE[scope]}`;
|
|
21
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
|
+
}
|
|
36
|
+
const HOST_ROUTE = {
|
|
37
|
+
info: "/info",
|
|
38
|
+
capabilities: "/capabilities"
|
|
39
|
+
};
|
|
40
|
+
/** Build the absolute URL for one host discovery route. */
|
|
41
|
+
function hostUrlOf(config, route) {
|
|
42
|
+
return `${(config.hqEdgeBaseUrl ?? "").replace(/\/+$/, "")}/${(config.hostPathPrefix ?? "/api/v1/host").replace(/^\/+|\/+$/g, "")}${HOST_ROUTE[route]}`;
|
|
43
|
+
}
|
|
22
44
|
//#endregion
|
|
23
45
|
//#region src/types.ts
|
|
24
46
|
var NetlistError = class extends Error {
|
|
@@ -32,38 +54,128 @@ var NetlistError = class extends Error {
|
|
|
32
54
|
//#endregion
|
|
33
55
|
//#region src/client.ts
|
|
34
56
|
/**
|
|
35
|
-
*
|
|
57
|
+
* Transport for `@huaqiu/dsh-eda-host`.
|
|
36
58
|
*
|
|
37
59
|
* DSH → dsh-eda-host → hq-edge → EDA Host is the ONLY production path. This
|
|
38
|
-
* module fetches the semantic netlist
|
|
39
|
-
* maps
|
|
40
|
-
*
|
|
60
|
+
* module fetches the semantic netlist and the EDA-independent host information
|
|
61
|
+
* from hq-edge, maps HTTP statuses back to the semantic gRPC error categories,
|
|
62
|
+
* and enforces the single end-to-end request budget. It never parses schematic
|
|
63
|
+
* files and never touches KiCad.
|
|
41
64
|
*
|
|
42
65
|
* @module
|
|
43
66
|
*/
|
|
44
|
-
/** HTTP status → semantic error kind (see routes/
|
|
67
|
+
/** HTTP status → semantic error kind (see routes/edaHostStatus.ts on hq-edge). */
|
|
45
68
|
function statusToKind(status) {
|
|
46
69
|
if (status === 412) return "FAILED_PRECONDITION";
|
|
47
70
|
if (status === 501) return "UNIMPLEMENTED";
|
|
48
71
|
if (status === 503) return "UNAVAILABLE";
|
|
72
|
+
if (status === 504) return "DEADLINE_EXCEEDED";
|
|
49
73
|
return "INTERNAL";
|
|
50
74
|
}
|
|
75
|
+
/** True when a thrown fetch error is an abort/timeout rather than a transport error. */
|
|
76
|
+
function isAbortError(err) {
|
|
77
|
+
const name = err?.name;
|
|
78
|
+
return name === "AbortError" || name === "TimeoutError";
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Combine an optional caller signal with the plugin's own budget.
|
|
82
|
+
*
|
|
83
|
+
* `AbortSignal.any` is not available on every Node version DSH may run on, so
|
|
84
|
+
* fall back to whichever signal exists — the budget is always present, which is
|
|
85
|
+
* what guarantees no request can wait indefinitely.
|
|
86
|
+
*/
|
|
87
|
+
function resolveSignal(deadlineMs, caller) {
|
|
88
|
+
const signals = [caller, deadlineMs > 0 ? AbortSignal.timeout(deadlineMs) : void 0].filter((s) => Boolean(s));
|
|
89
|
+
if (signals.length === 0) return void 0;
|
|
90
|
+
if (signals.length === 1) return signals[0];
|
|
91
|
+
const anyFn = AbortSignal.any;
|
|
92
|
+
return typeof anyFn === "function" ? anyFn.call(AbortSignal, signals) : signals[0];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Extract the semantic `SchematicNetlist` from an hq-edge netlist body.
|
|
96
|
+
*
|
|
97
|
+
* hq-edge now emits a single-level body: `{ netlist: { components, nets } }`.
|
|
98
|
+
* Older hq-edge builds serialized the protobuf envelope
|
|
99
|
+
* (`GetNetListResponse.oneof result`), which produced a second `netlist` level
|
|
100
|
+
* and made every populated design look empty. That legacy shape is unwrapped
|
|
101
|
+
* EXPLICITLY — not silently — and anything else is a hard error, because
|
|
102
|
+
* "malformed" must never masquerade as "empty design".
|
|
103
|
+
*/
|
|
104
|
+
function parseNetlistBody(body) {
|
|
105
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) throw new NetlistError("INTERNAL", "eda-host: malformed netlist response from hq-edge");
|
|
106
|
+
let candidate = body.netlist;
|
|
107
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate) && candidate.components === void 0 && typeof candidate.netlist === "object") candidate = candidate.netlist;
|
|
108
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) throw new NetlistError("INTERNAL", "eda-host: malformed netlist response from hq-edge");
|
|
109
|
+
const { components, nets } = candidate;
|
|
110
|
+
if (components !== void 0 && !Array.isArray(components)) throw new NetlistError("INTERNAL", "eda-host: netlist.components is not an array");
|
|
111
|
+
if (nets !== void 0 && !Array.isArray(nets)) throw new NetlistError("INTERNAL", "eda-host: netlist.nets is not an array");
|
|
112
|
+
return {
|
|
113
|
+
components: components ?? [],
|
|
114
|
+
nets: nets ?? []
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function str(value) {
|
|
118
|
+
return typeof value === "string" ? value : "";
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Extract a complete `EdaHostInfo` from an hq-edge host-info body.
|
|
122
|
+
*
|
|
123
|
+
* `hq.host.v1` has no availability flags, so the only way to distinguish "the
|
|
124
|
+
* host is here" from "the host is not" is whether this call succeeded at all.
|
|
125
|
+
* That makes the *shape* the contract: proto3 JSON omits default-valued fields,
|
|
126
|
+
* so a host that legitimately reports nothing arrives as `{}`. Rather than let
|
|
127
|
+
* a half-empty object reach the agent — where a missing field could be misread
|
|
128
|
+
* as "not available" — absent values are filled with their proto3 defaults.
|
|
129
|
+
*
|
|
130
|
+
* Values that are present are never reinterpreted; unknown `hostType` strings
|
|
131
|
+
* pass through so a newer host cannot be silently downgraded.
|
|
132
|
+
*/
|
|
133
|
+
function parseEdaHostInfo(value) {
|
|
134
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new NetlistError("INTERNAL", "eda-host: malformed host info response from hq-edge");
|
|
135
|
+
const raw = value;
|
|
136
|
+
const executables = Array.isArray(raw.installation?.executables) ? (raw.installation?.executables).filter((e) => Boolean(e) && typeof e === "object").map((e) => ({
|
|
137
|
+
name: str(e.name),
|
|
138
|
+
path: str(e.path)
|
|
139
|
+
})) : [];
|
|
140
|
+
return {
|
|
141
|
+
identity: {
|
|
142
|
+
hostType: typeof raw.identity?.hostType === "string" ? raw.identity.hostType : "EDA_HOST_TYPE_UNSPECIFIED",
|
|
143
|
+
hostName: str(raw.identity?.hostName),
|
|
144
|
+
version: str(raw.identity?.version)
|
|
145
|
+
},
|
|
146
|
+
installation: {
|
|
147
|
+
applicationPath: str(raw.installation?.applicationPath),
|
|
148
|
+
executables
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
51
152
|
function createEdaHostClient(config, deps = {}) {
|
|
52
153
|
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
53
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the host endpoint per request. A resolver (ctx.hqEdge) wins over
|
|
156
|
+
* the static config/env value; if neither yields a URL we degrade to the
|
|
157
|
+
* same clear FAILED_PRECONDITION the standalone install path uses.
|
|
158
|
+
*/
|
|
159
|
+
function resolveConfig() {
|
|
54
160
|
const baseUrl = deps.baseUrlResolver?.()?.trim() ?? config.hqEdgeBaseUrl?.trim() ?? "";
|
|
55
|
-
if (baseUrl.length === 0) throw new NetlistError("FAILED_PRECONDITION", "eda-host: no hq-edge base URL configured (hqEdgeBaseUrl / HQ_EDGE_BASE_URL) —
|
|
56
|
-
|
|
161
|
+
if (baseUrl.length === 0) throw new NetlistError("FAILED_PRECONDITION", "eda-host: no hq-edge base URL configured (ctx.hqEdge.baseUrl / hqEdgeBaseUrl / HQ_EDGE_BASE_URL) — EDA host tools require the hq-edge bridge.");
|
|
162
|
+
return {
|
|
57
163
|
...config,
|
|
58
164
|
hqEdgeBaseUrl: baseUrl
|
|
59
|
-
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** Perform one GET and return the parsed JSON body, mapping failures. */
|
|
168
|
+
async function getJson(url, options, what) {
|
|
169
|
+
const signal = resolveSignal(config.requestTimeoutMs ?? 3e4, options?.signal);
|
|
60
170
|
let response;
|
|
61
171
|
try {
|
|
62
172
|
response = await fetchImpl(url, {
|
|
63
173
|
method: "GET",
|
|
64
|
-
headers: { Accept: "application/json" }
|
|
174
|
+
headers: { Accept: "application/json" },
|
|
175
|
+
...signal ? { signal } : {}
|
|
65
176
|
});
|
|
66
177
|
} catch (err) {
|
|
178
|
+
if (isAbortError(err)) throw new NetlistError("DEADLINE_EXCEEDED", `eda-host: ${what} request exceeded its time budget at ${url}`);
|
|
67
179
|
throw new NetlistError("UNAVAILABLE", `eda-host: cannot reach hq-edge at ${url}: ${String(err?.message ?? err)}`);
|
|
68
180
|
}
|
|
69
181
|
if (!response.ok) {
|
|
@@ -72,19 +184,47 @@ function createEdaHostClient(config, deps = {}) {
|
|
|
72
184
|
const body = await response.json();
|
|
73
185
|
if (typeof body.detail === "string") detail = body.detail;
|
|
74
186
|
} catch {}
|
|
75
|
-
throw new NetlistError(statusToKind(response.status), `eda-host:
|
|
187
|
+
throw new NetlistError(statusToKind(response.status), `eda-host: ${what} request failed (${response.status}${detail ? `: ${detail}` : ""})`);
|
|
76
188
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
189
|
+
return response.json();
|
|
190
|
+
}
|
|
191
|
+
async function fetchScope(scope, options) {
|
|
192
|
+
return parseNetlistBody(await getJson(netlistUrlOf(resolveConfig(), scope), options, "netlist"));
|
|
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
|
+
};
|
|
83
210
|
}
|
|
84
211
|
return {
|
|
85
|
-
getSelectionNetlist: () => fetchScope("selection"),
|
|
86
|
-
getProjectNetlist: () => fetchScope("project"),
|
|
87
|
-
getActivePageNetlist: () => fetchScope("active-page")
|
|
212
|
+
getSelectionNetlist: (options) => fetchScope("selection", options),
|
|
213
|
+
getProjectNetlist: (options) => fetchScope("project", options),
|
|
214
|
+
getActivePageNetlist: (options) => fetchScope("active-page", options),
|
|
215
|
+
getEdaHostInfo: async (options) => {
|
|
216
|
+
const body = await getJson(hostUrlOf(resolveConfig(), "info"), options, "host info");
|
|
217
|
+
if (!body || typeof body.info !== "object" || body.info === null) throw new NetlistError("INTERNAL", "eda-host: malformed host info response from hq-edge");
|
|
218
|
+
return parseEdaHostInfo(body.info);
|
|
219
|
+
},
|
|
220
|
+
getEdaHostCapabilities: async (options) => {
|
|
221
|
+
const body = await getJson(hostUrlOf(resolveConfig(), "capabilities"), options, "host capabilities");
|
|
222
|
+
if (!body || !Array.isArray(body.capabilities)) throw new NetlistError("INTERNAL", "eda-host: malformed host capabilities response from hq-edge");
|
|
223
|
+
return body.capabilities.filter((c) => typeof c === "string");
|
|
224
|
+
},
|
|
225
|
+
getPcbSelection: async (options) => {
|
|
226
|
+
return parsePcbSelection(await getJson(pcbSelectionUrlOf(resolveConfig()), options, "pcb selection"));
|
|
227
|
+
}
|
|
88
228
|
};
|
|
89
229
|
}
|
|
90
230
|
//#endregion
|
|
@@ -92,17 +232,23 @@ function createEdaHostClient(config, deps = {}) {
|
|
|
92
232
|
/**
|
|
93
233
|
* Agent tools for `@huaqiu/dsh-eda-host`.
|
|
94
234
|
*
|
|
95
|
-
* Three semantic operations, one per
|
|
235
|
+
* Three semantic netlist operations, one per scope:
|
|
96
236
|
*
|
|
97
237
|
* get_project_netlist complete logical netlist of the current project
|
|
98
238
|
* get_selection_netlist netlist of the currently selected components
|
|
99
239
|
* get_active_page_netlist netlist of the active schematic page
|
|
100
240
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
241
|
+
* Two EDA host discovery operations:
|
|
242
|
+
*
|
|
243
|
+
* get_eda_host_info which host, which version, where it is installed
|
|
244
|
+
* get_eda_host_capabilities what the current host can actually do
|
|
245
|
+
*
|
|
246
|
+
* The netlist tools are pure pass-throughs: they call the hq-edge netlist
|
|
247
|
+
* router and return the semantic `SchematicNetlist` as lossless JSON. Errors
|
|
248
|
+
* are propagated with a semantic `kind` (FAILED_PRECONDITION / UNIMPLEMENTED /
|
|
249
|
+
* INTERNAL / UNAVAILABLE / DEADLINE_EXCEEDED) — never converted into a fake
|
|
250
|
+
* empty netlist. A valid-but-empty netlist is `ok: true` with empty
|
|
251
|
+
* `components`/`nets`.
|
|
106
252
|
*
|
|
107
253
|
* @module
|
|
108
254
|
*/
|
|
@@ -116,12 +262,25 @@ function renderJson(_args, value) {
|
|
|
116
262
|
text: JSON.stringify(value)
|
|
117
263
|
}];
|
|
118
264
|
}
|
|
119
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Every failure kind, and what the agent should do about it.
|
|
267
|
+
*
|
|
268
|
+
* Shared by all tools so the prompt contract cannot drift between them.
|
|
269
|
+
*/
|
|
270
|
+
const ERROR_SEMANTICS = "IMPORTANT SEMANTICS: on ok:false, error.kind distinguishes the cause: \"FAILED_PRECONDITION\" (no EDA host / no live editor — ask the user to open the design in the EDA editor first, then retry), \"UNIMPLEMENTED\" (this capability is not supported by the current host — do NOT retry; report it to the user), \"UNAVAILABLE\" (hq-edge / EDA host unreachable), \"DEADLINE_EXCEEDED\" (the host did not answer in time — retry once, then report), \"INTERNAL\" (host-side failure). Do NOT fabricate data.";
|
|
271
|
+
function failureOf(err) {
|
|
272
|
+
return {
|
|
273
|
+
kind: err instanceof NetlistError ? err.kind : "INTERNAL",
|
|
274
|
+
message: String(err?.message ?? err)
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
async function runScope(env, scope, exec) {
|
|
278
|
+
const options = exec?.signal ? { signal: exec.signal } : {};
|
|
120
279
|
try {
|
|
121
280
|
let netlist;
|
|
122
|
-
if (scope === "project") netlist = await env.client.getProjectNetlist();
|
|
123
|
-
else if (scope === "selection") netlist = await env.client.getSelectionNetlist();
|
|
124
|
-
else netlist = await env.client.getActivePageNetlist();
|
|
281
|
+
if (scope === "project") netlist = await env.client.getProjectNetlist(options);
|
|
282
|
+
else if (scope === "selection") netlist = await env.client.getSelectionNetlist(options);
|
|
283
|
+
else netlist = await env.client.getActivePageNetlist(options);
|
|
125
284
|
return {
|
|
126
285
|
ok: true,
|
|
127
286
|
scope,
|
|
@@ -131,15 +290,12 @@ async function runScope(env, scope) {
|
|
|
131
290
|
return {
|
|
132
291
|
ok: false,
|
|
133
292
|
scope,
|
|
134
|
-
error:
|
|
135
|
-
kind: err instanceof NetlistError ? err.kind : "INTERNAL",
|
|
136
|
-
message: String(err?.message ?? err)
|
|
137
|
-
}
|
|
293
|
+
error: failureOf(err)
|
|
138
294
|
};
|
|
139
295
|
}
|
|
140
296
|
}
|
|
141
297
|
function scopeDescription(scope, extra) {
|
|
142
|
-
return "Read the current schematic netlist from the EDA host through hq-edge (DSH → dsh-eda-host → hq-edge → EDA host). " + extra + " The result is a semantic netlist JSON: { ok, scope, netlist: { components[], nets[] } }. Each component has referenceDesignators[], value, manufacturerPartNumber, footprint, description and pins[] (pinNumber, pinName, electricalType); each net has name and pinReferences[] (referenceDesignator, pinNumber). IMPORTANT
|
|
298
|
+
return "Read the current schematic netlist from the EDA host through hq-edge (DSH → dsh-eda-host → hq-edge → EDA host). " + extra + " The result is a semantic netlist JSON: { ok, scope, netlist: { components[], nets[] } }. Each component has referenceDesignators[], value, manufacturerPartNumber, footprint, description and pins[] (pinNumber, pinName, electricalType); each net has name and pinReferences[] (referenceDesignator, pinNumber). IMPORTANT: ok:true with empty components/nets is a VALID empty design — do not treat it as a failure. IMPORTANT SEMANTICS: on ok:false, error.kind distinguishes the cause: \"FAILED_PRECONDITION\" (no EDA host / no live editor — ask the user to open the design in the EDA editor first, then retry), \"UNIMPLEMENTED\" (this capability is not supported by the current host — do NOT retry; report it to the user), \"UNAVAILABLE\" (hq-edge / EDA host unreachable), \"DEADLINE_EXCEEDED\" (the host did not answer in time — retry once, then report), \"INTERNAL\" (host-side failure). Do NOT fabricate data.";
|
|
143
299
|
}
|
|
144
300
|
function createNetListTools(env) {
|
|
145
301
|
const mkTool = (scope, name, desc) => defineTool({
|
|
@@ -150,14 +306,94 @@ function createNetListTools(env) {
|
|
|
150
306
|
schema: { type: "json" },
|
|
151
307
|
render: renderJson
|
|
152
308
|
},
|
|
153
|
-
async execute(_args,
|
|
154
|
-
return asJson(await runScope(env, scope));
|
|
309
|
+
async execute(_args, exec) {
|
|
310
|
+
return asJson(await runScope(env, scope, exec));
|
|
155
311
|
}
|
|
156
312
|
});
|
|
157
313
|
return [
|
|
158
314
|
mkTool("project", "get_project_netlist", scopeDescription("project", "Returns the complete logical netlist for the current project (all sheets, all nets).")),
|
|
159
315
|
mkTool("selection", "get_selection_netlist", scopeDescription("selection", "Returns the netlist associated with the currently selected schematic components, including the nets they participate in (each net lists every connected pin, not only the selected ones).")),
|
|
160
|
-
mkTool("active_page", "get_active_page_netlist", scopeDescription("active_page", "Returns the netlist for the currently active schematic page. NOTE: KiCad host does not implement this scope — expect ok:false with error.kind \"UNIMPLEMENTED\"."))
|
|
316
|
+
mkTool("active_page", "get_active_page_netlist", scopeDescription("active_page", "Returns the netlist for the currently active schematic page. NOTE: the KiCad host does not implement this scope — expect ok:false with error.kind \"UNIMPLEMENTED\". Check get_eda_host_capabilities before relying on it."))
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* EDA host discovery tools.
|
|
321
|
+
*
|
|
322
|
+
* These expose what the EDA host ALREADY knows and can ALREADY do — they
|
|
323
|
+
* implement no EDA functionality themselves. A capability being advertised is
|
|
324
|
+
* a claim that the host can provide it, nothing more.
|
|
325
|
+
*/
|
|
326
|
+
function createEdaHostTools(env) {
|
|
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
|
+
}
|
|
349
|
+
}
|
|
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
|
+
}
|
|
372
|
+
}
|
|
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
|
+
})
|
|
161
397
|
];
|
|
162
398
|
}
|
|
163
399
|
//#endregion
|
|
@@ -182,6 +418,7 @@ const name = "@huaqiu/dsh-eda-host";
|
|
|
182
418
|
*/
|
|
183
419
|
const inject = ["hqEdge", "tools"];
|
|
184
420
|
const log = getLogger("dsh-eda-host");
|
|
421
|
+
log.info("dsh-eda-host: module loaded (waiting for the hqEdge + tools services)");
|
|
185
422
|
/**
|
|
186
423
|
* Host plugin body — provide `edaHost` and register the three netlist tools.
|
|
187
424
|
*
|
|
@@ -201,22 +438,26 @@ const log = getLogger("dsh-eda-host");
|
|
|
201
438
|
function apply(ctx, config = {}) {
|
|
202
439
|
if (!ctx.tools || typeof ctx.tools.register !== "function") throw new Error("@huaqiu/dsh-eda-host requires the DSH `tools` service (ctx.tools.register).");
|
|
203
440
|
const resolved = resolveEdaHostConfig(config);
|
|
441
|
+
const hq = ctx.hqEdge;
|
|
442
|
+
if (!hq || typeof hq.baseUrl !== "string" || hq.baseUrl.trim().length === 0) throw new Error("@huaqiu/dsh-eda-host requires a usable hq-edge context: the edge-bridge plugin did not provide ctx.hqEdge.baseUrl. EDA host tools cannot work without hq-edge — check that the bridge started and that the HQ Edge port is valid.");
|
|
204
443
|
const getHqEdgeBaseUrl = () => {
|
|
205
|
-
const
|
|
206
|
-
return
|
|
444
|
+
const current = ctx.hqEdge;
|
|
445
|
+
return current?.baseUrl && current.baseUrl.trim().length > 0 ? current.baseUrl : void 0;
|
|
207
446
|
};
|
|
208
447
|
log.info("applying dsh-eda-host node half", {
|
|
209
448
|
hasConfigHost: hasHost(resolved),
|
|
210
449
|
hqEdgeBaseUrlFromConfig: resolved.hqEdgeBaseUrl ?? null,
|
|
211
|
-
netlistPathPrefix: resolved.netlistPathPrefix
|
|
450
|
+
netlistPathPrefix: resolved.netlistPathPrefix,
|
|
451
|
+
hostPathPrefix: resolved.hostPathPrefix,
|
|
452
|
+
requestTimeoutMs: resolved.requestTimeoutMs
|
|
212
453
|
});
|
|
213
454
|
const client = createEdaHostClient(resolved, { baseUrlResolver: getHqEdgeBaseUrl });
|
|
214
455
|
ctx.effect(() => ctx.provide("edaHost", client));
|
|
215
|
-
const tools = createNetListTools({ client });
|
|
456
|
+
const tools = [...createNetListTools({ client }), ...createEdaHostTools({ client })];
|
|
216
457
|
const disposers = [];
|
|
217
458
|
for (const tool of tools) disposers.push(ctx.tools.register(tool));
|
|
218
459
|
log.info("dsh-eda-host node half ready", {
|
|
219
|
-
tools:
|
|
460
|
+
tools: tools.length,
|
|
220
461
|
configHostMode: hasHost(resolved)
|
|
221
462
|
});
|
|
222
463
|
return () => {
|
|
@@ -226,4 +467,4 @@ function apply(ctx, config = {}) {
|
|
|
226
467
|
};
|
|
227
468
|
}
|
|
228
469
|
//#endregion
|
|
229
|
-
export { NetlistError, apply, inject, name };
|
|
470
|
+
export { NetlistError, apply, inject, name, parseNetlistBody };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huaqiu/dsh-eda-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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.
|
|
25
|
+
"@huaqiu/dsh-plugin-log": "0.4.1"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"lib",
|