dsh-eda-mcp 0.7.1 → 0.7.3

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/bridge.mjs CHANGED
@@ -293,11 +293,50 @@ var BOARD_OUTLINE_WIDTH_MIL = 10;
293
293
  var SILKSCREEN_HEIGHT_MIN_MM = 1;
294
294
  var SILKSCREEN_WIDTH_MIN_MM = 0.15;
295
295
 
296
+ // src/bridge/workflow-guide.ts
297
+ var WORKFLOW_SUMMARY = [
298
+ "\u539F\u7406\u56FE\u5DE5\u4F5C\u89C4\u8303\uFF08\u9ED8\u8BA4\u5168\u90E8\u624B\u5DE5\uFF0C\u7981\u6B62\u56FE\u7701\u4E8B\u7528\u81EA\u52A8\u5E03\u5C40/\u81EA\u52A8\u5E03\u7EBF\uFF09\uFF1A",
299
+ "1) \u5E03\u5C40\uFF1A\u6309 10 \u5355\u4F4D\u6805\u683C\u5BF9\u9F50\uFF1B\u4FE1\u53F7\u4ECE\u5DE6\u5230\u53F3\u3001\u7535\u6E90\u5728\u4E0A\u3001\u5730\u5728\u4E0B\uFF1B\u540C\u7EA7\u5668\u4EF6\u7B49\u95F4\u8DDD\uFF1B\u4E0D\u538B\u5B57\u3001\u4E0D\u538B\u6807\u9898\u680F\uFF1B\u5148\u770B eda_get_page_info \u7684\u5B89\u5168\u533A\u518D\u6446\u4F4D\u3002",
300
+ "2) \u8FDE\u7EBF\uFF1A\u53EA\u7528 eda_draw_wire \u8D70\u76F4\u7EBF/45 \u5EA6\uFF1B**\u8DE8\u533A\u57DF\u540C\u540D\u7F51\u7EDC\u5FC5\u987B\u7528\u7F51\u7EDC\u7AEF\u53E3\uFF08eda_place_net_port\uFF09\u6216\u7F51\u7EDC\u6807\u7B7E**\uFF0C\u4E0D\u80FD\u9760\u5728\u4E24\u5904\u753B\u4E00\u6761\u53EB\u540C\u540D\u7684\u5BFC\u7EBF\u2014\u2014\u5BFC\u7EBF\u4E00\u65E6\u63A5\u89E6\u5F15\u811A\uFF0Cnet \u4F1A\u88AB\u8FDE\u901A\u6027\u91CD\u7B97\uFF0C\u6307\u5B9A\u540D\u4F1A\u88AB\u8986\u76D6\u3002",
301
+ "3) \u7F51\u7EDC\u6807\u7B7E\uFF1A\u5148\u8BD5 eda_place_net_label\uFF1B\u82E5\u8BE5\u6784\u5EFA\u7684 createNetLabel \u4E0D\u54CD\u5E94\uFF0C\u4F1A\u81EA\u52A8\u964D\u7EA7\u4E3A\u7EAF\u6587\u672C\u6807\u7B7E\uFF08\u65E0\u7535\u6C14\u8BED\u4E49\uFF09\uFF0C\u6B64\u65F6\u9760 stub \u5BFC\u7EBF\u627F\u8F7D\u7F51\u7EDC\uFF1B\u6279\u91CF\u7528 eda_place_net_labels \u4E00\u6B21\u8C03\u7528\uFF0C\u4E0D\u8981\u4E32\u884C\u4E0A\u767E\u6B21\u3002",
302
+ "4) \u6821\u9A8C\uFF1Aeda_export_netlist \u770B\u6BCF\u4E2A\u7F51\u7EDC\u7684\u7AEF\u70B9\u662F\u5426 \u22652\uFF1Beda_run_drc \u770B\u9010\u6761 DRC \u660E\u7EC6\uFF1B\u89C6\u89C9\u68C0\u67E5\u4F18\u5148\u5BFC\u51FA PNG \u81EA\u884C\u8BC6\u522B\uFF0C\u800C\u4E0D\u662F\u8BA9 EDA \u91CD\u6392\u3002",
303
+ "5) \u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42\u65F6\u624D eda_auto_layout / eda_auto_route\uFF08\u9700 force:true \u4E14\u8BBE\u7F6E\u91CC\u653E\u884C\uFF09\u3002",
304
+ "6) \u5F15\u811A\u4FE1\u606F\u7528 eda_get_pins \u4E00\u6B21\u62FF\u5168\uFF08\u542B pinName/pinNumber\uFF09\uFF0C\u4E0D\u8981\u9010\u811A\u8C03 raw API\u3002"
305
+ ].join("\n");
306
+ var WORKFLOW_GUIDE = WORKFLOW_SUMMARY + "\n\n" + [
307
+ "## \u5E03\u5C40\u7EC6\u5219\u662F\u600E\u4E48\u843D\u5730\u7684",
308
+ "",
309
+ "- \u5750\u6807\u5355\u4F4D\u662F 0.01 \u82F1\u5BF8\uFF08MCP \u5355\u4F4D\uFF09\uFF1BA4 \u56FE\u6846 0..1170 / 0..825\uFF0C\u6807\u9898\u680F\u5360 right 460..1160 / bottom 10..190\uFF0C\u5B89\u5168\u533A\u89C1 eda_get_page_info\u3002",
310
+ "- \u4E00\u9875\u53EA\u653E\u4E00\u4E2A\u529F\u80FD\u5757\uFF1B\u5668\u4EF6\u6309\u4FE1\u53F7\u6D41\u4ECE\u5DE6\u5230\u53F3\u6392\uFF0C\u53CD\u9988\u5143\u4EF6\u653E\u5728\u653E\u5927\u5668\u4E0B\u65B9\uFF0C\u53BB\u8026\u7535\u5BB9\u8D34\u8FD1\u7535\u6E90\u811A\u3002",
311
+ "- \u5668\u4EF6\u4E4B\u95F4\u7559\u51FA\u81F3\u5C11 20 \u5355\u4F4D\u7528\u4E8E\u8D70\u7EBF\u548C\u6807\u7B7E\uFF1B\u7F51\u7EDC\u7AEF\u53E3\u653E\u5728\u6A21\u5757\u8FB9\u7F18\uFF0C\u5DE6\u53F3\u5404\u4E00\u5217\u3002",
312
+ "- \u6446\u597D\u4E00\u4E2A\u6A21\u5757\u518D\u6446\u4E0B\u4E00\u4E2A\uFF1B\u6BCF\u6B21\u6446\u5B8C\u7528 eda_get_components \u68C0\u67E5\u6CA1\u6709\u91CD\u53E0\uFF08\u5305\u56F4\u76D2\u76F8\u4EA4\uFF09\u3002",
313
+ "",
314
+ "## \u7F51\u7EDC\u4E0E\u6807\u7B7E",
315
+ "",
316
+ "- eda_place_net_port \u4EA7\u751F\u771F\u6B63\u7684\u7F51\u7EDC\u7AEF\u53E3\u56FE\u5143\uFF0C\u662F\u8DE8\u533A\u57DF\u8FDE\u63A5\u7684\u9996\u9009\u3002",
317
+ "- eda_place_net_label \u4F18\u5148\u8C03 sch_PrimitiveAttribute.createNetLabel\uFF1B\u8BE5 API \u5728 3.2.186 \u4E0A\u4E0D\u54CD\u5E94\uFF0C\u63A2\u6D4B\u7ED3\u8BBA\u6309\u4F1A\u8BDD\u7F13\u5B58\uFF0C\u4E4B\u540E\u76F4\u63A5\u964D\u7EA7\u4E3A\u6587\u672C\u6807\u7B7E\uFF08\u4E0D\u518D\u6BCF\u6B21\u4ED8 4 \u79D2\u8D85\u65F6\uFF09\u3002",
318
+ "- \u964D\u7EA7\u540E\u7684\u6587\u672C\u6807\u7B7E\u6CA1\u6709\u7535\u6C14\u8BED\u4E49\uFF1A\u8FDE\u901A\u6027\u7531 eda_place_net_label_at_pin \u540C\u65F6\u753B\u7684 stub \u5BFC\u7EBF\u627F\u62C5\u3002",
319
+ "- eda_get_net_labels \u4F1A\u540C\u65F6\u5217\u51FA\u5C5E\u6027\u56FE\u5143\uFF08netlabel/netport/netflag\uFF09\u548C\u6587\u672C\u6807\u7B7E\uFF0C\u5220\u9664\u7528 eda_delete_primitive \u7684\u5BF9\u5E94 kind\u3002",
320
+ "- \u6279\u91CF\u653E\u7F6E\u7528 eda_place_net_labels([{pinPrimitiveId, net}, ...])\uFF0C\u4E00\u6B21\u8C03\u7528\u5B8C\u6210\u5E76\u9010\u6761\u56DE\u62A5\u6210\u8D25\u3002",
321
+ "",
322
+ "## \u5BFC\u7EBF net \u88AB\u8986\u76D6\uFF08\u52A1\u5FC5\u77E5\u9053\uFF09",
323
+ "",
324
+ "eda_draw_wire \u4F20\u7684 net \u53EA\u5BF9**\u60AC\u7A7A\u5BFC\u7EBF**\u6709\u6548\uFF1B\u5BFC\u7EBF\u7AEF\u70B9\u4E00\u65E6\u843D\u5728\u5F15\u811A/\u7F51\u7EDC\u4E0A\uFF0CEDA \u4F1A\u6309\u8FDE\u901A\u6027\u91CD\u7B97\u7F51\u7EDC\u540D\uFF08\u5B9E\u6D4B TESTNET \u5E76\u5165\u4E86 LED_A\uFF09\u3002\u6240\u4EE5\u201C\u540C\u540D\u5BFC\u7EBF = \u540C\u540D\u7F51\u7EDC\u201D\u4E0D\u6210\u7ACB\uFF0C\u8DE8\u533A\u57DF\u540C\u540D\u5FC5\u987B\u7528\u7F51\u7EDC\u6807\u7B7E\u6216\u7F51\u7EDC\u7AEF\u53E3\u3002",
325
+ "",
326
+ "## \u6821\u9A8C\u987A\u5E8F",
327
+ "",
328
+ "1. eda_get_active_document \u786E\u8BA4\u5728\u539F\u7406\u56FE\u9875\uFF08\u4E0D\u662F HOME/PCB\uFF09\u3002",
329
+ "2. eda_export_netlist\uFF08\u5DF2\u89E3\u6790\u6210\u5BF9\u8C61\uFF09\u68C0\u67E5\u6BCF\u4E2A\u7F51\u7EDC \u22652 \u7AEF\u70B9\u3001\u65E0\u60AC\u7A7A\u3002",
330
+ "3. eda_run_drc \u770B\u9010\u6761\u660E\u7EC6\uFF08\u542B primitiveId \u4E0E\u5750\u6807\uFF09\u3002",
331
+ "4. \u9700\u8981\u4EBA\u770B\u6548\u679C\u5C31\u5BFC\u51FA\u56FE\u7247\u81EA\u884C\u8BC6\u522B\uFF0C\u4E0D\u8981\u7528\u81EA\u52A8\u5E03\u5C40\u201C\u4FEE\u201D\u7248\u9762\u3002"
332
+ ].join("\n");
333
+
296
334
  // src/bridge/index.ts
297
- var VERSION = "0.4.0";
335
+ var VERSION = "0.4.2";
298
336
  var WS_PORT = Number(process.env.DSH_EDA_MCP_WS_PORT ?? 39009);
299
337
  var WS_HOST = "127.0.0.1";
300
338
  var ALLOW_RAW_API = (process.env.DSH_EDA_MCP_ALLOW_RAW_API ?? "1") !== "0";
339
+ var ALLOW_AUTO_LAYOUT = (process.env.DSH_EDA_ALLOW_AUTO_LAYOUT ?? "0") === "1";
301
340
  var TOOL_TIMEOUT_MS = Number(process.env.DSH_EDA_MCP_TOOL_TIMEOUT_MS ?? 6e4);
302
341
  var MCP_UNITS_PER_INCH = 100;
303
342
  var PAPER_SIZES = {
@@ -321,6 +360,18 @@ function safeAreas(paperName) {
321
360
  lowerLeft: { left: 40, bottom: 40, right: paper.titleBlock.left - 20, top: paper.titleBlock.top - 10 }
322
361
  };
323
362
  }
363
+ function decodeTextPayload(value) {
364
+ const text = typeof value === "string" ? value : "";
365
+ if (text === "") return text;
366
+ if (text.charCodeAt(0) === 65279) return text.slice(1);
367
+ if (text.charCodeAt(0) === 255 && text.charCodeAt(1) === 254) {
368
+ return Buffer.from(text.slice(2), "latin1").toString("utf16le");
369
+ }
370
+ if (text.length > 3 && text.charCodeAt(1) === 0 && text.charCodeAt(3) === 0) {
371
+ return Buffer.from(text, "latin1").toString("utf16le");
372
+ }
373
+ return text;
374
+ }
324
375
  function checkPointInFrame(x, y, paperName, margin = 0) {
325
376
  const paper = paperOf(paperName);
326
377
  const f = paper.frame;
@@ -539,11 +590,11 @@ var TOOLS = [
539
590
  }, ["libraryUuid", "uuid", "x", "y"]) },
540
591
  { name: "eda_place_net_flag", description: "Place a power/ground net flag (VCC/GND etc.) on the active schematic page.", inputSchema: objectSchema({ identification: { type: "string", enum: ["Power", "Ground", "AnalogGround", "ProtectGround"] }, net: { type: "string" }, x: { type: "number" }, y: { type: "number" }, rotation: { type: "integer", enum: [0, 90, 180, 270] }, mirror: { type: "boolean" }, paper: { type: "string" }, allowOutsideFrame: { type: "boolean" } }, ["identification", "net", "x", "y"]) },
541
592
  { name: "eda_place_net_port", description: "Place a net port on the active schematic page.", inputSchema: objectSchema({ direction: { type: "string", enum: ["IN", "OUT", "BI"] }, net: { type: "string" }, x: { type: "number" }, y: { type: "number" }, rotation: { type: "integer", enum: [0, 90, 180, 270] }, mirror: { type: "boolean" }, paper: { type: "string" }, allowOutsideFrame: { type: "boolean" } }, ["direction", "net", "x", "y"]) },
542
- { name: "eda_get_pins", description: "List pins of one schematic component. Use the returned primitiveId with eda_place_net_flag_at_pin, eda_place_net_label_at_pin, or eda_set_no_connect.", inputSchema: objectSchema({ componentPrimitiveId: { type: "string" } }, ["componentPrimitiveId"]) },
593
+ { name: "eda_get_pins", description: "List pins of one schematic component in a single round trip, including pinName and pinNumber (no per-pin sch_PrimitivePin.get needed). Use the returned primitiveId with eda_place_net_flag_at_pin, eda_place_net_label_at_pin, or eda_set_no_connect.", inputSchema: objectSchema({ componentPrimitiveId: { type: "string" } }, ["componentPrimitiveId"]) },
543
594
  { name: "eda_place_net_flag_at_pin", description: "Place a power/ground net flag at a pin WITHOUT covering the symbol: the pin direction is read from the pin rotation, a stub wire of stubLength MCP units (default 20) is drawn outwards from the pin with the net name, and the flag is placed at its end. Pass offsetX/offsetY to place it manually (that disables the stub) or stubLength=0 for the old on-pin behaviour.", inputSchema: objectSchema({ pinPrimitiveId: { type: "string" }, identification: { type: "string", enum: ["Power", "Ground", "AnalogGround", "ProtectGround"] }, net: { type: "string" }, stubLength: { type: "number" }, offsetX: { type: "number" }, offsetY: { type: "number" }, rotation: { type: "integer", enum: [0, 90, 180, 270] }, mirror: { type: "boolean" }, color: { type: "string" }, lineWidth: { type: "integer" }, lineType: { type: "integer" }, paper: { type: "string" }, allowOutsideFrame: { type: "boolean" } }, ["pinPrimitiveId", "identification", "net"]) },
544
595
  { name: "eda_place_net_label_at_pin", description: "Place a REAL net label at a pin (sch_PrimitiveAttribute.createNetLabel; falls back to text and reports method) WITHOUT covering the symbol: a stub wire of stubLength MCP units (default 20) is drawn outwards along the pin direction and the label goes at its end. offsetX/offsetY place it manually (disables the stub); stubLength=0 keeps the old on-pin behaviour.", inputSchema: objectSchema({ pinPrimitiveId: { type: "string" }, net: { type: "string" }, stubLength: { type: "number" }, offsetX: { type: "number" }, offsetY: { type: "number" }, rotation: { type: "integer", enum: [0, 90, 180, 270] }, fontSize: { type: "number" }, color: { type: "string" }, lineWidth: { type: "integer" }, lineType: { type: "integer" }, paper: { type: "string" }, allowOutsideFrame: { type: "boolean" } }, ["pinPrimitiveId", "net"]) },
545
596
  { name: "eda_set_no_connect", description: "Set or clear the no-connect flag on one or more schematic pins via sch_PrimitivePin.modify.", inputSchema: objectSchema({ primitiveIds: { type: "array", items: { type: "string" } }, noConnected: { type: "boolean" } }, ["primitiveIds"]) },
546
- { name: "eda_draw_wire", description: "Draw a schematic wire (polyline). Coordinates are MCP units (0.01 inch).", inputSchema: objectSchema({ points: { type: "array", items: { type: "number" } }, net: { type: "string" }, color: { type: "string" }, lineWidth: { type: "integer", minimum: 1, maximum: 10 }, lineType: { type: "integer", enum: [0, 1, 2, 3] } }, ["points"]) },
597
+ { name: "eda_draw_wire", description: "Draw a schematic wire (polyline). Coordinates are MCP units (0.01 inch). IMPORTANT: the net argument only sticks on a floating wire \u2014 as soon as an endpoint touches a pin, EDA recomputes the net from connectivity and the name you passed is overwritten (measured: TESTNET merged into LED_A). To join same-named nets across regions use eda_place_net_port or a net label; do not rely on two distantly drawn wires sharing a name.", inputSchema: objectSchema({ points: { type: "array", items: { type: "number" } }, net: { type: "string" }, color: { type: "string" }, lineWidth: { type: "integer", minimum: 1, maximum: 10 }, lineType: { type: "integer", enum: [0, 1, 2, 3] } }, ["points"]) },
547
598
  { name: "eda_draw_rectangle", description: "Draw a rectangle. topLeftY is the UPPER y coordinate (max y); height extends downward. Coordinates are MCP units.", inputSchema: objectSchema({ topLeftX: { type: "number" }, topLeftY: { type: "number" }, width: { type: "number" }, height: { type: "number" }, cornerRadius: { type: "number" }, rotation: { type: "integer", enum: [0, 90, 180, 270] }, color: { type: "string" }, fillColor: { type: "string" }, lineWidth: { type: "integer", minimum: 1, maximum: 10 }, lineType: { type: "integer", enum: [0, 1, 2, 3] }, fillStyle: { type: "string" } }, ["topLeftX", "topLeftY", "width", "height"]) },
548
599
  { name: "eda_draw_functional_box", description: "Draw a dashed functional-group box around one group of components, with an optional label. Margins and coordinates are MCP units; default margin 1 unit (about 0.254 mm).", inputSchema: objectSchema({ componentIds: { type: "array", items: { type: "string" } }, label: { type: "string" }, margin: { type: "number" }, color: { type: "string" }, lineWidth: { type: "integer", minimum: 1, maximum: 10 }, lineType: { type: "integer", enum: [0, 1, 2, 3] } }) },
549
600
  { name: "eda_draw_circle", description: "Draw a circle. Coordinates are MCP units.", inputSchema: objectSchema({ centerX: { type: "number" }, centerY: { type: "number" }, radius: { type: "number" }, color: { type: "string" }, fillColor: { type: "string" }, lineWidth: { type: "integer", minimum: 1, maximum: 10 }, lineType: { type: "integer", enum: [0, 1, 2, 3] }, fillStyle: { type: "string" } }, ["centerX", "centerY", "radius"]) },
@@ -556,14 +607,18 @@ var TOOLS = [
556
607
  { name: "eda_get_nets", description: "List nets of the current schematic project. Uses sch_Net.* first; when the schematic is not the active document those APIs return [] and this tool falls back to parsing sch_Netlist.getNetlist, reporting which source was used.", inputSchema: objectSchema({ name: { type: "string" }, parseNetlist: { type: "boolean" } }) },
557
608
  { name: "eda_get_active_document", description: "Return the document/tab that is currently active (documentType, uuid, tabId, parent project). Most tab-dependent APIs (sch_Net.*, netlist export, DRC) only work when the matching document is active - use eda_open_document to switch.", inputSchema: objectSchema({}) },
558
609
  { name: "eda_open_document", description: "Open/activate a document tab by uuid (dmt_EditorControl.openDocument). Works for a schematic page or a PCB, so tab-dependent APIs can run.", inputSchema: objectSchema({ documentUuid: { type: "string" }, splitScreenId: { type: "string" } }, ["documentUuid"]) },
559
- { name: "eda_get_net_labels", description: "List net-label / attribute primitives on the active schematic page (sch_PrimitiveAttribute.getAll), optionally for one parent primitive.", inputSchema: objectSchema({ parentPrimitiveId: { type: "string" } }) },
560
- { name: "eda_place_net_label", description: "Place a real net label at x/y via sch_PrimitiveAttribute.createNetLabel (falls back to a plain text label when that API is missing, and reports which method was used). Coordinates are MCP units.", inputSchema: objectSchema({ x: { type: "number" }, y: { type: "number" }, net: { type: "string" }, fontSize: { type: "number" }, rotation: { type: "integer", enum: [0, 90, 180, 270] } }, ["x", "y", "net"]) },
561
- { name: "eda_run_drc", description: "Run schematic DRC and return the verbose result array.", inputSchema: objectSchema({ strict: { type: "boolean" } }) },
562
- { name: "eda_auto_layout", description: "Run schematic auto-layout (sch_Document.autoLayout).", inputSchema: objectSchema({ uuids: { type: "array", items: { type: "string" } } }) },
563
- { name: "eda_auto_route", description: "Run schematic auto-routing (sch_Document.autoRouting).", inputSchema: objectSchema({ uuids: { type: "array", items: { type: "string" } } }) },
610
+ { name: "eda_get_net_labels", description: "List every label-like primitive on the active schematic page: attribute primitives (netlabel/netport/netflag) PLUS plain texts left by the createNetLabel fallback, so they can be audited and deleted. Reports kind/primitiveId/net per entry.", inputSchema: objectSchema({ parentPrimitiveId: { type: "string" } }) },
611
+ { name: "eda_place_net_label", description: "Place a net label at x/y. The real API (sch_PrimitiveAttribute.createNetLabel) is probed once per session; when the build never answers, a plain TEXT label is placed instead, which carries no electrical meaning and must be paired with a stub wire. Pass probe:true to re-check.", inputSchema: objectSchema({ x: { type: "number" }, y: { type: "number" }, net: { type: "string" }, fontSize: { type: "number" }, rotation: { type: "integer", enum: [0, 90, 180, 270] } }, ["x", "y", "net"]) },
612
+ { name: "eda_place_net_labels", description: "Place many net labels in ONE call (per-pin or by coordinate) and report done/failed per item: a 133-label schematic used to exceed the caller timeout with no progress. items: [{ pinPrimitiveId, net, stubLength?, offsetX?, offsetY? }] or [{ x, y, net, fontSize? }].", inputSchema: objectSchema({ items: { type: "array", items: { type: "object", additionalProperties: true } }, probe: { type: "boolean" }, concurrency: { type: "integer", minimum: 1, maximum: 8 } }, ["items"]) },
613
+ { name: "eda_run_drc", description: 'Run schematic DRC with includeVerboseError. On EDA 3.2.186 the app still returns AGGREGATED entries ({type:"warn",count:N}) with no primitiveId/coordinates, so entries are reported as-is and raw keeps the untouched payload; expect counts plus a hint until the app exposes details.', inputSchema: objectSchema({ strict: { type: "boolean" } }) },
614
+ { name: "eda_auto_layout", description: "Run schematic auto-layout (sch_Document.autoLayout). DISABLED by default: automated layout is not used unless the user asks for it. Pass force:true only then.", inputSchema: objectSchema({ uuids: { type: "array", items: { type: "string" } }, force: { type: "boolean" } }) },
615
+ { name: "eda_auto_route", description: "Run schematic auto-routing (sch_Document.autoRouting). DISABLED by default: route by hand unless the user explicitly asks. Pass force:true only then.", inputSchema: objectSchema({ uuids: { type: "array", items: { type: "string" } }, force: { type: "boolean" } }) },
564
616
  { name: "eda_export_netlist", description: "Export the schematic netlist as text (default JLCEDA format). Returns null when no schematic is active - check eda_get_active_document / open the schematic page first.", inputSchema: objectSchema({ type: { type: "string" } }) },
565
- { name: "eda_export_bom", description: "Export the schematic BOM. csv returns text; xlsx returns a base64 data URL.", inputSchema: objectSchema({ format: { type: "string", enum: ["csv", "xlsx"] } }) },
566
- { name: "eda_delete_primitive", description: "Delete a schematic primitive by kind and primitive id.", inputSchema: objectSchema({ kind: { type: "string", enum: ["component", "wire", "rectangle", "circle", "arc", "polygon", "text"] }, primitiveId: { type: "string" } }, ["kind", "primitiveId"]) },
617
+ { name: "eda_export_bom", description: "Export the schematic BOM. csv is decoded from UTF-16 to readable text; xlsx returns a base64 data URL.", inputSchema: objectSchema({ format: { type: "string", enum: ["csv", "xlsx"] } }) },
618
+ { name: "eda_move_component", description: "Move/rotate/mirror a schematic component (sch_PrimitiveComponent.modify) without deleting and re-placing it, so the designator and value survive.", inputSchema: objectSchema({ primitiveId: { type: "string" }, x: { type: "number" }, y: { type: "number" }, rotation: { type: "number" }, mirror: { type: "boolean" } }, ["primitiveId"]) },
619
+ { name: "eda_workflow_guide", description: "Return the schematic working standard: grid-aligned manual placement, signal flow, when net labels/ports are required (a wire net name is overridden by connectivity), net-label fallback behaviour, batch placement and the verification order. Read it before laying out a sheet.", inputSchema: objectSchema({}) },
620
+ { name: "eda_list_api_methods", description: 'List every API method the connected extension accepts (its allow-list). Call it when eda_api_call answers "API method not allowed".', inputSchema: objectSchema({}) },
621
+ { name: "eda_delete_primitive", description: 'Delete a schematic primitive by kind and primitive id. Net ports, net flags and net labels are attribute primitives and need kind "netport"/"netflag"/"netlabel"; the text fallback labels are kind "text".', inputSchema: objectSchema({ kind: { type: "string", enum: ["component", "wire", "rectangle", "circle", "arc", "polygon", "text", "netport", "netflag", "netlabel"] }, primitiveId: { type: "string" } }, ["kind", "primitiveId"]) },
567
622
  { name: "eda_create_board", description: "Create a board linking a schematic and a PCB (dmt_Board.createBoard).", inputSchema: objectSchema({ schematicUuid: { type: "string" }, pcbUuid: { type: "string" } }) },
568
623
  { name: "eda_api_call", description: "Call an allow-listed JLCEDA Pro extension API method directly.", inputSchema: objectSchema({ method: { type: "string" }, args: { type: "array" } }, ["method", "args"]) },
569
624
  { name: "pcb_get_components", description: "List PCB components (mil). includePins=true also reads pcb_PrimitiveComponent.getAllPinsByPrimitiveId per component, which is the accurate pad/net source; the fast summary pad list can be stale.", inputSchema: objectSchema({ layer: { type: ["string", "integer"] }, locked: { type: "boolean" }, includeBounds: { type: "boolean" }, includePins: { type: "boolean" } }) },
@@ -747,6 +802,8 @@ var WsConnection = class {
747
802
  var clients = /* @__PURE__ */ new Set();
748
803
  var pendingCalls = /* @__PURE__ */ new Map();
749
804
  var pcbComponentBaseline = null;
805
+ var netLabelSupport = null;
806
+ var placedTextLabels = [];
750
807
  function sleep(ms) {
751
808
  return new Promise((resolve) => setTimeout(resolve, ms));
752
809
  }
@@ -1213,30 +1270,88 @@ async function parseNetlist(previous, timeoutMs) {
1213
1270
  const nets = [...byNet.entries()].map(([net, pins]) => ({ net, pins, pinCount: pins.length }));
1214
1271
  return { source: "sch_Netlist.getNetlist", count: nets.length, nets, netlistBytes: text.length };
1215
1272
  }
1216
- async function placeNetLabel(net, x, y, fontSize, timeoutMs) {
1273
+ async function placeNetLabel(net, x, y, fontSize, timeoutMs, options = {}) {
1217
1274
  if (net === "" || net === "undefined") throw new Error("a net name is required");
1218
1275
  assertNetName(String(net), "eda_place_net_label");
1219
1276
  const size = fontSize === void 0 ? 12 : Number(fontSize);
1220
- try {
1221
- const raw = await callExtension("sch_PrimitiveAttribute.createNetLabel", [x, y, net], Math.min(timeoutMs, 4e3));
1222
- if (raw === void 0 || raw === null) throw new Error("createNetLabel returned nothing");
1223
- return { method: "netLabel", net, x, y, label: summarizePrimitive(raw) };
1224
- } catch (error) {
1225
- const message = error instanceof Error ? error.message : String(error);
1226
- if (!/not found|not available|not allowed|timed out|timeout/i.test(message)) throw error;
1227
- const raw = await callExtension("sch_PrimitiveText.create", [x, y, net, 0, null, null, size, false, false, false, 1], timeoutMs);
1228
- const label = summarizePrimitive(raw);
1229
- return {
1230
- method: "text",
1231
- net,
1277
+ if (options.probe === true) netLabelSupport = null;
1278
+ let failure = null;
1279
+ if (netLabelSupport !== false) {
1280
+ try {
1281
+ const raw2 = await callExtension("sch_PrimitiveAttribute.createNetLabel", [x, y, net], Math.min(timeoutMs, 4e3));
1282
+ if (raw2 === void 0 || raw2 === null) throw new Error("createNetLabel returned nothing");
1283
+ netLabelSupport = true;
1284
+ return { method: "netLabel", net, x, y, label: summarizePrimitive(raw2) };
1285
+ } catch (error) {
1286
+ const message = error instanceof Error ? error.message : String(error);
1287
+ if (!/not found|not available|not allowed|timed out|timeout|nothing|undefined/i.test(message)) throw error;
1288
+ failure = message;
1289
+ netLabelSupport = false;
1290
+ }
1291
+ }
1292
+ const raw = await callExtension("sch_PrimitiveText.create", [x, y, net, 0, null, null, size, false, false, false, 1], timeoutMs);
1293
+ const label = summarizePrimitive(raw);
1294
+ if (label.primitiveId !== void 0) {
1295
+ placedTextLabels.push({ net: String(net), x, y, primitiveId: label.primitiveId });
1296
+ if (placedTextLabels.length > 2e3) placedTextLabels.shift();
1297
+ }
1298
+ return {
1299
+ method: "text",
1300
+ net,
1301
+ x,
1302
+ y,
1303
+ created: label.primitiveId !== void 0,
1304
+ warning: "createNetLabel is unavailable on this EDA build (" + (failure ?? "probe already failed this session") + "), so a plain TEXT label was placed: it carries no electrical meaning. The stub wire drawn by the caller is what joins the net; eda_get_net_labels lists these texts by name and id.",
1305
+ ...label.primitiveId === void 0 ? { hint: "No label primitive was created: the schematic page must be the ACTIVE document (sch_PrimitiveText.create is tab-dependent). Use eda_get_active_document / eda_open_document, then retry." } : {},
1306
+ label
1307
+ };
1308
+ }
1309
+ async function placeAtPin(toolName, args, timeoutMs) {
1310
+ const pinPrimitiveId = String(args.pinPrimitiveId ?? "");
1311
+ if (pinPrimitiveId === "") throw new Error("pinPrimitiveId is required");
1312
+ const isFlag = toolName === "eda_place_net_flag_at_pin";
1313
+ assertNetName(String(args.net ?? ""), toolName);
1314
+ const pin = await callExtension("sch_PrimitivePin.get", [pinPrimitiveId], timeoutMs);
1315
+ const pinRecord = asRecord(pin);
1316
+ const pinX = Number(pickValue(pinRecord, "X", "x"));
1317
+ const pinY = Number(pickValue(pinRecord, "Y", "y"));
1318
+ if (!Number.isFinite(pinX) || !Number.isFinite(pinY)) throw new Error("could not read pin position for " + pinPrimitiveId);
1319
+ const pinRotation = Number(pickValue(pinRecord, "Rotation", "rotation") ?? 0);
1320
+ const direction = pinDirection(pinRotation);
1321
+ const explicitOffset = args.offsetX !== void 0 || args.offsetY !== void 0;
1322
+ const stubLength = args.stubLength === void 0 ? explicitOffset ? 0 : 20 : Math.max(0, Number(args.stubLength));
1323
+ const x = pinX + direction.dx * stubLength + Number(args.offsetX ?? 0);
1324
+ const y = pinY + direction.dy * stubLength + Number(args.offsetY ?? 0);
1325
+ if (args.allowOutsideFrame !== true) {
1326
+ const problem = checkPointInFrame(x, y, args.paper, 5) ?? checkPointAgainstTitleBlock(x, y, args.paper, 5);
1327
+ if (problem !== void 0) {
1328
+ throw new Error(toolName + " refused: " + problem + " - reduce stubLength or pass offsetX/offsetY to place it elsewhere.");
1329
+ }
1330
+ }
1331
+ let stub;
1332
+ if (stubLength > 0 && typeof args.net === "string" && args.net !== "") {
1333
+ stub = summarizePrimitive(await callExtension("sch_PrimitiveWire.create", [
1334
+ [pinX, pinY, x, y],
1335
+ String(args.net),
1336
+ args.color === void 0 ? void 0 : String(args.color),
1337
+ args.lineWidth === void 0 ? void 0 : Number(args.lineWidth),
1338
+ args.lineType === void 0 ? void 0 : Number(args.lineType)
1339
+ ], timeoutMs));
1340
+ }
1341
+ const placement = { pin: { primitiveId: pinPrimitiveId, x: pinX, y: pinY, rotation: pinRotation }, direction, stubLength, x, y, stub };
1342
+ if (isFlag) {
1343
+ const flag = await callExtension("sch_PrimitiveComponent.createNetFlag", [
1344
+ String(args.identification),
1345
+ String(args.net),
1232
1346
  x,
1233
1347
  y,
1234
- created: label.primitiveId !== void 0,
1235
- warning: "createNetLabel could not be called (" + message + "). On JLCEDA Pro 3.2.186 the API exists but never resolves, so it is probed with a 4 s timeout. A plain text label does NOT create an electrical connection - the stub wire drawn by eda_place_net_label_at_pin carries the net, so keep it.",
1236
- ...label.primitiveId === void 0 ? { hint: "No label primitive was created: the schematic page must be the ACTIVE document (sch_PrimitiveText.create is tab-dependent). Use eda_get_active_document / eda_open_document, then retry." } : {},
1237
- label
1238
- };
1348
+ args.rotation === void 0 ? pinRotation : Number(args.rotation),
1349
+ Boolean(args.mirror ?? false)
1350
+ ], timeoutMs);
1351
+ return { ...placement, flag: summarizePrimitive(flag) };
1239
1352
  }
1353
+ const label = await placeNetLabel(String(args.net), x, y, args.fontSize, timeoutMs);
1354
+ return { ...label, ...placement };
1240
1355
  }
1241
1356
  function isInternalHelperMissing(error) {
1242
1357
  const message = error instanceof Error ? error.message : String(error);
@@ -1269,10 +1384,10 @@ async function resolveProjectUuid(args, timeoutMs) {
1269
1384
  const teamUuid = String(pickValue(asRecord(team), "uuid", "Uuid") ?? "");
1270
1385
  if (teamUuid === "") continue;
1271
1386
  const owned = await callExtension("dmt_Project.getAllProjectsUuid", [teamUuid], toolTimeout(args, timeoutMs)).catch(() => []);
1272
- for (const uuid of Array.isArray(owned) ? owned.map(String) : []) {
1273
- if (seen.has(uuid)) continue;
1274
- seen.add(uuid);
1275
- candidates.push(uuid);
1387
+ for (const uuid2 of Array.isArray(owned) ? owned.map(String) : []) {
1388
+ if (seen.has(uuid2)) continue;
1389
+ seen.add(uuid2);
1390
+ candidates.push(uuid2);
1276
1391
  }
1277
1392
  }
1278
1393
  }
@@ -1291,6 +1406,56 @@ async function resolveProjectUuid(args, timeoutMs) {
1291
1406
  }
1292
1407
  return { uuid: matched.uuid, matched };
1293
1408
  }
1409
+ async function waitForActiveProject(uuid2, timeoutMs = 4e3) {
1410
+ const deadline = Date.now() + timeoutMs;
1411
+ let lastActive = null;
1412
+ for (; ; ) {
1413
+ try {
1414
+ const info = asRecord(await callExtension("dmt_Project.getCurrentProjectInfo", [], 3e3));
1415
+ lastActive = String(pickValue(info, "uuid", "Uuid") ?? "") || null;
1416
+ if (lastActive === uuid2) return { active: true, project: info };
1417
+ } catch {
1418
+ }
1419
+ if (Date.now() > deadline) return { active: false, project: null, lastActive };
1420
+ await sleep(400);
1421
+ }
1422
+ }
1423
+ function internalIdsOf(info) {
1424
+ const ids = /* @__PURE__ */ new Set();
1425
+ const boards = Array.isArray(asRecord(info).data) ? asRecord(info).data : [];
1426
+ for (const board of boards) {
1427
+ const record = asRecord(board);
1428
+ const boardUuid = String(pickValue(record, "uuid", "Uuid") ?? "");
1429
+ if (boardUuid !== "") ids.add(boardUuid);
1430
+ for (const docKey of ["schematic", "pcb"]) {
1431
+ const doc = asRecord(record[docKey]);
1432
+ const docUuid = String(pickValue(doc, "uuid", "Uuid") ?? "");
1433
+ if (docUuid !== "") ids.add(docUuid);
1434
+ const pages = Array.isArray(doc.pages) ? doc.pages : [];
1435
+ for (const page of pages) {
1436
+ const pageUuid = String(pickValue(asRecord(page), "uuid", "Uuid") ?? "");
1437
+ if (pageUuid !== "") ids.add(pageUuid);
1438
+ }
1439
+ }
1440
+ }
1441
+ return ids;
1442
+ }
1443
+ async function assertKnownPcbNet(toolName, net, timeoutMs, force) {
1444
+ const name = String(net ?? "").trim();
1445
+ if (name === "") throw new Error(toolName + " requires a net name");
1446
+ if (force === true) return;
1447
+ let names;
1448
+ try {
1449
+ const raw = await callExtension("pcb_Net.getAllNetsName", [], Math.min(timeoutMs, 8e3));
1450
+ names = new Set((Array.isArray(raw) ? raw : []).map((value) => String(value)));
1451
+ } catch {
1452
+ return;
1453
+ }
1454
+ if (names.size === 0) return;
1455
+ if (names.has(name)) return;
1456
+ const sample = [...names].slice(0, 12).join(", ");
1457
+ throw new Error(toolName + ': net "' + name + '" is not on this PCB (' + names.size + " nets: " + sample + "\u2026). A write on an unknown net creates a phantom net that DRC reports as a netlist error; pass force:true if that is really intended.");
1458
+ }
1294
1459
  async function executeTool(name, rawArgs, timeoutMs) {
1295
1460
  const args = asRecord(rawArgs);
1296
1461
  const tool = PCB_TOOL_ALIASES[name] ?? name;
@@ -1361,24 +1526,45 @@ async function executeTool(name, rawArgs, timeoutMs) {
1361
1526
  saveTo
1362
1527
  ], toolTimeout(args, timeoutMs));
1363
1528
  const raw = imported?.result ?? imported;
1364
- const uuid = typeof raw === "string" ? raw : typeof raw?.uuid === "string" ? raw.uuid : void 0;
1529
+ const uuid2 = typeof raw === "string" ? raw : typeof raw?.uuid === "string" ? raw.uuid : void 0;
1365
1530
  const landed = raw !== null && raw !== void 0;
1366
1531
  let opened = false;
1367
- if (args.open === true && uuid !== void 0) {
1532
+ if (args.open === true && uuid2 !== void 0) {
1368
1533
  try {
1369
- opened = await callExtension("dmt_Project.openProject", [uuid], timeoutMs) !== false;
1534
+ await callExtension("dmt_Project.openProject", [uuid2], timeoutMs);
1535
+ opened = (await waitForActiveProject(uuid2, 4e3)).active;
1370
1536
  } catch {
1371
1537
  opened = false;
1372
1538
  }
1373
1539
  }
1540
+ let collisions = null;
1541
+ if (landed && uuid2 !== void 0 && args.checkCollisions !== false) {
1542
+ try {
1543
+ const mine = await callExtension("dmt_Project.getProjectInfo", [uuid2], toolTimeout(args, timeoutMs));
1544
+ const myIds = internalIdsOf(mine);
1545
+ const others = await callExtension("dmt_Project.getAllProjectsUuid", [], toolTimeout(args, timeoutMs));
1546
+ const list = (Array.isArray(others) ? others.map(String) : []).filter((other) => other !== uuid2).slice(0, 20);
1547
+ collisions = [];
1548
+ for (const other of list) {
1549
+ const info = await callExtension("dmt_Project.getProjectInfo", [other], timeoutMs).catch(() => void 0);
1550
+ const shared = [...internalIdsOf(info)].filter((sharedId) => myIds.has(sharedId));
1551
+ if (shared.length > 0) {
1552
+ collisions.push({ projectUuid: other, friendlyName: pickValue(asRecord(info), "friendlyName", "FriendlyName") ?? null, sharedIds: shared });
1553
+ }
1554
+ }
1555
+ } catch {
1556
+ collisions = null;
1557
+ }
1558
+ }
1374
1559
  return {
1375
1560
  ok: landed,
1376
1561
  imported: landed,
1377
1562
  name: basename(path),
1378
1563
  size: bytes.length,
1379
1564
  teamUuid,
1380
- uuid: uuid ?? null,
1565
+ uuid: uuid2 ?? null,
1381
1566
  opened,
1567
+ collisions,
1382
1568
  result: raw ?? null,
1383
1569
  hint: landed ? null : "The app returned nothing: the import did not create a project. That means this build ignores the saveTo target (same limitation as createProject), so import cannot be used to create projects here."
1384
1570
  };
@@ -1441,10 +1627,10 @@ async function executeTool(name, rawArgs, timeoutMs) {
1441
1627
  const teamUuid = String(pickValue(asRecord(team), "uuid", "Uuid") ?? "");
1442
1628
  if (teamUuid === "") continue;
1443
1629
  const owned = await callExtension("dmt_Project.getAllProjectsUuid", [teamUuid], toolTimeout(args, timeoutMs)).catch(() => []);
1444
- for (const uuid of Array.isArray(owned) ? owned.map(String) : []) {
1445
- if (seen.has(uuid)) continue;
1446
- seen.add(uuid);
1447
- list.push(uuid);
1630
+ for (const uuid2 of Array.isArray(owned) ? owned.map(String) : []) {
1631
+ if (seen.has(uuid2)) continue;
1632
+ seen.add(uuid2);
1633
+ list.push(uuid2);
1448
1634
  }
1449
1635
  }
1450
1636
  aggregated = true;
@@ -1452,11 +1638,11 @@ async function executeTool(name, rawArgs, timeoutMs) {
1452
1638
  if (args.resolveNames === false) return { count: list.length, uuids: list, aggregated };
1453
1639
  const limit = Number.isFinite(Number(args.limit)) ? Math.max(1, Number(args.limit)) : list.length;
1454
1640
  const projects = [];
1455
- for (const uuid of list.slice(0, limit)) {
1641
+ for (const uuid2 of list.slice(0, limit)) {
1456
1642
  try {
1457
- projects.push(await callExtension("dmt_Project.getProjectInfo", [uuid], toolTimeout(args, timeoutMs)));
1643
+ projects.push(await callExtension("dmt_Project.getProjectInfo", [uuid2], toolTimeout(args, timeoutMs)));
1458
1644
  } catch {
1459
- projects.push({ uuid });
1645
+ projects.push({ uuid: uuid2 });
1460
1646
  }
1461
1647
  }
1462
1648
  return { count: list.length, resolved: projects.length, aggregated, projects };
@@ -1467,10 +1653,17 @@ async function executeTool(name, rawArgs, timeoutMs) {
1467
1653
  }
1468
1654
  case "eda_open_project": {
1469
1655
  const resolvedProject = await resolveProjectUuid(args, timeoutMs);
1470
- const uuid = resolvedProject.uuid;
1471
- const opened = await callExtension("dmt_Project.openProject", [uuid], toolTimeout(args, timeoutMs));
1472
- const current = await callExtension("dmt_Project.getCurrentProjectInfo", [], timeoutMs).catch(() => void 0);
1473
- return { opened: opened !== false, uuid, matched: resolvedProject.matched, current: current === void 0 ? null : summarizePrimitive(current) };
1656
+ const uuid2 = resolvedProject.uuid;
1657
+ const returned = await callExtension("dmt_Project.openProject", [uuid2], toolTimeout(args, timeoutMs));
1658
+ const activation = await waitForActiveProject(uuid2, args.verifyMs === void 0 ? 4e3 : Number(args.verifyMs));
1659
+ return {
1660
+ opened: activation.active,
1661
+ openReturned: returned !== false,
1662
+ uuid: uuid2,
1663
+ matched: resolvedProject.matched,
1664
+ current: activation.active ? summarizePrimitive(activation.project) : null,
1665
+ ...activation.active ? {} : { hint: "The project did not become active within the verification window (last active: " + (activation.lastActive ?? "unknown") + "); it may still be loading, or the app refused the switch." }
1666
+ };
1474
1667
  }
1475
1668
  case "eda_get_workspace": {
1476
1669
  const workspace = await callExtension("dmt_Workspace.getCurrentWorkspaceInfo", [], timeoutMs).catch(() => void 0);
@@ -1545,13 +1738,26 @@ async function executeTool(name, rawArgs, timeoutMs) {
1545
1738
  uuid: args.sourceUuid,
1546
1739
  name: args.sourceName
1547
1740
  }, timeoutMs);
1548
- const uuid = await callExtension("dmt_Project.copyProject", [
1549
- source.uuid,
1550
- args.targetTeamUuid === void 0 ? void 0 : String(args.targetTeamUuid),
1551
- args.targetFolderUuid === void 0 ? void 0 : String(args.targetFolderUuid),
1552
- args.newFriendlyName === void 0 ? void 0 : String(args.newFriendlyName),
1553
- args.newName === void 0 ? void 0 : String(args.newName)
1554
- ], toolTimeout(args, timeoutMs));
1741
+ let copyRaw;
1742
+ try {
1743
+ copyRaw = await callExtension("dmt_Project.copyProject", [
1744
+ source.uuid,
1745
+ args.targetTeamUuid === void 0 ? void 0 : String(args.targetTeamUuid),
1746
+ args.targetFolderUuid === void 0 ? void 0 : String(args.targetFolderUuid),
1747
+ args.newFriendlyName === void 0 ? void 0 : String(args.newFriendlyName),
1748
+ args.newName === void 0 ? void 0 : String(args.newName)
1749
+ ], toolTimeout(args, timeoutMs));
1750
+ } catch (failure) {
1751
+ throw new Error("eda_copy_project failed: " + (failure instanceof Error ? failure.message : String(failure)));
1752
+ }
1753
+ const copyUuid = typeof copyRaw === "string" ? copyRaw : String(pickValue(asRecord(copyRaw), "uuid", "Uuid") ?? "");
1754
+ return {
1755
+ copied: copyUuid !== "",
1756
+ sourceUuid: source.uuid,
1757
+ uuid: copyUuid === "" ? null : copyUuid,
1758
+ raw: copyRaw ?? null,
1759
+ hint: copyUuid === "" ? "The app returned no uuid and created nothing: copyProject does not work on this build for local projects (same family as createProject/deleteProject). Duplicate it in the app, or export with eda_export_project and import it back with eda_import_project to get an independent copy." : null
1760
+ };
1555
1761
  return { copied: typeof uuid === "string" && uuid !== "", sourceUuid: source.uuid, uuid: uuid ?? null };
1556
1762
  }
1557
1763
  case "eda_create_project": {
@@ -1568,25 +1774,26 @@ async function executeTool(name, rawArgs, timeoutMs) {
1568
1774
  throw new Error("eda_create_project: no team is available (sign in to a JLC account in JLCEDA Pro, or pass teamUuid from eda_list_teams)");
1569
1775
  }
1570
1776
  const folderUuid = args.folderUuid === void 0 ? "" : String(args.folderUuid).trim();
1571
- const uuid = await callExtension("dmt_Project.createProject", [
1777
+ const uuid2 = await callExtension("dmt_Project.createProject", [
1572
1778
  friendlyName,
1573
1779
  args.name === void 0 ? void 0 : String(args.name),
1574
1780
  teamUuid,
1575
1781
  folderUuid === "" ? void 0 : folderUuid,
1576
1782
  args.description === void 0 ? void 0 : String(args.description)
1577
1783
  ], toolTimeout(args, timeoutMs));
1578
- if (typeof uuid !== "string" || uuid === "") {
1784
+ if (typeof uuid2 !== "string" || uuid2 === "") {
1579
1785
  throw new Error("eda_create_project: the app returned no project uuid (team " + teamUuid + " from " + teamSource + "; the app may have shown a dialog or rejected the name)");
1580
1786
  }
1581
1787
  let opened = false;
1582
1788
  if (args.open === true) {
1583
1789
  try {
1584
- opened = await callExtension("dmt_Project.openProject", [uuid], timeoutMs) !== false;
1790
+ await callExtension("dmt_Project.openProject", [uuid2], timeoutMs);
1791
+ opened = (await waitForActiveProject(uuid2, 4e3)).active;
1585
1792
  } catch {
1586
1793
  opened = false;
1587
1794
  }
1588
1795
  }
1589
- return { created: true, uuid, friendlyName, teamUuid, teamSource, folderUuid: folderUuid === "" ? null : folderUuid, opened };
1796
+ return { created: true, uuid: uuid2, friendlyName, teamUuid, teamSource, folderUuid: folderUuid === "" ? null : folderUuid, opened };
1590
1797
  }
1591
1798
  case "eda_launch": {
1592
1799
  const probe = launchProbe();
@@ -1768,68 +1975,90 @@ async function executeTool(name, rawArgs, timeoutMs) {
1768
1975
  const componentPrimitiveId = String(args.componentPrimitiveId ?? "");
1769
1976
  if (componentPrimitiveId === "") throw new Error("componentPrimitiveId is required");
1770
1977
  const raw = await callExtension("sch_PrimitiveComponent.getAllPinsByPrimitiveId", [componentPrimitiveId], timeoutMs);
1771
- if (Array.isArray(raw)) return raw.map(summarizePrimitive);
1772
- return raw;
1978
+ if (!Array.isArray(raw)) return raw;
1979
+ return raw.map((entry) => {
1980
+ const record = asRecord(entry);
1981
+ return {
1982
+ primitiveId: pickValue(record, "PrimitiveId", "primitiveId"),
1983
+ pinNumber: pickValue(record, "PinNumber", "pinNumber") ?? null,
1984
+ pinName: pickValue(record, "PinName", "pinName") ?? null,
1985
+ x: pickValue(record, "X", "x"),
1986
+ y: pickValue(record, "Y", "y"),
1987
+ rotation: pickValue(record, "Rotation", "rotation") ?? 0,
1988
+ pinLength: pickValue(record, "PinLength", "pinLength") ?? null,
1989
+ noConnected: pickValue(record, "NoConnected", "noConnected") ?? null
1990
+ };
1991
+ });
1773
1992
  }
1774
1993
  case "eda_place_net_flag_at_pin":
1775
- case "eda_place_net_label_at_pin": {
1776
- const pinPrimitiveId = String(args.pinPrimitiveId ?? "");
1777
- if (pinPrimitiveId === "") throw new Error("pinPrimitiveId is required");
1778
- assertNetName(String(args.net ?? ""), name === "eda_place_net_flag_at_pin" ? "eda_place_net_flag_at_pin" : "eda_place_net_label_at_pin");
1779
- const pin = await callExtension("sch_PrimitivePin.get", [pinPrimitiveId], timeoutMs);
1780
- const pinRecord = asRecord(pin);
1781
- const pinX = Number(pickValue(pinRecord, "X", "x"));
1782
- const pinY = Number(pickValue(pinRecord, "Y", "y"));
1783
- if (!Number.isFinite(pinX) || !Number.isFinite(pinY)) throw new Error("could not read pin position for " + pinPrimitiveId);
1784
- const pinRotation = Number(pickValue(pinRecord, "Rotation", "rotation") ?? 0);
1785
- const direction = pinDirection(pinRotation);
1786
- const explicitOffset = args.offsetX !== void 0 || args.offsetY !== void 0;
1787
- const stubLength = args.stubLength === void 0 ? explicitOffset ? 0 : 20 : Math.max(0, Number(args.stubLength));
1788
- const x = pinX + direction.dx * stubLength + Number(args.offsetX ?? 0);
1789
- const y = pinY + direction.dy * stubLength + Number(args.offsetY ?? 0);
1790
- if (args.allowOutsideFrame !== true) {
1791
- const problem = checkPointInFrame(x, y, args.paper, 5) ?? checkPointAgainstTitleBlock(x, y, args.paper, 5);
1792
- if (problem !== void 0) {
1793
- throw new Error((name === "eda_place_net_flag_at_pin" ? "eda_place_net_flag_at_pin" : "eda_place_net_label_at_pin") + " refused: " + problem + " - reduce stubLength or pass offsetX/offsetY to place it elsewhere.");
1794
- }
1795
- }
1796
- let stub;
1797
- if (stubLength > 0 && typeof args.net === "string" && args.net !== "") {
1798
- stub = summarizePrimitive(await callExtension("sch_PrimitiveWire.create", [
1799
- [pinX, pinY, x, y],
1800
- String(args.net),
1801
- args.color === void 0 ? void 0 : String(args.color),
1802
- args.lineWidth === void 0 ? void 0 : Number(args.lineWidth),
1803
- args.lineType === void 0 ? void 0 : Number(args.lineType)
1804
- ], timeoutMs));
1805
- }
1806
- const placement = { pin: { primitiveId: pinPrimitiveId, x: pinX, y: pinY, rotation: pinRotation }, direction, stubLength, x, y, stub };
1807
- if (name === "eda_place_net_flag_at_pin") {
1808
- const flag = await callExtension("sch_PrimitiveComponent.createNetFlag", [
1809
- String(args.identification),
1810
- String(args.net),
1811
- x,
1812
- y,
1813
- args.rotation === void 0 ? pinRotation : Number(args.rotation),
1814
- Boolean(args.mirror ?? false)
1815
- ], timeoutMs);
1816
- return { ...placement, flag: summarizePrimitive(flag) };
1817
- }
1818
- const label = await placeNetLabel(String(args.net), x, y, args.fontSize, timeoutMs);
1819
- return { ...label, ...placement };
1820
- }
1994
+ case "eda_place_net_label_at_pin":
1995
+ return await placeAtPin(name, args, timeoutMs);
1821
1996
  case "eda_place_net_label": {
1822
1997
  const x = Number(args.x);
1823
1998
  const y = Number(args.y);
1824
1999
  if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error("eda_place_net_label requires numeric x/y");
1825
- return await placeNetLabel(String(args.net), x, y, args.fontSize, timeoutMs);
2000
+ return await placeNetLabel(String(args.net), x, y, args.fontSize, timeoutMs, { probe: args.probe === true });
2001
+ }
2002
+ case "eda_place_net_labels": {
2003
+ const items = Array.isArray(args.items) ? args.items.map(asRecord) : [];
2004
+ if (items.length === 0) throw new Error("eda_place_net_labels requires items: [{ pinPrimitiveId, net }] or [{ x, y, net }]");
2005
+ const concurrency = Number.isFinite(Number(args.concurrency)) ? Math.max(1, Math.min(8, Number(args.concurrency))) : 4;
2006
+ const results = await mapLimit(items, concurrency, async (item) => {
2007
+ const net = item.net === void 0 ? "" : String(item.net);
2008
+ try {
2009
+ const placed = item.pinPrimitiveId !== void 0 ? await placeAtPin("eda_place_net_label_at_pin", { ...item, net }, timeoutMs) : await placeNetLabel(net, Number(item.x), Number(item.y), item.fontSize, timeoutMs, { probe: args.probe === true });
2010
+ return { ok: true, net, ...placed };
2011
+ } catch (error) {
2012
+ return { ok: false, net, error: error instanceof Error ? error.message : String(error) };
2013
+ }
2014
+ });
2015
+ const done = results.filter((entry) => entry.ok).length;
2016
+ return {
2017
+ total: items.length,
2018
+ done,
2019
+ failed: items.length - done,
2020
+ method: netLabelSupport === true ? "netLabel" : "text",
2021
+ results
2022
+ };
1826
2023
  }
1827
2024
  case "eda_get_net_labels": {
1828
- const raw = await callExtension("sch_PrimitiveAttribute.getAll", [
2025
+ const attributes = await callExtension("sch_PrimitiveAttribute.getAll", [
1829
2026
  args.parentPrimitiveId === void 0 ? void 0 : String(args.parentPrimitiveId)
1830
- ], timeoutMs);
1831
- if (!Array.isArray(raw)) return raw;
1832
- return { count: raw.length, labels: raw.map(summarizePrimitive) };
2027
+ ], timeoutMs).catch(() => []);
2028
+ const texts = args.parentPrimitiveId === void 0 ? await callExtension("sch_PrimitiveText.getAll", [], timeoutMs).catch(() => []) : [];
2029
+ const entries = [];
2030
+ for (const entry of Array.isArray(attributes) ? attributes : []) {
2031
+ const record = asRecord(entry);
2032
+ entries.push({
2033
+ primitiveId: pickValue(record, "PrimitiveId", "primitiveId"),
2034
+ kind: String(pickValue(record, "PrimitiveType", "primitiveType", "Type", "type") ?? "attribute"),
2035
+ net: pickValue(record, "Net", "net") ?? null,
2036
+ userValue: pickValue(record, "UserValue", "userValue", "Value", "value") ?? null,
2037
+ x: pickValue(record, "X", "x"),
2038
+ y: pickValue(record, "Y", "y")
2039
+ });
2040
+ }
2041
+ for (const entry of Array.isArray(texts) ? texts : []) {
2042
+ const record = asRecord(entry);
2043
+ const content = String(pickValue(record, "Content", "content", "Text", "text") ?? "");
2044
+ if (content === "") continue;
2045
+ entries.push({
2046
+ primitiveId: pickValue(record, "PrimitiveId", "primitiveId"),
2047
+ kind: "text",
2048
+ net: content,
2049
+ userValue: content,
2050
+ x: pickValue(record, "X", "x"),
2051
+ y: pickValue(record, "Y", "y")
2052
+ });
2053
+ }
2054
+ return {
2055
+ count: entries.length,
2056
+ attributeCount: Array.isArray(attributes) ? attributes.length : 0,
2057
+ textCount: Array.isArray(texts) ? texts.length : 0,
2058
+ sessionTextLabels: placedTextLabels.length,
2059
+ note: 'kind "text" entries are plain texts (no electrical meaning). Delete any entry with eda_delete_primitive(kind: "netlabel"|"netport"|"netflag"|"text", primitiveId).',
2060
+ labels: entries
2061
+ };
1833
2062
  }
1834
2063
  case "eda_get_active_document": {
1835
2064
  const info = await callExtension("dmt_SelectControl.getCurrentDocumentInfo", [], timeoutMs);
@@ -1837,11 +2066,35 @@ async function executeTool(name, rawArgs, timeoutMs) {
1837
2066
  const type = Number(record.documentType);
1838
2067
  return { document: info, documentTypeName: DOCUMENT_TYPES[type] ?? String(record.documentType ?? "unknown"), documentType: record.documentType };
1839
2068
  }
1840
- case "eda_open_document":
1841
- return await callExtension("dmt_EditorControl.openDocument", [
1842
- String(args.documentUuid),
2069
+ case "eda_open_document": {
2070
+ const documentUuid = String(args.documentUuid ?? "");
2071
+ if (documentUuid === "") throw new Error("documentUuid is required");
2072
+ const raw = await callExtension("dmt_EditorControl.openDocument", [
2073
+ documentUuid,
1843
2074
  args.splitScreenId === void 0 ? void 0 : String(args.splitScreenId)
1844
2075
  ], timeoutMs);
2076
+ await sleep(600);
2077
+ let tabIds = [];
2078
+ try {
2079
+ const tree = asRecord(await callExtension("dmt_EditorControl.getSplitScreenTree", [], Math.min(timeoutMs, 4e3)));
2080
+ tabIds = (Array.isArray(tree.tabs) ? tree.tabs : []).map((tab) => String(pickValue(asRecord(tab), "tabId") ?? ""));
2081
+ } catch {
2082
+ }
2083
+ let activeUuid = "";
2084
+ try {
2085
+ const info = asRecord(await callExtension("dmt_SelectControl.getCurrentDocumentInfo", [], Math.min(timeoutMs, 4e3)));
2086
+ activeUuid = String(pickValue(info, "uuid", "Uuid") ?? "");
2087
+ } catch {
2088
+ }
2089
+ const tabOpened = tabIds.length > 1 || activeUuid !== "" && activeUuid !== "0";
2090
+ return {
2091
+ ...raw !== null && typeof raw === "object" ? raw : { result: raw ?? null },
2092
+ opened: tabOpened,
2093
+ activeDocumentUuid: activeUuid || null,
2094
+ tabIds,
2095
+ hint: tabOpened ? null : "The app accepted openDocument but created no editor tab (only the Start Page is open). This build does not open a document programmatically from the HOME screen: open the schematic/PCB page once in the app (double-click the project), then retry. Do not assume the page is active."
2096
+ };
2097
+ }
1845
2098
  case "eda_set_no_connect": {
1846
2099
  const ids = Array.isArray(args.primitiveIds) ? args.primitiveIds.map(String) : [];
1847
2100
  if (ids.length === 0) throw new Error("primitiveIds is required");
@@ -1955,8 +2208,33 @@ async function executeTool(name, rawArgs, timeoutMs) {
1955
2208
  ], timeoutMs);
1956
2209
  case "eda_save_document":
1957
2210
  return await callExtension("sch_Document.save", [], timeoutMs);
1958
- case "eda_zoom_to_fit":
1959
- return await callExtension("dmt_EditorControl.zoomToAllPrimitives", [args.tabId], timeoutMs);
2211
+ case "eda_zoom_to_fit": {
2212
+ const raw = await callExtension("dmt_EditorControl.zoomToAllPrimitives", [args.tabId], timeoutMs);
2213
+ const record = asRecord(raw);
2214
+ const left = Number(pickValue(record, "left", "Left"));
2215
+ const right = Number(pickValue(record, "right", "Right"));
2216
+ const bottom = Number(pickValue(record, "bottom", "Bottom"));
2217
+ const top = Number(pickValue(record, "top", "Top"));
2218
+ if (![left, right, bottom, top].every((value) => Number.isFinite(value))) return raw;
2219
+ const frame = paperOf(args.paper).frame;
2220
+ const clamped = {
2221
+ left: Math.max(frame.left, left),
2222
+ right: Math.min(frame.right, right),
2223
+ bottom: Math.max(frame.bottom, bottom),
2224
+ top: Math.min(frame.top, top)
2225
+ };
2226
+ if (clamped.left >= clamped.right || clamped.bottom >= clamped.top) {
2227
+ return { ...record, requested: { left, right, bottom, top }, clamped: null, note: "the primitive bounds do not overlap the page frame" };
2228
+ }
2229
+ await callExtension("dmt_EditorControl.zoomToRegion", [
2230
+ clamped.left,
2231
+ clamped.right,
2232
+ clamped.top,
2233
+ clamped.bottom,
2234
+ args.tabId
2235
+ ], timeoutMs);
2236
+ return { ...record, requested: { left, right, bottom, top }, clamped };
2237
+ }
1960
2238
  case "eda_zoom_to_region":
1961
2239
  return await callExtension("dmt_EditorControl.zoomToRegion", [
1962
2240
  Number(args.left),
@@ -2000,24 +2278,99 @@ async function executeTool(name, rawArgs, timeoutMs) {
2000
2278
  const parsed = await parseNetlist(Array.isArray(all) ? all : project, timeoutMs);
2001
2279
  return parsed;
2002
2280
  }
2003
- case "eda_run_drc":
2004
- return await callExtension("sch_Drc.check", [args.strict === void 0 ? true : Boolean(args.strict), false, true], timeoutMs);
2281
+ case "eda_run_drc": {
2282
+ const verbose = await callExtension("sch_Drc.check", [
2283
+ args.strict === void 0 ? true : Boolean(args.strict),
2284
+ false,
2285
+ true
2286
+ ], toolTimeout(args, timeoutMs));
2287
+ if (Array.isArray(verbose)) {
2288
+ return {
2289
+ ok: verbose.length === 0,
2290
+ count: verbose.length,
2291
+ entries: verbose.map((entry) => {
2292
+ const record = asRecord(entry);
2293
+ return {
2294
+ type: pickValue(record, "Type", "type", "Level", "level") ?? null,
2295
+ message: pickValue(record, "Message", "message", "Error", "error", "Description", "description") ?? null,
2296
+ primitiveId: pickValue(record, "PrimitiveId", "primitiveId") ?? null,
2297
+ x: pickValue(record, "X", "x") ?? null,
2298
+ y: pickValue(record, "Y", "y") ?? null,
2299
+ raw: entry
2300
+ };
2301
+ }),
2302
+ hint: 'EDA 3.2.186 returns AGGREGATED entries only (e.g. {type:"warn",count:1}) even with includeVerboseError, so message/primitiveId stay null \u2014 "raw" carries exactly what the app sent. Per-problem coordinates are not available on this build; use eda_pcb_run_drc for the PCB side and the DRC panel for details.'
2303
+ };
2304
+ }
2305
+ return { ok: Boolean(verbose), count: null, entries: [], raw: verbose };
2306
+ }
2005
2307
  case "eda_auto_layout":
2006
- return await callExtension("sch_Document.autoLayout", [Array.isArray(args.uuids) ? { uuids: args.uuids.map(String) } : void 0], timeoutMs);
2007
- case "eda_auto_route":
2008
- return await callExtension("sch_Document.autoRouting", [Array.isArray(args.uuids) ? { uuids: args.uuids.map(String) } : void 0], timeoutMs);
2009
- case "eda_export_netlist":
2010
- return await callExtension("sch_ManufactureData.getNetlistFile", [void 0, typeof args.type === "string" && args.type !== "" ? args.type : "JLCEDA"], timeoutMs);
2011
- case "eda_export_bom":
2012
- return await callExtension("sch_ManufactureData.getBomFile", [void 0, args.format === "xlsx" ? "xlsx" : "csv"], timeoutMs);
2013
- case "eda_delete_primitive":
2014
- return await callExtension(primitiveKindToClass(String(args.kind)) + ".delete", [String(args.primitiveId)], timeoutMs);
2308
+ case "eda_auto_route": {
2309
+ if (ALLOW_AUTO_LAYOUT !== true && args.force !== true) {
2310
+ throw new Error(
2311
+ name + " is disabled by policy (settings allowAutoLayoutRouting=false, env DSH_EDA_ALLOW_AUTO_LAYOUT). Do NOT retry with force:true unless the user explicitly asked for automatic layout/routing.",
2312
+ "",
2313
+ "Do this instead, in order:",
2314
+ "1. eda_get_active_document \u2014 make sure a schematic page is active (eda_open_document if not).",
2315
+ "2. eda_get_page_info \u2014 read the A4 frame and the safe areas before placing anything.",
2316
+ "3. eda_get_components \u2014 see what is already placed and where (x/y + bounding boxes) to avoid overlap.",
2317
+ "4. Place or move parts yourself on a 10-unit grid: eda_place_component for new ones, eda_move_component (keeps designator/value) to re-grid existing ones. Signal flow left to right, power rails on top, ground at the bottom, >=20 units between symbols for wires and labels.",
2318
+ "5. Connect with eda_draw_wire for local nets; for anything that spans the sheet use eda_place_net_port or a net label (a wire net name is overridden by connectivity once an endpoint touches a pin).",
2319
+ "6. Verify: eda_export_netlist (every net should have >=2 endpoints) and eda_run_drc (per-problem entries). For a visual check export the canvas to an image and look at it, rather than letting EDA rearrange the sheet.",
2320
+ "7. Call eda_workflow_guide for the full standard."
2321
+ );
2322
+ }
2323
+ const payload = Array.isArray(args.uuids) ? { uuids: args.uuids.map(String) } : void 0;
2324
+ return await callExtension(name === "eda_auto_layout" ? "sch_Document.autoLayout" : "sch_Document.autoRouting", [payload], toolTimeout(args, timeoutMs));
2325
+ }
2326
+ case "eda_export_netlist": {
2327
+ const payload = await callExtension("sch_ManufactureData.getNetlistFile", [
2328
+ void 0,
2329
+ typeof args.type === "string" && args.type !== "" ? args.type : void 0
2330
+ ], toolTimeout(args, timeoutMs));
2331
+ const text = typeof payload === "string" ? payload : payload !== null && typeof payload === "object" && typeof payload.text === "string" ? payload.text : void 0;
2332
+ if (text === void 0) return { parsed: false, netlist: payload ?? null };
2333
+ if (args.format === "text") return { parsed: false, netlistText: text, bytes: text.length };
2334
+ try {
2335
+ return { parsed: true, bytes: text.length, netlist: JSON.parse(text) };
2336
+ } catch {
2337
+ return { parsed: false, bytes: text.length, netlistText: text };
2338
+ }
2339
+ }
2340
+ case "eda_export_bom": {
2341
+ const format = args.format === "xlsx" ? "xlsx" : "csv";
2342
+ const payload = await callExtension("sch_ManufactureData.getBomFile", [void 0, format], toolTimeout(args, timeoutMs));
2343
+ if (format === "xlsx") return payload;
2344
+ const raw = typeof payload === "string" ? payload : payload !== null && typeof payload === "object" && typeof payload.text === "string" ? payload.text : "";
2345
+ const encoding = payload !== null && typeof payload === "object" ? payload.encoding : void 0;
2346
+ return { format, bom: decodeTextPayload(raw), encoding: encoding ?? "unknown" };
2347
+ }
2348
+ case "eda_delete_primitive": {
2349
+ const kind = String(args.kind);
2350
+ const className = kind === "netport" || kind === "netflag" || kind === "netlabel" ? "sch_PrimitiveAttribute" : primitiveKindToClass(kind);
2351
+ return await callExtension(className + ".delete", [String(args.primitiveId)], timeoutMs);
2352
+ }
2353
+ case "eda_move_component":
2354
+ return await callExtension("sch_PrimitiveComponent.modify", [String(args.primitiveId), {
2355
+ x: args.x === void 0 ? void 0 : Number(args.x),
2356
+ y: args.y === void 0 ? void 0 : Number(args.y),
2357
+ rotation: args.rotation === void 0 ? void 0 : Number(args.rotation),
2358
+ mirror: args.mirror === void 0 ? void 0 : Boolean(args.mirror)
2359
+ }], timeoutMs);
2360
+ case "eda_list_api_methods":
2361
+ return { count: ALLOWED_API.size, methods: [...ALLOWED_API].sort() };
2362
+ case "eda_workflow_guide":
2363
+ return { summary: WORKFLOW_SUMMARY, guide: WORKFLOW_GUIDE, autoLayoutAllowed: ALLOW_AUTO_LAYOUT };
2364
+ case "eda_workflow_guide":
2365
+ return { summary: WORKFLOW_SUMMARY, guide: WORKFLOW_GUIDE, autoLayoutAllowed: ALLOW_AUTO_LAYOUT };
2015
2366
  case "eda_create_board":
2016
2367
  return await callExtension("dmt_Board.createBoard", [args.schematicUuid, args.pcbUuid], timeoutMs);
2017
2368
  case "eda_api_call": {
2018
2369
  if (!ALLOW_RAW_API) throw new Error("eda_api_call is disabled by settings (allowRawApi=false)");
2019
2370
  const method = String(args.method ?? "");
2020
- if (!ALLOWED_API.has(method)) throw new Error("API method not allowed: " + method);
2371
+ if (!ALLOWED_API.has(method)) {
2372
+ throw new Error("API method not allowed: " + method + ". The extension only accepts its allow-list (" + ALLOWED_API.size + " methods): call eda_list_api_methods to get it. Common schematic helpers such as sch_PrimitiveComponent.getAllPinsByPrimitiveId and sch_PrimitivePin.get are already wrapped by eda_get_pins.");
2373
+ }
2021
2374
  return await callExtension(method, Array.isArray(args.args) ? args.args : [], timeoutMs);
2022
2375
  }
2023
2376
  case "pcb_get_components": {
@@ -2080,6 +2433,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
2080
2433
  if (layer === void 0) throw new Error("pcb_draw_track requires a valid layer");
2081
2434
  const net = String(args.net);
2082
2435
  assertNetName(net, "pcb_draw_track");
2436
+ await assertKnownPcbNet("pcb_draw_track", net, timeoutMs, args.force === true);
2083
2437
  const width = checkTrackWidth(net, args.lineWidth === void 0 ? void 0 : Number(args.lineWidth), args.force === true);
2084
2438
  if (!width.ok) throw new Error("pcb_draw_track: " + width.errors.join("; "));
2085
2439
  const created = await callExtension("pcb_PrimitiveLine.create", [
@@ -2096,6 +2450,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
2096
2450
  case "pcb_draw_via": {
2097
2451
  const net = String(args.net);
2098
2452
  assertNetName(net, "pcb_draw_via");
2453
+ await assertKnownPcbNet("pcb_draw_via", net, timeoutMs, args.force === true);
2099
2454
  const role = classifyNet(net);
2100
2455
  const defaultVia = role === "signal" ? VIA_SIZES_MM[0] : VIA_SIZES_MM[1];
2101
2456
  const hole = args.holeDiameter === void 0 ? defaultVia.hole * 39.37007874015748 : Number(args.holeDiameter);
@@ -2196,17 +2551,17 @@ async function executeTool(name, rawArgs, timeoutMs) {
2196
2551
  case "pcb_save_document":
2197
2552
  return await callExtension("pcb_Document.save", [], timeoutMs);
2198
2553
  case "pcb_import_changes": {
2199
- const uuid = args.schematicUuid === void 0 || args.schematicUuid === null ? null : String(args.schematicUuid);
2554
+ const uuid2 = args.schematicUuid === void 0 || args.schematicUuid === null ? null : String(args.schematicUuid);
2200
2555
  if (args.wait === true) {
2201
- return await callExtension("pcb_Document.importChanges", [uuid === null ? void 0 : uuid], toolTimeout(args, timeoutMs));
2556
+ return await callExtension("pcb_Document.importChanges", [uuid2 === null ? void 0 : uuid2], toolTimeout(args, timeoutMs));
2202
2557
  }
2203
2558
  pcbComponentBaseline = await pcbComponentIds(timeoutMs);
2204
2559
  let started;
2205
2560
  try {
2206
- started = asRecord(await callExtension("dsh.internal.jobStart", ["importChanges", uuid], timeoutMs));
2561
+ started = asRecord(await callExtension("dsh.internal.jobStart", ["importChanges", uuid2], timeoutMs));
2207
2562
  } catch (error) {
2208
2563
  if (!isInternalHelperMissing(error)) throw error;
2209
- const result2 = await callExtension("pcb_Document.importChanges", [uuid === null ? void 0 : uuid], toolTimeout(args, timeoutMs));
2564
+ const result2 = await callExtension("pcb_Document.importChanges", [uuid2 === null ? void 0 : uuid2], toolTimeout(args, timeoutMs));
2210
2565
  const after = await pcbComponentIds(timeoutMs);
2211
2566
  return {
2212
2567
  triggered: true,