dsh-eda-mcp 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/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
335
  var VERSION = "0.4.0";
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,13 @@ 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
+ const looksUtf16 = text.charCodeAt(0) === 65279 || text.length > 3 && text.charCodeAt(1) === 0 && text.charCodeAt(3) === 0;
367
+ if (!looksUtf16) return text;
368
+ return Buffer.from(text, "latin1").toString("utf16le").replace(String.fromCharCode(65279), "");
369
+ }
324
370
  function checkPointInFrame(x, y, paperName, margin = 0) {
325
371
  const paper = paperOf(paperName);
326
372
  const f = paper.frame;
@@ -539,11 +585,11 @@ var TOOLS = [
539
585
  }, ["libraryUuid", "uuid", "x", "y"]) },
540
586
  { 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
587
  { 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"]) },
588
+ { 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
589
  { 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
590
  { 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
591
  { 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"]) },
592
+ { 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
593
  { 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
594
  { 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
595
  { 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 +602,18 @@ var TOOLS = [
556
602
  { 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
603
  { 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
604
  { 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" } } }) },
605
+ { 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" } }) },
606
+ { 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"]) },
607
+ { 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" } }, ["items"]) },
608
+ { name: "eda_run_drc", description: "Run schematic DRC with includeVerboseError and return one entry per problem (type, message, primitiveId, x/y plus the raw record) instead of bare counts.", inputSchema: objectSchema({ strict: { type: "boolean" } }) },
609
+ { 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" } }) },
610
+ { 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
611
  { 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"]) },
612
+ { 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"] } }) },
613
+ { 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"]) },
614
+ { 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({}) },
615
+ { 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({}) },
616
+ { 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
617
  { 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
618
  { 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
619
  { 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 +797,8 @@ var WsConnection = class {
747
797
  var clients = /* @__PURE__ */ new Set();
748
798
  var pendingCalls = /* @__PURE__ */ new Map();
749
799
  var pcbComponentBaseline = null;
800
+ var netLabelSupport = null;
801
+ var placedTextLabels = [];
750
802
  function sleep(ms) {
751
803
  return new Promise((resolve) => setTimeout(resolve, ms));
752
804
  }
@@ -1213,30 +1265,88 @@ async function parseNetlist(previous, timeoutMs) {
1213
1265
  const nets = [...byNet.entries()].map(([net, pins]) => ({ net, pins, pinCount: pins.length }));
1214
1266
  return { source: "sch_Netlist.getNetlist", count: nets.length, nets, netlistBytes: text.length };
1215
1267
  }
1216
- async function placeNetLabel(net, x, y, fontSize, timeoutMs) {
1268
+ async function placeNetLabel(net, x, y, fontSize, timeoutMs, options = {}) {
1217
1269
  if (net === "" || net === "undefined") throw new Error("a net name is required");
1218
1270
  assertNetName(String(net), "eda_place_net_label");
1219
1271
  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,
1272
+ if (options.probe === true) netLabelSupport = null;
1273
+ let failure = null;
1274
+ if (netLabelSupport !== false) {
1275
+ try {
1276
+ const raw2 = await callExtension("sch_PrimitiveAttribute.createNetLabel", [x, y, net], Math.min(timeoutMs, 4e3));
1277
+ if (raw2 === void 0 || raw2 === null) throw new Error("createNetLabel returned nothing");
1278
+ netLabelSupport = true;
1279
+ return { method: "netLabel", net, x, y, label: summarizePrimitive(raw2) };
1280
+ } catch (error) {
1281
+ const message = error instanceof Error ? error.message : String(error);
1282
+ if (!/not found|not available|not allowed|timed out|timeout|nothing|undefined/i.test(message)) throw error;
1283
+ failure = message;
1284
+ netLabelSupport = false;
1285
+ }
1286
+ }
1287
+ const raw = await callExtension("sch_PrimitiveText.create", [x, y, net, 0, null, null, size, false, false, false, 1], timeoutMs);
1288
+ const label = summarizePrimitive(raw);
1289
+ if (label.primitiveId !== void 0) {
1290
+ placedTextLabels.push({ net: String(net), x, y, primitiveId: label.primitiveId });
1291
+ if (placedTextLabels.length > 2e3) placedTextLabels.shift();
1292
+ }
1293
+ return {
1294
+ method: "text",
1295
+ net,
1296
+ x,
1297
+ y,
1298
+ created: label.primitiveId !== void 0,
1299
+ 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.",
1300
+ ...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." } : {},
1301
+ label
1302
+ };
1303
+ }
1304
+ async function placeAtPin(toolName, args, timeoutMs) {
1305
+ const pinPrimitiveId = String(args.pinPrimitiveId ?? "");
1306
+ if (pinPrimitiveId === "") throw new Error("pinPrimitiveId is required");
1307
+ const isFlag = toolName === "eda_place_net_flag_at_pin";
1308
+ assertNetName(String(args.net ?? ""), toolName);
1309
+ const pin = await callExtension("sch_PrimitivePin.get", [pinPrimitiveId], timeoutMs);
1310
+ const pinRecord = asRecord(pin);
1311
+ const pinX = Number(pickValue(pinRecord, "X", "x"));
1312
+ const pinY = Number(pickValue(pinRecord, "Y", "y"));
1313
+ if (!Number.isFinite(pinX) || !Number.isFinite(pinY)) throw new Error("could not read pin position for " + pinPrimitiveId);
1314
+ const pinRotation = Number(pickValue(pinRecord, "Rotation", "rotation") ?? 0);
1315
+ const direction = pinDirection(pinRotation);
1316
+ const explicitOffset = args.offsetX !== void 0 || args.offsetY !== void 0;
1317
+ const stubLength = args.stubLength === void 0 ? explicitOffset ? 0 : 20 : Math.max(0, Number(args.stubLength));
1318
+ const x = pinX + direction.dx * stubLength + Number(args.offsetX ?? 0);
1319
+ const y = pinY + direction.dy * stubLength + Number(args.offsetY ?? 0);
1320
+ if (args.allowOutsideFrame !== true) {
1321
+ const problem = checkPointInFrame(x, y, args.paper, 5) ?? checkPointAgainstTitleBlock(x, y, args.paper, 5);
1322
+ if (problem !== void 0) {
1323
+ throw new Error(toolName + " refused: " + problem + " - reduce stubLength or pass offsetX/offsetY to place it elsewhere.");
1324
+ }
1325
+ }
1326
+ let stub;
1327
+ if (stubLength > 0 && typeof args.net === "string" && args.net !== "") {
1328
+ stub = summarizePrimitive(await callExtension("sch_PrimitiveWire.create", [
1329
+ [pinX, pinY, x, y],
1330
+ String(args.net),
1331
+ args.color === void 0 ? void 0 : String(args.color),
1332
+ args.lineWidth === void 0 ? void 0 : Number(args.lineWidth),
1333
+ args.lineType === void 0 ? void 0 : Number(args.lineType)
1334
+ ], timeoutMs));
1335
+ }
1336
+ const placement = { pin: { primitiveId: pinPrimitiveId, x: pinX, y: pinY, rotation: pinRotation }, direction, stubLength, x, y, stub };
1337
+ if (isFlag) {
1338
+ const flag = await callExtension("sch_PrimitiveComponent.createNetFlag", [
1339
+ String(args.identification),
1340
+ String(args.net),
1232
1341
  x,
1233
1342
  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
- };
1343
+ args.rotation === void 0 ? pinRotation : Number(args.rotation),
1344
+ Boolean(args.mirror ?? false)
1345
+ ], timeoutMs);
1346
+ return { ...placement, flag: summarizePrimitive(flag) };
1239
1347
  }
1348
+ const label = await placeNetLabel(String(args.net), x, y, args.fontSize, timeoutMs);
1349
+ return { ...label, ...placement };
1240
1350
  }
1241
1351
  function isInternalHelperMissing(error) {
1242
1352
  const message = error instanceof Error ? error.message : String(error);
@@ -1269,10 +1379,10 @@ async function resolveProjectUuid(args, timeoutMs) {
1269
1379
  const teamUuid = String(pickValue(asRecord(team), "uuid", "Uuid") ?? "");
1270
1380
  if (teamUuid === "") continue;
1271
1381
  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);
1382
+ for (const uuid2 of Array.isArray(owned) ? owned.map(String) : []) {
1383
+ if (seen.has(uuid2)) continue;
1384
+ seen.add(uuid2);
1385
+ candidates.push(uuid2);
1276
1386
  }
1277
1387
  }
1278
1388
  }
@@ -1291,6 +1401,40 @@ async function resolveProjectUuid(args, timeoutMs) {
1291
1401
  }
1292
1402
  return { uuid: matched.uuid, matched };
1293
1403
  }
1404
+ async function waitForActiveProject(uuid2, timeoutMs = 4e3) {
1405
+ const deadline = Date.now() + timeoutMs;
1406
+ let lastActive = null;
1407
+ for (; ; ) {
1408
+ try {
1409
+ const info = asRecord(await callExtension("dmt_Project.getCurrentProjectInfo", [], 3e3));
1410
+ lastActive = String(pickValue(info, "uuid", "Uuid") ?? "") || null;
1411
+ if (lastActive === uuid2) return { active: true, project: info };
1412
+ } catch {
1413
+ }
1414
+ if (Date.now() > deadline) return { active: false, project: null, lastActive };
1415
+ await sleep(400);
1416
+ }
1417
+ }
1418
+ function internalIdsOf(info) {
1419
+ const ids = /* @__PURE__ */ new Set();
1420
+ const boards = Array.isArray(asRecord(info).data) ? asRecord(info).data : [];
1421
+ for (const board of boards) {
1422
+ const record = asRecord(board);
1423
+ const boardUuid = String(pickValue(record, "uuid", "Uuid") ?? "");
1424
+ if (boardUuid !== "") ids.add(boardUuid);
1425
+ for (const docKey of ["schematic", "pcb"]) {
1426
+ const doc = asRecord(record[docKey]);
1427
+ const docUuid = String(pickValue(doc, "uuid", "Uuid") ?? "");
1428
+ if (docUuid !== "") ids.add(docUuid);
1429
+ const pages = Array.isArray(doc.pages) ? doc.pages : [];
1430
+ for (const page of pages) {
1431
+ const pageUuid = String(pickValue(asRecord(page), "uuid", "Uuid") ?? "");
1432
+ if (pageUuid !== "") ids.add(pageUuid);
1433
+ }
1434
+ }
1435
+ }
1436
+ return ids;
1437
+ }
1294
1438
  async function executeTool(name, rawArgs, timeoutMs) {
1295
1439
  const args = asRecord(rawArgs);
1296
1440
  const tool = PCB_TOOL_ALIASES[name] ?? name;
@@ -1361,24 +1505,45 @@ async function executeTool(name, rawArgs, timeoutMs) {
1361
1505
  saveTo
1362
1506
  ], toolTimeout(args, timeoutMs));
1363
1507
  const raw = imported?.result ?? imported;
1364
- const uuid = typeof raw === "string" ? raw : typeof raw?.uuid === "string" ? raw.uuid : void 0;
1508
+ const uuid2 = typeof raw === "string" ? raw : typeof raw?.uuid === "string" ? raw.uuid : void 0;
1365
1509
  const landed = raw !== null && raw !== void 0;
1366
1510
  let opened = false;
1367
- if (args.open === true && uuid !== void 0) {
1511
+ if (args.open === true && uuid2 !== void 0) {
1368
1512
  try {
1369
- opened = await callExtension("dmt_Project.openProject", [uuid], timeoutMs) !== false;
1513
+ await callExtension("dmt_Project.openProject", [uuid2], timeoutMs);
1514
+ opened = (await waitForActiveProject(uuid2, 4e3)).active;
1370
1515
  } catch {
1371
1516
  opened = false;
1372
1517
  }
1373
1518
  }
1519
+ let collisions = null;
1520
+ if (landed && uuid2 !== void 0 && args.checkCollisions !== false) {
1521
+ try {
1522
+ const mine = await callExtension("dmt_Project.getProjectInfo", [uuid2], toolTimeout(args, timeoutMs));
1523
+ const myIds = internalIdsOf(mine);
1524
+ const others = await callExtension("dmt_Project.getAllProjectsUuid", [], toolTimeout(args, timeoutMs));
1525
+ const list = (Array.isArray(others) ? others.map(String) : []).filter((other) => other !== uuid2).slice(0, 20);
1526
+ collisions = [];
1527
+ for (const other of list) {
1528
+ const info = await callExtension("dmt_Project.getProjectInfo", [other], timeoutMs).catch(() => void 0);
1529
+ const shared = [...internalIdsOf(info)].filter((sharedId) => myIds.has(sharedId));
1530
+ if (shared.length > 0) {
1531
+ collisions.push({ projectUuid: other, friendlyName: pickValue(asRecord(info), "friendlyName", "FriendlyName") ?? null, sharedIds: shared });
1532
+ }
1533
+ }
1534
+ } catch {
1535
+ collisions = null;
1536
+ }
1537
+ }
1374
1538
  return {
1375
1539
  ok: landed,
1376
1540
  imported: landed,
1377
1541
  name: basename(path),
1378
1542
  size: bytes.length,
1379
1543
  teamUuid,
1380
- uuid: uuid ?? null,
1544
+ uuid: uuid2 ?? null,
1381
1545
  opened,
1546
+ collisions,
1382
1547
  result: raw ?? null,
1383
1548
  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
1549
  };
@@ -1441,10 +1606,10 @@ async function executeTool(name, rawArgs, timeoutMs) {
1441
1606
  const teamUuid = String(pickValue(asRecord(team), "uuid", "Uuid") ?? "");
1442
1607
  if (teamUuid === "") continue;
1443
1608
  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);
1609
+ for (const uuid2 of Array.isArray(owned) ? owned.map(String) : []) {
1610
+ if (seen.has(uuid2)) continue;
1611
+ seen.add(uuid2);
1612
+ list.push(uuid2);
1448
1613
  }
1449
1614
  }
1450
1615
  aggregated = true;
@@ -1452,11 +1617,11 @@ async function executeTool(name, rawArgs, timeoutMs) {
1452
1617
  if (args.resolveNames === false) return { count: list.length, uuids: list, aggregated };
1453
1618
  const limit = Number.isFinite(Number(args.limit)) ? Math.max(1, Number(args.limit)) : list.length;
1454
1619
  const projects = [];
1455
- for (const uuid of list.slice(0, limit)) {
1620
+ for (const uuid2 of list.slice(0, limit)) {
1456
1621
  try {
1457
- projects.push(await callExtension("dmt_Project.getProjectInfo", [uuid], toolTimeout(args, timeoutMs)));
1622
+ projects.push(await callExtension("dmt_Project.getProjectInfo", [uuid2], toolTimeout(args, timeoutMs)));
1458
1623
  } catch {
1459
- projects.push({ uuid });
1624
+ projects.push({ uuid: uuid2 });
1460
1625
  }
1461
1626
  }
1462
1627
  return { count: list.length, resolved: projects.length, aggregated, projects };
@@ -1467,10 +1632,17 @@ async function executeTool(name, rawArgs, timeoutMs) {
1467
1632
  }
1468
1633
  case "eda_open_project": {
1469
1634
  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) };
1635
+ const uuid2 = resolvedProject.uuid;
1636
+ const returned = await callExtension("dmt_Project.openProject", [uuid2], toolTimeout(args, timeoutMs));
1637
+ const activation = await waitForActiveProject(uuid2, args.verifyMs === void 0 ? 4e3 : Number(args.verifyMs));
1638
+ return {
1639
+ opened: activation.active,
1640
+ openReturned: returned !== false,
1641
+ uuid: uuid2,
1642
+ matched: resolvedProject.matched,
1643
+ current: activation.active ? summarizePrimitive(activation.project) : null,
1644
+ ...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." }
1645
+ };
1474
1646
  }
1475
1647
  case "eda_get_workspace": {
1476
1648
  const workspace = await callExtension("dmt_Workspace.getCurrentWorkspaceInfo", [], timeoutMs).catch(() => void 0);
@@ -1545,13 +1717,26 @@ async function executeTool(name, rawArgs, timeoutMs) {
1545
1717
  uuid: args.sourceUuid,
1546
1718
  name: args.sourceName
1547
1719
  }, 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));
1720
+ let copyRaw;
1721
+ try {
1722
+ copyRaw = await callExtension("dmt_Project.copyProject", [
1723
+ source.uuid,
1724
+ args.targetTeamUuid === void 0 ? void 0 : String(args.targetTeamUuid),
1725
+ args.targetFolderUuid === void 0 ? void 0 : String(args.targetFolderUuid),
1726
+ args.newFriendlyName === void 0 ? void 0 : String(args.newFriendlyName),
1727
+ args.newName === void 0 ? void 0 : String(args.newName)
1728
+ ], toolTimeout(args, timeoutMs));
1729
+ } catch (failure) {
1730
+ throw new Error("eda_copy_project failed: " + (failure instanceof Error ? failure.message : String(failure)));
1731
+ }
1732
+ const copyUuid = typeof copyRaw === "string" ? copyRaw : String(pickValue(asRecord(copyRaw), "uuid", "Uuid") ?? "");
1733
+ return {
1734
+ copied: copyUuid !== "",
1735
+ sourceUuid: source.uuid,
1736
+ uuid: copyUuid === "" ? null : copyUuid,
1737
+ raw: copyRaw ?? null,
1738
+ 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
1739
+ };
1555
1740
  return { copied: typeof uuid === "string" && uuid !== "", sourceUuid: source.uuid, uuid: uuid ?? null };
1556
1741
  }
1557
1742
  case "eda_create_project": {
@@ -1568,25 +1753,26 @@ async function executeTool(name, rawArgs, timeoutMs) {
1568
1753
  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
1754
  }
1570
1755
  const folderUuid = args.folderUuid === void 0 ? "" : String(args.folderUuid).trim();
1571
- const uuid = await callExtension("dmt_Project.createProject", [
1756
+ const uuid2 = await callExtension("dmt_Project.createProject", [
1572
1757
  friendlyName,
1573
1758
  args.name === void 0 ? void 0 : String(args.name),
1574
1759
  teamUuid,
1575
1760
  folderUuid === "" ? void 0 : folderUuid,
1576
1761
  args.description === void 0 ? void 0 : String(args.description)
1577
1762
  ], toolTimeout(args, timeoutMs));
1578
- if (typeof uuid !== "string" || uuid === "") {
1763
+ if (typeof uuid2 !== "string" || uuid2 === "") {
1579
1764
  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
1765
  }
1581
1766
  let opened = false;
1582
1767
  if (args.open === true) {
1583
1768
  try {
1584
- opened = await callExtension("dmt_Project.openProject", [uuid], timeoutMs) !== false;
1769
+ await callExtension("dmt_Project.openProject", [uuid2], timeoutMs);
1770
+ opened = (await waitForActiveProject(uuid2, 4e3)).active;
1585
1771
  } catch {
1586
1772
  opened = false;
1587
1773
  }
1588
1774
  }
1589
- return { created: true, uuid, friendlyName, teamUuid, teamSource, folderUuid: folderUuid === "" ? null : folderUuid, opened };
1775
+ return { created: true, uuid: uuid2, friendlyName, teamUuid, teamSource, folderUuid: folderUuid === "" ? null : folderUuid, opened };
1590
1776
  }
1591
1777
  case "eda_launch": {
1592
1778
  const probe = launchProbe();
@@ -1768,68 +1954,90 @@ async function executeTool(name, rawArgs, timeoutMs) {
1768
1954
  const componentPrimitiveId = String(args.componentPrimitiveId ?? "");
1769
1955
  if (componentPrimitiveId === "") throw new Error("componentPrimitiveId is required");
1770
1956
  const raw = await callExtension("sch_PrimitiveComponent.getAllPinsByPrimitiveId", [componentPrimitiveId], timeoutMs);
1771
- if (Array.isArray(raw)) return raw.map(summarizePrimitive);
1772
- return raw;
1957
+ if (!Array.isArray(raw)) return raw;
1958
+ return raw.map((entry) => {
1959
+ const record = asRecord(entry);
1960
+ return {
1961
+ primitiveId: pickValue(record, "PrimitiveId", "primitiveId"),
1962
+ pinNumber: pickValue(record, "PinNumber", "pinNumber") ?? null,
1963
+ pinName: pickValue(record, "PinName", "pinName") ?? null,
1964
+ x: pickValue(record, "X", "x"),
1965
+ y: pickValue(record, "Y", "y"),
1966
+ rotation: pickValue(record, "Rotation", "rotation") ?? 0,
1967
+ pinLength: pickValue(record, "PinLength", "pinLength") ?? null,
1968
+ noConnected: pickValue(record, "NoConnected", "noConnected") ?? null
1969
+ };
1970
+ });
1773
1971
  }
1774
1972
  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
- }
1973
+ case "eda_place_net_label_at_pin":
1974
+ return await placeAtPin(name, args, timeoutMs);
1821
1975
  case "eda_place_net_label": {
1822
1976
  const x = Number(args.x);
1823
1977
  const y = Number(args.y);
1824
1978
  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);
1979
+ return await placeNetLabel(String(args.net), x, y, args.fontSize, timeoutMs, { probe: args.probe === true });
1980
+ }
1981
+ case "eda_place_net_labels": {
1982
+ const items = Array.isArray(args.items) ? args.items.map(asRecord) : [];
1983
+ if (items.length === 0) throw new Error("eda_place_net_labels requires items: [{ pinPrimitiveId, net }] or [{ x, y, net }]");
1984
+ const results = [];
1985
+ for (const item of items) {
1986
+ const net = item.net === void 0 ? "" : String(item.net);
1987
+ try {
1988
+ 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 });
1989
+ results.push({ ok: true, net, ...placed });
1990
+ } catch (error) {
1991
+ results.push({ ok: false, net, error: error instanceof Error ? error.message : String(error) });
1992
+ }
1993
+ }
1994
+ const done = results.filter((entry) => entry.ok).length;
1995
+ return {
1996
+ total: items.length,
1997
+ done,
1998
+ failed: items.length - done,
1999
+ method: netLabelSupport === true ? "netLabel" : "text",
2000
+ results
2001
+ };
1826
2002
  }
1827
2003
  case "eda_get_net_labels": {
1828
- const raw = await callExtension("sch_PrimitiveAttribute.getAll", [
2004
+ const attributes = await callExtension("sch_PrimitiveAttribute.getAll", [
1829
2005
  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) };
2006
+ ], timeoutMs).catch(() => []);
2007
+ const texts = args.parentPrimitiveId === void 0 ? await callExtension("sch_PrimitiveText.getAll", [], timeoutMs).catch(() => []) : [];
2008
+ const entries = [];
2009
+ for (const entry of Array.isArray(attributes) ? attributes : []) {
2010
+ const record = asRecord(entry);
2011
+ entries.push({
2012
+ primitiveId: pickValue(record, "PrimitiveId", "primitiveId"),
2013
+ kind: String(pickValue(record, "PrimitiveType", "primitiveType", "Type", "type") ?? "attribute"),
2014
+ net: pickValue(record, "Net", "net") ?? null,
2015
+ userValue: pickValue(record, "UserValue", "userValue", "Value", "value") ?? null,
2016
+ x: pickValue(record, "X", "x"),
2017
+ y: pickValue(record, "Y", "y")
2018
+ });
2019
+ }
2020
+ for (const entry of Array.isArray(texts) ? texts : []) {
2021
+ const record = asRecord(entry);
2022
+ const content = String(pickValue(record, "Content", "content", "Text", "text") ?? "");
2023
+ if (content === "") continue;
2024
+ entries.push({
2025
+ primitiveId: pickValue(record, "PrimitiveId", "primitiveId"),
2026
+ kind: "text",
2027
+ net: content,
2028
+ userValue: content,
2029
+ x: pickValue(record, "X", "x"),
2030
+ y: pickValue(record, "Y", "y")
2031
+ });
2032
+ }
2033
+ return {
2034
+ count: entries.length,
2035
+ attributeCount: Array.isArray(attributes) ? attributes.length : 0,
2036
+ textCount: Array.isArray(texts) ? texts.length : 0,
2037
+ sessionTextLabels: placedTextLabels.length,
2038
+ note: 'kind "text" entries are plain texts (no electrical meaning). Delete any entry with eda_delete_primitive(kind: "netlabel"|"netport"|"netflag"|"text", primitiveId).',
2039
+ labels: entries
2040
+ };
1833
2041
  }
1834
2042
  case "eda_get_active_document": {
1835
2043
  const info = await callExtension("dmt_SelectControl.getCurrentDocumentInfo", [], timeoutMs);
@@ -1955,8 +2163,33 @@ async function executeTool(name, rawArgs, timeoutMs) {
1955
2163
  ], timeoutMs);
1956
2164
  case "eda_save_document":
1957
2165
  return await callExtension("sch_Document.save", [], timeoutMs);
1958
- case "eda_zoom_to_fit":
1959
- return await callExtension("dmt_EditorControl.zoomToAllPrimitives", [args.tabId], timeoutMs);
2166
+ case "eda_zoom_to_fit": {
2167
+ const raw = await callExtension("dmt_EditorControl.zoomToAllPrimitives", [args.tabId], timeoutMs);
2168
+ const record = asRecord(raw);
2169
+ const left = Number(pickValue(record, "left", "Left"));
2170
+ const right = Number(pickValue(record, "right", "Right"));
2171
+ const bottom = Number(pickValue(record, "bottom", "Bottom"));
2172
+ const top = Number(pickValue(record, "top", "Top"));
2173
+ if (![left, right, bottom, top].every((value) => Number.isFinite(value))) return raw;
2174
+ const frame = paperOf(args.paper).frame;
2175
+ const clamped = {
2176
+ left: Math.max(frame.left, left),
2177
+ right: Math.min(frame.right, right),
2178
+ bottom: Math.max(frame.bottom, bottom),
2179
+ top: Math.min(frame.top, top)
2180
+ };
2181
+ if (clamped.left >= clamped.right || clamped.bottom >= clamped.top) {
2182
+ return { ...record, requested: { left, right, bottom, top }, clamped: null, note: "the primitive bounds do not overlap the page frame" };
2183
+ }
2184
+ await callExtension("dmt_EditorControl.zoomToRegion", [
2185
+ clamped.left,
2186
+ clamped.right,
2187
+ clamped.top,
2188
+ clamped.bottom,
2189
+ args.tabId
2190
+ ], timeoutMs);
2191
+ return { ...record, requested: { left, right, bottom, top }, clamped };
2192
+ }
1960
2193
  case "eda_zoom_to_region":
1961
2194
  return await callExtension("dmt_EditorControl.zoomToRegion", [
1962
2195
  Number(args.left),
@@ -2000,24 +2233,98 @@ async function executeTool(name, rawArgs, timeoutMs) {
2000
2233
  const parsed = await parseNetlist(Array.isArray(all) ? all : project, timeoutMs);
2001
2234
  return parsed;
2002
2235
  }
2003
- case "eda_run_drc":
2004
- return await callExtension("sch_Drc.check", [args.strict === void 0 ? true : Boolean(args.strict), false, true], timeoutMs);
2236
+ case "eda_run_drc": {
2237
+ const verbose = await callExtension("sch_Drc.check", [
2238
+ args.strict === void 0 ? true : Boolean(args.strict),
2239
+ false,
2240
+ true
2241
+ ], toolTimeout(args, timeoutMs));
2242
+ if (Array.isArray(verbose)) {
2243
+ return {
2244
+ ok: verbose.length === 0,
2245
+ count: verbose.length,
2246
+ entries: verbose.map((entry) => {
2247
+ const record = asRecord(entry);
2248
+ return {
2249
+ type: pickValue(record, "Type", "type", "Level", "level") ?? null,
2250
+ message: pickValue(record, "Message", "message", "Error", "error", "Description", "description") ?? null,
2251
+ primitiveId: pickValue(record, "PrimitiveId", "primitiveId") ?? null,
2252
+ x: pickValue(record, "X", "x") ?? null,
2253
+ y: pickValue(record, "Y", "y") ?? null,
2254
+ raw: entry
2255
+ };
2256
+ }),
2257
+ hint: 'Field names vary by EDA build; "raw" keeps the untouched record.'
2258
+ };
2259
+ }
2260
+ return { ok: Boolean(verbose), count: null, entries: [], raw: verbose };
2261
+ }
2005
2262
  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);
2263
+ case "eda_auto_route": {
2264
+ if (ALLOW_AUTO_LAYOUT !== true && args.force !== true) {
2265
+ throw new Error(
2266
+ 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.",
2267
+ "",
2268
+ "Do this instead, in order:",
2269
+ "1. eda_get_active_document \u2014 make sure a schematic page is active (eda_open_document if not).",
2270
+ "2. eda_get_page_info \u2014 read the A4 frame and the safe areas before placing anything.",
2271
+ "3. eda_get_components \u2014 see what is already placed and where (x/y + bounding boxes) to avoid overlap.",
2272
+ "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.",
2273
+ "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).",
2274
+ "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.",
2275
+ "7. Call eda_workflow_guide for the full standard."
2276
+ );
2277
+ }
2278
+ const payload = Array.isArray(args.uuids) ? { uuids: args.uuids.map(String) } : void 0;
2279
+ return await callExtension(name === "eda_auto_layout" ? "sch_Document.autoLayout" : "sch_Document.autoRouting", [payload], toolTimeout(args, timeoutMs));
2280
+ }
2281
+ case "eda_export_netlist": {
2282
+ const payload = await callExtension("sch_ManufactureData.getNetlistFile", [
2283
+ void 0,
2284
+ typeof args.type === "string" && args.type !== "" ? args.type : void 0
2285
+ ], toolTimeout(args, timeoutMs));
2286
+ const text = typeof payload === "string" ? payload : payload !== null && typeof payload === "object" && typeof payload.text === "string" ? payload.text : void 0;
2287
+ if (text === void 0) return { parsed: false, netlist: payload ?? null };
2288
+ if (args.format === "text") return { parsed: false, netlistText: text, bytes: text.length };
2289
+ try {
2290
+ return { parsed: true, bytes: text.length, netlist: JSON.parse(text) };
2291
+ } catch {
2292
+ return { parsed: false, bytes: text.length, netlistText: text };
2293
+ }
2294
+ }
2295
+ case "eda_export_bom": {
2296
+ const format = args.format === "xlsx" ? "xlsx" : "csv";
2297
+ const payload = await callExtension("sch_ManufactureData.getBomFile", [void 0, format], toolTimeout(args, timeoutMs));
2298
+ if (format === "xlsx") return payload;
2299
+ const raw = typeof payload === "string" ? payload : payload !== null && typeof payload === "object" && typeof payload.text === "string" ? payload.text : "";
2300
+ return { format, bom: decodeTextPayload(raw) };
2301
+ }
2302
+ case "eda_delete_primitive": {
2303
+ const kind = String(args.kind);
2304
+ const className = kind === "netport" || kind === "netflag" || kind === "netlabel" ? "sch_PrimitiveAttribute" : primitiveKindToClass(kind);
2305
+ return await callExtension(className + ".delete", [String(args.primitiveId)], timeoutMs);
2306
+ }
2307
+ case "eda_move_component":
2308
+ return await callExtension("sch_PrimitiveComponent.modify", [String(args.primitiveId), {
2309
+ x: args.x === void 0 ? void 0 : Number(args.x),
2310
+ y: args.y === void 0 ? void 0 : Number(args.y),
2311
+ rotation: args.rotation === void 0 ? void 0 : Number(args.rotation),
2312
+ mirror: args.mirror === void 0 ? void 0 : Boolean(args.mirror)
2313
+ }], timeoutMs);
2314
+ case "eda_list_api_methods":
2315
+ return { count: ALLOWED_API.size, methods: [...ALLOWED_API].sort() };
2316
+ case "eda_workflow_guide":
2317
+ return { summary: WORKFLOW_SUMMARY, guide: WORKFLOW_GUIDE, autoLayoutAllowed: ALLOW_AUTO_LAYOUT };
2318
+ case "eda_workflow_guide":
2319
+ return { summary: WORKFLOW_SUMMARY, guide: WORKFLOW_GUIDE, autoLayoutAllowed: ALLOW_AUTO_LAYOUT };
2015
2320
  case "eda_create_board":
2016
2321
  return await callExtension("dmt_Board.createBoard", [args.schematicUuid, args.pcbUuid], timeoutMs);
2017
2322
  case "eda_api_call": {
2018
2323
  if (!ALLOW_RAW_API) throw new Error("eda_api_call is disabled by settings (allowRawApi=false)");
2019
2324
  const method = String(args.method ?? "");
2020
- if (!ALLOWED_API.has(method)) throw new Error("API method not allowed: " + method);
2325
+ if (!ALLOWED_API.has(method)) {
2326
+ 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.");
2327
+ }
2021
2328
  return await callExtension(method, Array.isArray(args.args) ? args.args : [], timeoutMs);
2022
2329
  }
2023
2330
  case "pcb_get_components": {
@@ -2196,17 +2503,17 @@ async function executeTool(name, rawArgs, timeoutMs) {
2196
2503
  case "pcb_save_document":
2197
2504
  return await callExtension("pcb_Document.save", [], timeoutMs);
2198
2505
  case "pcb_import_changes": {
2199
- const uuid = args.schematicUuid === void 0 || args.schematicUuid === null ? null : String(args.schematicUuid);
2506
+ const uuid2 = args.schematicUuid === void 0 || args.schematicUuid === null ? null : String(args.schematicUuid);
2200
2507
  if (args.wait === true) {
2201
- return await callExtension("pcb_Document.importChanges", [uuid === null ? void 0 : uuid], toolTimeout(args, timeoutMs));
2508
+ return await callExtension("pcb_Document.importChanges", [uuid2 === null ? void 0 : uuid2], toolTimeout(args, timeoutMs));
2202
2509
  }
2203
2510
  pcbComponentBaseline = await pcbComponentIds(timeoutMs);
2204
2511
  let started;
2205
2512
  try {
2206
- started = asRecord(await callExtension("dsh.internal.jobStart", ["importChanges", uuid], timeoutMs));
2513
+ started = asRecord(await callExtension("dsh.internal.jobStart", ["importChanges", uuid2], timeoutMs));
2207
2514
  } catch (error) {
2208
2515
  if (!isInternalHelperMissing(error)) throw error;
2209
- const result2 = await callExtension("pcb_Document.importChanges", [uuid === null ? void 0 : uuid], toolTimeout(args, timeoutMs));
2516
+ const result2 = await callExtension("pcb_Document.importChanges", [uuid2 === null ? void 0 : uuid2], toolTimeout(args, timeoutMs));
2210
2517
  const after = await pcbComponentIds(timeoutMs);
2211
2518
  return {
2212
2519
  triggered: true,