@vcad/mcp 0.11.0-main.159 → 0.11.0-main.163

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,4 +9,4 @@ vcad's MCP server as a self-contained bundle (server + BRep kernel WASM).
9
9
  Or install vcad.app, which registers this server with your agents on first launch.
10
10
  The agent skill ships at `skill/SKILL.md`.
11
11
 
12
- Built from 6ca36502681cbcd14c0248e16ea270d62193338f at 2026-11-11T16:44:44.291Z. Source: https://github.com/muni-works/vcad
12
+ Built from 816ee12b085c12b403a35ebfedb6c276e175a3af at 2026-11-11T21:40:44.212Z. Source: https://github.com/muni-works/vcad
package/index.mjs CHANGED
@@ -10277,6 +10277,11 @@ function evaluateOp(op, nodes, Solid2, cache, depth = 0) {
10277
10277
  }
10278
10278
  case "Sketch2D":
10279
10279
  return Solid2.empty();
10280
+ // A sketch attached to a named face: like Sketch2D, it is not a solid
10281
+ // itself. (The native evaluator resolves the face; this legacy TS path
10282
+ // only needs the switch to stay exhaustive.)
10283
+ case "SketchOnFace":
10284
+ return Solid2.empty();
10280
10285
  case "Extrude": {
10281
10286
  const sketchNode = nodes[String(op.sketch)];
10282
10287
  if (!sketchNode) {
@@ -11307,9 +11312,9 @@ function workerTransport(worker) {
11307
11312
  pending.delete(id);
11308
11313
  });
11309
11314
  return {
11310
- request: (req) => new Promise((resolve8) => {
11315
+ request: (req) => new Promise((resolve9) => {
11311
11316
  const id = next++;
11312
- pending.set(id, resolve8);
11317
+ pending.set(id, resolve9);
11313
11318
  worker.postMessage({ id, req });
11314
11319
  }),
11315
11320
  dispose: () => worker.terminate()
@@ -12188,11 +12193,11 @@ var init_analyze = __esm({
12188
12193
  type: "module"
12189
12194
  });
12190
12195
  this.worker = worker;
12191
- this.ready = new Promise((resolve8, reject) => {
12196
+ this.ready = new Promise((resolve9, reject) => {
12192
12197
  const onMessage = (e) => {
12193
12198
  const { type, id, message, analysis } = e.data;
12194
12199
  if (type === "ready") {
12195
- resolve8();
12200
+ resolve9();
12196
12201
  return;
12197
12202
  }
12198
12203
  if (type === "error" && id === null) {
@@ -12216,9 +12221,9 @@ var init_analyze = __esm({
12216
12221
  return this.ready;
12217
12222
  }
12218
12223
  request(msg, transfer = []) {
12219
- return this.ensureWorker().then(() => new Promise((resolve8, reject) => {
12224
+ return this.ensureWorker().then(() => new Promise((resolve9, reject) => {
12220
12225
  const id = this.nextId++;
12221
- this.pending.set(id, { resolve: resolve8, reject });
12226
+ this.pending.set(id, { resolve: resolve9, reject });
12222
12227
  this.worker.postMessage({ ...msg, id }, transfer);
12223
12228
  }));
12224
12229
  }
@@ -12835,11 +12840,11 @@ var init_dist = __esm({
12835
12840
  return;
12836
12841
  try {
12837
12842
  const worker = new Worker(new URL("./eval-worker.js", import.meta.url), { type: "module" });
12838
- this.workerReady = new Promise((resolve8, reject) => {
12843
+ this.workerReady = new Promise((resolve9, reject) => {
12839
12844
  const onMessage = (e) => {
12840
12845
  if (e.data.type === "ready") {
12841
12846
  worker.removeEventListener("message", onMessage);
12842
- resolve8();
12847
+ resolve9();
12843
12848
  } else if (e.data.type === "error" && e.data.id === null) {
12844
12849
  worker.removeEventListener("message", onMessage);
12845
12850
  console.warn("[ENGINE] Worker WASM init failed:", e.data.message);
@@ -13062,7 +13067,7 @@ var init_dist = __esm({
13062
13067
  const worker = this.worker;
13063
13068
  const id = Math.random().toString(36).slice(2);
13064
13069
  const skipClash = options.skipClashDetection ?? false;
13065
- return new Promise((resolve8, reject) => {
13070
+ return new Promise((resolve9, reject) => {
13066
13071
  const onMessage = (e) => {
13067
13072
  if (e.data.id !== id)
13068
13073
  return;
@@ -13072,7 +13077,7 @@ var init_dist = __esm({
13072
13077
  if (scene.timing && _Engine.DEV) {
13073
13078
  _Engine.logTiming(scene.timing, e.data.workerTotalMs);
13074
13079
  }
13075
- resolve8(scene);
13080
+ resolve9(scene);
13076
13081
  } else if (e.data.type === "error") {
13077
13082
  reject(new Error(e.data.message));
13078
13083
  }
@@ -17360,7 +17365,7 @@ function isRef(value) {
17360
17365
  function cloneIssues(issues) {
17361
17366
  return issues.map((iss) => iss.path ? { ...iss, path: iss.path.slice() } : { ...iss });
17362
17367
  }
17363
- function isRecursive(inst, stack, resolve8) {
17368
+ function isRecursive(inst, stack, resolve9) {
17364
17369
  const cached3 = recursive.get(inst);
17365
17370
  if (cached3 !== void 0)
17366
17371
  return cached3 ? PROVEN : NONE;
@@ -17370,7 +17375,7 @@ function isRecursive(inst, stack, resolve8) {
17370
17375
  let result = NONE;
17371
17376
  const check = (child) => {
17372
17377
  if (result !== PROVEN && child?._zod) {
17373
- const answer = isRecursive(child, stack, resolve8);
17378
+ const answer = isRecursive(child, stack, resolve9);
17374
17379
  if (answer > result)
17375
17380
  result = answer;
17376
17381
  }
@@ -17381,7 +17386,7 @@ function isRecursive(inst, stack, resolve8) {
17381
17386
  const desc = Object.getOwnPropertyDescriptor(sh, key3);
17382
17387
  if (spread && !desc.enumerable)
17383
17388
  continue;
17384
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve8) : NONE;
17389
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve9) : NONE;
17385
17390
  if (child > answer)
17386
17391
  answer = child;
17387
17392
  }
@@ -17445,7 +17450,7 @@ function isRecursive(inst, stack, resolve8) {
17445
17450
  break;
17446
17451
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
17447
17452
  case "lazy": {
17448
- const inner = def._cachedInner ?? (resolve8 ? inst._zod.innerType : void 0);
17453
+ const inner = def._cachedInner ?? (resolve9 ? inst._zod.innerType : void 0);
17449
17454
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
17450
17455
  break;
17451
17456
  }
@@ -22199,12 +22204,12 @@ var init_stdio2 = __esm({
22199
22204
  this.onclose?.();
22200
22205
  }
22201
22206
  send(message) {
22202
- return new Promise((resolve8) => {
22207
+ return new Promise((resolve9) => {
22203
22208
  const json = serializeMessage(message);
22204
22209
  if (this._stdout.write(json)) {
22205
- resolve8();
22210
+ resolve9();
22206
22211
  } else {
22207
- this._stdout.once("drain", resolve8);
22212
+ this._stdout.once("drain", resolve9);
22208
22213
  }
22209
22214
  });
22210
22215
  }
@@ -23241,7 +23246,7 @@ var init_protocol = __esm({
23241
23246
  return;
23242
23247
  }
23243
23248
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
23244
- await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
23249
+ await new Promise((resolve9) => setTimeout(resolve9, pollInterval));
23245
23250
  options?.signal?.throwIfAborted();
23246
23251
  }
23247
23252
  } catch (error2) {
@@ -23258,7 +23263,7 @@ var init_protocol = __esm({
23258
23263
  */
23259
23264
  request(request, resultSchema, options) {
23260
23265
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
23261
- return new Promise((resolve8, reject) => {
23266
+ return new Promise((resolve9, reject) => {
23262
23267
  const earlyReject = (error2) => {
23263
23268
  reject(error2);
23264
23269
  };
@@ -23336,7 +23341,7 @@ var init_protocol = __esm({
23336
23341
  if (!parseResult.success) {
23337
23342
  reject(parseResult.error);
23338
23343
  } else {
23339
- resolve8(parseResult.data);
23344
+ resolve9(parseResult.data);
23340
23345
  }
23341
23346
  } catch (error2) {
23342
23347
  reject(error2);
@@ -23597,12 +23602,12 @@ var init_protocol = __esm({
23597
23602
  }
23598
23603
  } catch {
23599
23604
  }
23600
- return new Promise((resolve8, reject) => {
23605
+ return new Promise((resolve9, reject) => {
23601
23606
  if (signal.aborted) {
23602
23607
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
23603
23608
  return;
23604
23609
  }
23605
- const timeoutId = setTimeout(resolve8, interval);
23610
+ const timeoutId = setTimeout(resolve9, interval);
23606
23611
  signal.addEventListener("abort", () => {
23607
23612
  clearTimeout(timeoutId);
23608
23613
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -26629,7 +26634,7 @@ var require_compile = __commonJS({
26629
26634
  const schOrFunc = root.refs[ref];
26630
26635
  if (schOrFunc)
26631
26636
  return schOrFunc;
26632
- let _sch = resolve8.call(this, root, ref);
26637
+ let _sch = resolve9.call(this, root, ref);
26633
26638
  if (_sch === void 0) {
26634
26639
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
26635
26640
  const { schemaId } = this.opts;
@@ -26656,7 +26661,7 @@ var require_compile = __commonJS({
26656
26661
  function sameSchemaEnv(s1, s2) {
26657
26662
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
26658
26663
  }
26659
- function resolve8(root, ref) {
26664
+ function resolve9(root, ref) {
26660
26665
  let sch;
26661
26666
  while (typeof (sch = this.refs[ref]) == "string")
26662
26667
  ref = sch;
@@ -27231,7 +27236,7 @@ var require_fast_uri = __commonJS({
27231
27236
  }
27232
27237
  return uri;
27233
27238
  }
27234
- function resolve8(baseURI, relativeURI, options) {
27239
+ function resolve9(baseURI, relativeURI, options) {
27235
27240
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
27236
27241
  const resolved = resolveComponent(parse5(baseURI, schemelessOptions), parse5(relativeURI, schemelessOptions), schemelessOptions, true);
27237
27242
  schemelessOptions.skipEscape = true;
@@ -27458,7 +27463,7 @@ var require_fast_uri = __commonJS({
27458
27463
  var fastUri = {
27459
27464
  SCHEMES,
27460
27465
  normalize: normalize2,
27461
- resolve: resolve8,
27466
+ resolve: resolve9,
27462
27467
  resolveComponent,
27463
27468
  equal,
27464
27469
  serialize,
@@ -33630,21 +33635,21 @@ var require_react_development = __commonJS({
33630
33635
  );
33631
33636
  actScopeDepth = prevActScopeDepth;
33632
33637
  }
33633
- function recursivelyFlushAsyncActWork(returnValue, resolve8, reject) {
33638
+ function recursivelyFlushAsyncActWork(returnValue, resolve9, reject) {
33634
33639
  var queue = ReactSharedInternals.actQueue;
33635
33640
  if (null !== queue)
33636
33641
  if (0 !== queue.length)
33637
33642
  try {
33638
33643
  flushActQueue(queue);
33639
33644
  enqueueTask(function() {
33640
- return recursivelyFlushAsyncActWork(returnValue, resolve8, reject);
33645
+ return recursivelyFlushAsyncActWork(returnValue, resolve9, reject);
33641
33646
  });
33642
33647
  return;
33643
33648
  } catch (error2) {
33644
33649
  ReactSharedInternals.thrownErrors.push(error2);
33645
33650
  }
33646
33651
  else ReactSharedInternals.actQueue = null;
33647
- 0 < ReactSharedInternals.thrownErrors.length ? (queue = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(queue)) : resolve8(returnValue);
33652
+ 0 < ReactSharedInternals.thrownErrors.length ? (queue = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(queue)) : resolve9(returnValue);
33648
33653
  }
33649
33654
  function flushActQueue(queue) {
33650
33655
  if (!isFlushing) {
@@ -33831,7 +33836,7 @@ var require_react_development = __commonJS({
33831
33836
  ));
33832
33837
  });
33833
33838
  return {
33834
- then: function(resolve8, reject) {
33839
+ then: function(resolve9, reject) {
33835
33840
  didAwaitActCall = true;
33836
33841
  thenable.then(
33837
33842
  function(returnValue) {
@@ -33841,7 +33846,7 @@ var require_react_development = __commonJS({
33841
33846
  flushActQueue(queue), enqueueTask(function() {
33842
33847
  return recursivelyFlushAsyncActWork(
33843
33848
  returnValue,
33844
- resolve8,
33849
+ resolve9,
33845
33850
  reject
33846
33851
  );
33847
33852
  });
@@ -33855,7 +33860,7 @@ var require_react_development = __commonJS({
33855
33860
  ReactSharedInternals.thrownErrors.length = 0;
33856
33861
  reject(_thrownError);
33857
33862
  }
33858
- } else resolve8(returnValue);
33863
+ } else resolve9(returnValue);
33859
33864
  },
33860
33865
  function(error2) {
33861
33866
  popActScope(prevActQueue, prevActScopeDepth);
@@ -33877,15 +33882,15 @@ var require_react_development = __commonJS({
33877
33882
  if (0 < ReactSharedInternals.thrownErrors.length)
33878
33883
  throw callback = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, callback;
33879
33884
  return {
33880
- then: function(resolve8, reject) {
33885
+ then: function(resolve9, reject) {
33881
33886
  didAwaitActCall = true;
33882
33887
  0 === prevActScopeDepth ? (ReactSharedInternals.actQueue = queue, enqueueTask(function() {
33883
33888
  return recursivelyFlushAsyncActWork(
33884
33889
  returnValue$jscomp$0,
33885
- resolve8,
33890
+ resolve9,
33886
33891
  reject
33887
33892
  );
33888
- })) : resolve8(returnValue$jscomp$0);
33893
+ })) : resolve9(returnValue$jscomp$0);
33889
33894
  }
33890
33895
  };
33891
33896
  };
@@ -44782,6 +44787,100 @@ var init_CHANGELOG = __esm({
44782
44787
  "export_cad"
44783
44788
  ]
44784
44789
  },
44790
+ {
44791
+ id: "2026-11-11-step-interop-audit",
44792
+ version: "0.11.0",
44793
+ date: "2026-11-11",
44794
+ category: "fix",
44795
+ title: "STEP import: no hangs, colours, assembly placement",
44796
+ summary: "KiCad and FreeCAD STEP files no longer hang; reversed face bounds read right; colours, part names and assembly instance poses import and colours export; rational surfaces export.",
44797
+ features: [
44798
+ "step",
44799
+ "import",
44800
+ "export",
44801
+ "native-app"
44802
+ ]
44803
+ },
44804
+ {
44805
+ id: "2026-11-11-sketch-on-face",
44806
+ version: "undefined",
44807
+ date: "2026-11-11",
44808
+ category: "feat",
44809
+ title: "Sketches drawn on a face follow that face",
44810
+ summary: "A sketch drawn on a named face now re-attaches to it on every rebuild, so a boss on a plate's top stays on top when the plate gets thicker. An unresolvable face fails closed.",
44811
+ features: [
44812
+ "sketch",
44813
+ "naming",
44814
+ "history"
44815
+ ]
44816
+ },
44817
+ {
44818
+ id: "2026-11-11-sketch-inference-smart-dimension",
44819
+ version: "0.11.0",
44820
+ date: "2026-11-11",
44821
+ category: "feat",
44822
+ title: "Mac sketching: inference, Smart Dimension, trim and fillet",
44823
+ summary: "The Mac app's sketcher infers constraints while drawing, sizes or pairs with one Smart Dimension tool, colours by solve state, drags to solve, and adds construction, trim, extend, fillet, mirror, patterns and live profile shading.",
44824
+ features: [
44825
+ "sketch",
44826
+ "constraints",
44827
+ "mac-app"
44828
+ ]
44829
+ },
44830
+ {
44831
+ id: "2026-11-11-revolve-profile-on-axis",
44832
+ version: "0.11.0",
44833
+ date: "2026-11-11",
44834
+ category: "fix",
44835
+ title: "Revolve profiles that touch their axis",
44836
+ summary: "Sketch revolve now accepts profiles with vertices on the axis: axis edges add no face, perpendicular edges become full discs, slanted edges become cones with an apex.",
44837
+ features: [
44838
+ "revolve",
44839
+ "sketch"
44840
+ ]
44841
+ },
44842
+ {
44843
+ id: "2026-11-11-print-from-vcad",
44844
+ version: "0.11.0",
44845
+ date: "2026-11-11",
44846
+ category: "feat",
44847
+ title: "Print to a Bambu printer straight from vcad",
44848
+ summary: "3D Print is now Orient \u2192 Slice \u2192 Printer \u2192 Print: auto-orientation for fewest supports, LAN pairing, live printer and AMS status, a confirmed gated send, and pause/resume/stop.",
44849
+ features: [
44850
+ "3d-print",
44851
+ "slicer",
44852
+ "printer",
44853
+ "mac-app"
44854
+ ]
44855
+ },
44856
+ {
44857
+ id: "2026-11-11-per-instance-agent-discovery",
44858
+ version: "undefined",
44859
+ date: "2026-11-11",
44860
+ category: "fix",
44861
+ title: "Agents no longer drive the wrong vcad.app window",
44862
+ summary: "Each vcad.app instance registers its own discovery file; clients pick one via VCAD_APP_PID/URL/DOCUMENT, every bridge result names its window, and a relaunch is reported.",
44863
+ features: [
44864
+ "agent-bridge",
44865
+ "mcp",
44866
+ "mac-app"
44867
+ ]
44868
+ },
44869
+ {
44870
+ id: "2026-11-11-one-click-drawings",
44871
+ version: "0.11.0",
44872
+ date: "2026-11-11",
44873
+ category: "feat",
44874
+ title: "One-click drawings with automatic dimensions",
44875
+ summary: "Drawing workspace lays out front/top/right/iso on an auto-sized sheet with hole callouts, center marks and editable dimensions; exports vector PDF and true-scale DXF.",
44876
+ features: [
44877
+ "drawings",
44878
+ "drafting",
44879
+ "native-app",
44880
+ "gdt"
44881
+ ],
44882
+ mcpTools: []
44883
+ },
44785
44884
  {
44786
44885
  id: "2026-11-11-one-breath-install",
44787
44886
  version: "0.11.0",
@@ -44798,6 +44897,20 @@ var init_CHANGELOG = __esm({
44798
44897
  "server_info"
44799
44898
  ]
44800
44899
  },
44900
+ {
44901
+ id: "2026-11-11-local-data-trust",
44902
+ version: "undefined",
44903
+ date: "2026-11-11",
44904
+ category: "feat",
44905
+ title: "Autosave, crash recovery, and version history",
44906
+ summary: "The Mac app autosaves every few seconds, offers to restore unsaved work after a crash, and keeps per-document versions you can preview, restore, or duplicate.",
44907
+ features: [
44908
+ "mac-app",
44909
+ "autosave",
44910
+ "versions",
44911
+ "recovery"
44912
+ ]
44913
+ },
44801
44914
  {
44802
44915
  id: "2026-11-11-live-receipt-badges",
44803
44916
  version: "0.11.0",
@@ -44819,6 +44932,46 @@ var init_CHANGELOG = __esm({
44819
44932
  "ask_user"
44820
44933
  ]
44821
44934
  },
44935
+ {
44936
+ id: "2026-11-11-history-edits-keep-references",
44937
+ version: "undefined",
44938
+ date: "2026-11-11",
44939
+ category: "feat",
44940
+ title: "Edits to earlier features keep downstream references",
44941
+ summary: "Extruded faces are named after their sketch segments and fillets, chamfers and shells keep face names, so picked edges survive edits. Move Up/Down refuses moves that would drop a feature.",
44942
+ features: [
44943
+ "naming",
44944
+ "feature-tree",
44945
+ "history"
44946
+ ]
44947
+ },
44948
+ {
44949
+ id: "2026-11-11-fusion-model-edges",
44950
+ version: "1.0.0",
44951
+ date: "2026-11-11",
44952
+ category: "feat",
44953
+ title: "Model edges drawn like Fusion 360",
44954
+ summary: "The Mac app now outlines every B-rep face boundary, tangent fillet edges included, and draws outlines on assembly instances too. Mesh-only parts keep crease edges.",
44955
+ features: [
44956
+ "viewport",
44957
+ "edges",
44958
+ "assembly"
44959
+ ]
44960
+ },
44961
+ {
44962
+ id: "2026-11-11-direct-manipulation",
44963
+ version: "undefined",
44964
+ date: "2026-11-11",
44965
+ category: "feat",
44966
+ title: "Direct manipulation: press-pull, handles, live dimensions",
44967
+ summary: "Mac app: press-pull planar faces into real features, drag on-canvas handles for extrude/fillet/shell, type exact values into a live dimension, and a redesigned hairline gizmo.",
44968
+ features: [
44969
+ "mac-app",
44970
+ "viewport",
44971
+ "gizmo",
44972
+ "press-pull"
44973
+ ]
44974
+ },
44822
44975
  {
44823
44976
  id: "2026-11-11-counterbore-breakout",
44824
44977
  version: "0.11.0",
@@ -44831,6 +44984,32 @@ var init_CHANGELOG = __esm({
44831
44984
  "kernel"
44832
44985
  ]
44833
44986
  },
44987
+ {
44988
+ id: "2026-11-11-assembly-export-clearance-step-cache",
44989
+ version: "undefined",
44990
+ date: "2026-11-11",
44991
+ category: "fix",
44992
+ title: "Assembly export, interference and big STEP opens fixed",
44993
+ summary: "Assembly GLB/STL no longer duplicates every part at the origin, STEP exports posed instances, interference scales to big assemblies, and STEP assemblies parse once.",
44994
+ features: [
44995
+ "assembly",
44996
+ "export",
44997
+ "step",
44998
+ "clearance"
44999
+ ]
45000
+ },
45001
+ {
45002
+ id: "2026-11-11-arc-extrude-boolean-tearing",
45003
+ version: "0.11.0",
45004
+ date: "2026-11-11",
45005
+ category: "fix",
45006
+ title: "No more tearing when cutting into extruded arc walls",
45007
+ summary: "Booleans that split the curved wall of an arc-bearing sketch extrude no longer tear the wall away from its caps; results stay watertight.",
45008
+ features: [
45009
+ "booleans",
45010
+ "extrude"
45011
+ ]
45012
+ },
44834
45013
  {
44835
45014
  id: "2026-11-11-agent-cursor",
44836
45015
  version: "0.11.0",
@@ -45116,6 +45295,22 @@ var init_CHANGELOG = __esm({
45116
45295
  "inspect_cad"
45117
45296
  ]
45118
45297
  },
45298
+ {
45299
+ id: "2026-11-10-import-from-photos",
45300
+ version: "0.11.0",
45301
+ date: "2026-11-10",
45302
+ category: "feat",
45303
+ title: "Experimental: import a part from photos",
45304
+ summary: "Mac app: photos of a part beside a tag36h11 AprilTag become an editable document with named, \xB1\u03C3 dimensions. Tested on synthetic renders only, not yet real photos.",
45305
+ features: [
45306
+ "native",
45307
+ "mac",
45308
+ "import",
45309
+ "image-to-cad",
45310
+ "parameters",
45311
+ "experimental"
45312
+ ]
45313
+ },
45119
45314
  {
45120
45315
  id: "2026-11-10-gym-step-angle-units",
45121
45316
  version: "0.11.0",
@@ -47192,6 +47387,20 @@ var init_CHANGELOG = __esm({
47192
47387
  "manufacture"
47193
47388
  ]
47194
47389
  },
47390
+ {
47391
+ id: "2026-10-13-cone-frustum-radii",
47392
+ version: "0.10.0",
47393
+ date: "2026-10-13",
47394
+ category: "fix",
47395
+ title: "Cone frustum keeps its radii when the top is wider",
47396
+ summary: "A cone/frustum whose top radius exceeds its base no longer swaps the two radii: the apex is now placed from the base radius, so the solid matches the requested geometry.",
47397
+ details: "ConeSurface::from_frustum placed the apex from radius_top in the radius_bottom < radius_top branch, so the lateral surface came out with the radii swapped even though the B-rep circles were correct: Solid::cone(2, 10, 10) produced radius 10 at z=0 and 18 at z=10 instead of 2 and 10, with a volume about 3x the analytic value. Volume and bounding box are symmetric in the two radii, so neither caught it; it only showed in an asymmetric operation or a per-z-band radius sample. This affected any cone with radius_top > radius_bottom, including cylinder(r1 < r2) imported from OpenSCAD and zero-radius cones such as cylinder(h=10, r1=0, r2=5), which additionally tessellated non-watertight.",
47398
+ features: [
47399
+ "kernel",
47400
+ "geometry",
47401
+ "primitives"
47402
+ ]
47403
+ },
47195
47404
  {
47196
47405
  id: "2026-09-17-native-instant-open-contour-cam",
47197
47406
  version: "0.10.0",
@@ -57195,10 +57404,86 @@ var init_source_provenance = __esm({
57195
57404
  }
57196
57405
  });
57197
57406
 
57198
- // src/app-link.ts
57199
- import { readFileSync as readFileSync5 } from "node:fs";
57407
+ // src/app-discovery.ts
57408
+ import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
57200
57409
  import { homedir } from "node:os";
57201
- import { dirname, isAbsolute as isAbsolute4, join as join3 } from "node:path";
57410
+ import { join as join3, resolve as resolve3 } from "node:path";
57411
+ function isAlive(pid) {
57412
+ if (typeof pid !== "number" || pid <= 0) return false;
57413
+ try {
57414
+ process.kill(pid, 0);
57415
+ return true;
57416
+ } catch (e) {
57417
+ return e.code === "EPERM";
57418
+ }
57419
+ }
57420
+ function describeInstance(r) {
57421
+ return `pid ${r.pid} (${r.document_name ?? r.document_path ?? "no document"})`;
57422
+ }
57423
+ function listInstances({ home = homedir(), alive = isAlive } = {}) {
57424
+ const root = join3(home, ".vcad");
57425
+ const read = (p) => {
57426
+ try {
57427
+ return JSON.parse(readFileSync5(p, "utf8"));
57428
+ } catch {
57429
+ return null;
57430
+ }
57431
+ };
57432
+ let records = [];
57433
+ try {
57434
+ const dir = join3(root, "instances");
57435
+ records = readdirSync2(dir).filter((f) => f.endsWith(".json")).map((f) => read(join3(dir, f))).filter((r) => r !== null);
57436
+ } catch {
57437
+ }
57438
+ if (records.length === 0) {
57439
+ const legacy = read(join3(root, "app.json"));
57440
+ if (legacy) records = [legacy];
57441
+ }
57442
+ return records.filter((r) => alive(r.pid)).sort((a, b) => String(b.started ?? "").localeCompare(String(a.started ?? "")));
57443
+ }
57444
+ function selectInstance({
57445
+ env = process.env,
57446
+ instances,
57447
+ documentPath
57448
+ }) {
57449
+ if (env.VCAD_APP_URL) {
57450
+ const url = env.VCAD_APP_URL;
57451
+ return { url, instance: instances.find((r) => urlOf(r) === url) ?? null };
57452
+ }
57453
+ if (env.VCAD_APP_PID) {
57454
+ const pid = Number(env.VCAD_APP_PID);
57455
+ const instance2 = instances.find((r) => r.pid === pid);
57456
+ if (!instance2) {
57457
+ const others2 = instances.length ? ` Running: ${instances.map(describeInstance).join(", ")}.` : "";
57458
+ return { error: `VCAD_APP_PID=${env.VCAD_APP_PID}: no running vcad.app with that pid.${others2}` };
57459
+ }
57460
+ return { url: urlOf(instance2), instance: instance2 };
57461
+ }
57462
+ if (instances.length === 0) return { error: "no running vcad.app found (~/.vcad/instances is empty)" };
57463
+ const wanted = documentPath ?? env.VCAD_APP_DOCUMENT;
57464
+ if (wanted) {
57465
+ const abs = resolve3(wanted);
57466
+ const instance2 = instances.find((r) => r.document_path && resolve3(r.document_path) === abs);
57467
+ if (instance2) return { url: urlOf(instance2), instance: instance2 };
57468
+ }
57469
+ const [instance, ...others] = instances;
57470
+ if (!others.length) return { url: urlOf(instance), instance };
57471
+ return {
57472
+ url: urlOf(instance),
57473
+ instance,
57474
+ warning: `${instances.length} vcad.app instances are running; using the newest, ${describeInstance(instance)}. Others: ${others.map(describeInstance).join(", ")}. Set VCAD_APP_PID to choose.`
57475
+ };
57476
+ }
57477
+ var urlOf;
57478
+ var init_app_discovery = __esm({
57479
+ "src/app-discovery.ts"() {
57480
+ "use strict";
57481
+ urlOf = (r) => r.url ?? `http://127.0.0.1:${r.port}/mcp`;
57482
+ }
57483
+ });
57484
+
57485
+ // src/app-link.ts
57486
+ import { dirname, isAbsolute as isAbsolute4 } from "node:path";
57202
57487
  function noteDocumentFolder(documentId, folder) {
57203
57488
  folders.set(documentId, folder);
57204
57489
  }
@@ -57210,13 +57495,27 @@ function documentFolder(documentId, document) {
57210
57495
  }
57211
57496
  function endpoint() {
57212
57497
  if (process.env.VCAD_APP === "0") return null;
57213
- try {
57214
- const info = JSON.parse(readFileSync5(join3(homedir(), ".vcad", "app.json"), "utf8"));
57215
- if (typeof info.pid === "number") process.kill(info.pid, 0);
57216
- return info.url ?? `http://127.0.0.1:${info.port}/mcp`;
57217
- } catch {
57218
- return null;
57498
+ const instances = listInstances();
57499
+ if (pinned && (process.env.VCAD_APP_URL || instances.some((r) => r.pid === pinned?.instance?.pid))) {
57500
+ return pinned.url;
57501
+ }
57502
+ const pick2 = selectInstance({ instances });
57503
+ if (pick2.error !== void 0) return null;
57504
+ if (pick2.warning && pick2.warning !== warned) {
57505
+ warned = pick2.warning;
57506
+ process.stderr.write(`[vcad-mcp] ${pick2.warning}
57507
+ `);
57219
57508
  }
57509
+ const prev = pinned?.instance;
57510
+ if (prev && pick2.instance && prev.instance_id !== pick2.instance.instance_id) {
57511
+ appChanged(prev, pick2.instance);
57512
+ }
57513
+ pinned = { url: pick2.url, instance: pick2.instance };
57514
+ return pick2.url;
57515
+ }
57516
+ function appChanged(prev, next) {
57517
+ const why = `The vcad app changed: ${describeInstance(prev)} is gone, now linked to ${describeInstance(next)} (it relaunched, or another window took over). Edits stay in this session. To show it there, call show_in_app.`;
57518
+ for (const id of links.keys()) loseWindow(id, why);
57220
57519
  }
57221
57520
  async function reconcileBeforeWrite(documentId, documents2, call = callApp) {
57222
57521
  const link = links.get(documentId);
@@ -57430,14 +57729,17 @@ function pushPose(documentId, joints, call = callApp, now = Date.now(), base) {
57430
57729
  sendPose(frame, now);
57431
57730
  return true;
57432
57731
  }
57433
- var links, folders, callApp, PUSH_WAIT_MS, RECONCILE_WAIT_MS, wait, pushes, lateNotes, LOST_WINDOW, OPENING_TOOLS, toolDefs3, POSE_INTERVAL_MS, poseBusy, lastPose, pendingRestart, pendingLatest, poseTimer;
57732
+ var links, folders, pinned, warned, callApp, PUSH_WAIT_MS, RECONCILE_WAIT_MS, wait, pushes, lateNotes, LOST_WINDOW, OPENING_TOOLS, toolDefs3, POSE_INTERVAL_MS, poseBusy, lastPose, pendingRestart, pendingLatest, poseTimer;
57434
57733
  var init_app_link = __esm({
57435
57734
  "src/app-link.ts"() {
57436
57735
  "use strict";
57736
+ init_app_discovery();
57437
57737
  init_session_core();
57438
57738
  init_tool_def();
57439
57739
  links = /* @__PURE__ */ new Map();
57440
57740
  folders = /* @__PURE__ */ new Map();
57741
+ pinned = null;
57742
+ warned = "";
57441
57743
  callApp = async (tool, args) => {
57442
57744
  const url = endpoint();
57443
57745
  if (!url) return null;
@@ -57507,7 +57809,7 @@ var init_app_link = __esm({
57507
57809
 
57508
57810
  // src/tools/session.ts
57509
57811
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync6 } from "node:fs";
57510
- import { dirname as dirname2, resolve as resolve3 } from "node:path";
57812
+ import { dirname as dirname2, resolve as resolve4 } from "node:path";
57511
57813
  import { randomBytes as randomBytes2 } from "node:crypto";
57512
57814
  import { AsyncLocalStorage } from "node:async_hooks";
57513
57815
  function durabilityWarning() {
@@ -57579,7 +57881,7 @@ function openDocument(args) {
57579
57881
  };
57580
57882
  }
57581
57883
  function writeLocalSnapshot(id, json) {
57582
- const dir = resolve3(stateRoot(), "get_document");
57884
+ const dir = resolve4(stateRoot(), "get_document");
57583
57885
  mkdirSync2(dir, { recursive: true });
57584
57886
  const path = resolveWithinRoot(`${id}.vcad`, dir);
57585
57887
  writeFileSync2(path, json);
@@ -57792,7 +58094,7 @@ async function loadDocumentFromPath(path, store, engine) {
57792
58094
  "`path` loads are local/stdio only \u2014 this server's filesystem is ephemeral. Pass the source inline to create_cad_loon, or save with save_document and reopen by `name`."
57793
58095
  );
57794
58096
  }
57795
- const abs = resolve3(path);
58097
+ const abs = resolve4(path);
57796
58098
  let raw;
57797
58099
  try {
57798
58100
  raw = readFileSync6(abs, "utf8");
@@ -61597,8 +61899,8 @@ async function openBridge(createServer2, clientInfo, clientCapabilities, hooks)
61597
61899
  await server.connect(serverSide);
61598
61900
  const rpc = (method, params) => {
61599
61901
  const id = nextId3++;
61600
- return new Promise((resolve8) => {
61601
- pending.set(id, (m) => resolve8(m));
61902
+ return new Promise((resolve9) => {
61903
+ pending.set(id, (m) => resolve9(m));
61602
61904
  void clientSide.send({
61603
61905
  jsonrpc: "2.0",
61604
61906
  id,
@@ -61935,8 +62237,8 @@ function taskElicitation(bridge, reqParams) {
61935
62237
  method: "elicitation/create",
61936
62238
  params: cleanParams
61937
62239
  };
61938
- return new Promise((resolve8) => {
61939
- record2.pendingInputs.set(key3, resolve8);
62240
+ return new Promise((resolve9) => {
62241
+ record2.pendingInputs.set(key3, resolve9);
61940
62242
  touchTask(record2, "input_required", "waiting for client input");
61941
62243
  });
61942
62244
  }
@@ -61961,11 +62263,11 @@ function handleTaskMethod(id, method, params) {
61961
62263
  if (method === "tasks/update") {
61962
62264
  const responses = params.inputResponses && typeof params.inputResponses === "object" ? params.inputResponses : {};
61963
62265
  for (const [key3, response] of Object.entries(responses)) {
61964
- const resolve8 = record2.pendingInputs.get(key3);
61965
- if (!resolve8) continue;
62266
+ const resolve9 = record2.pendingInputs.get(key3);
62267
+ if (!resolve9) continue;
61966
62268
  record2.pendingInputs.delete(key3);
61967
62269
  delete record2.inputRequests[key3];
61968
- resolve8(response);
62270
+ resolve9(response);
61969
62271
  }
61970
62272
  if (record2.status === "input_required" && Object.keys(record2.inputRequests).length === 0) {
61971
62273
  touchTask(record2, "working", `running ${record2.toolName}`);
@@ -61977,10 +62279,10 @@ function handleTaskMethod(id, method, params) {
61977
62279
  }
61978
62280
  record2.cancelRequested = true;
61979
62281
  if (record2.status === "working" || record2.status === "input_required") {
61980
- for (const [key3, resolve8] of record2.pendingInputs) {
62282
+ for (const [key3, resolve9] of record2.pendingInputs) {
61981
62283
  record2.pendingInputs.delete(key3);
61982
62284
  delete record2.inputRequests[key3];
61983
- resolve8({ action: "cancel" });
62285
+ resolve9({ action: "cancel" });
61984
62286
  }
61985
62287
  touchTask(record2, "cancelled", "cancelled by client");
61986
62288
  }
@@ -62658,7 +62960,7 @@ function normalizeOp(op, index) {
62658
62960
  return { name, args };
62659
62961
  }
62660
62962
  function resolveSymbolicRefs(args, createdIds, index) {
62661
- const resolve8 = (v) => {
62963
+ const resolve9 = (v) => {
62662
62964
  if (typeof v !== "string") return v;
62663
62965
  const m = SYMBOLIC_REF.exec(v);
62664
62966
  if (!m) return v;
@@ -62678,15 +62980,15 @@ function resolveSymbolicRefs(args, createdIds, index) {
62678
62980
  };
62679
62981
  const out = { ...args };
62680
62982
  for (const key3 of ["part_id", "node_id"]) {
62681
- if (key3 in out) out[key3] = resolve8(out[key3]);
62983
+ if (key3 in out) out[key3] = resolve9(out[key3]);
62682
62984
  }
62683
62985
  if (out.params && typeof out.params === "object" && !Array.isArray(out.params)) {
62684
62986
  const params = { ...out.params };
62685
62987
  for (const key3 of ["child", "left", "right", "sketch"]) {
62686
- if (key3 in params) params[key3] = resolve8(params[key3]);
62988
+ if (key3 in params) params[key3] = resolve9(params[key3]);
62687
62989
  }
62688
62990
  if (Array.isArray(params.sketches)) {
62689
- params.sketches = params.sketches.map(resolve8);
62991
+ params.sketches = params.sketches.map(resolve9);
62690
62992
  }
62691
62993
  out.params = params;
62692
62994
  }
@@ -67349,7 +67651,7 @@ async function flattenSchematic(sheet, netlister) {
67349
67651
  if (def.body.hierarchy) {
67350
67652
  err9(`Sheet '${def.name}' body has its own hierarchy \u2014 nest sheets with \`instances\` instead (ignored)`);
67351
67653
  }
67352
- const resolve8 = (name) => {
67654
+ const resolve9 = (name) => {
67353
67655
  if (ports.has(name)) {
67354
67656
  const bound = binding.get(name);
67355
67657
  if (bound !== void 0) return bound;
@@ -67377,7 +67679,7 @@ async function flattenSchematic(sheet, netlister) {
67377
67679
  if (pins.length === 0) continue;
67378
67680
  if (isAutoName(name) && pins.length < 2) continue;
67379
67681
  usedNames.add(name);
67380
- const flat = resolve8(name);
67682
+ const flat = resolve9(name);
67381
67683
  if (!ports.has(name) && !globals.has(name) && rootNames.has(flat)) {
67382
67684
  err9(
67383
67685
  `Net scope collision: local net '${name}' of sheet instance '${path}' flattens to '${flat}', which the root sheet also uses`
@@ -67401,9 +67703,9 @@ async function flattenSchematic(sheet, netlister) {
67401
67703
  warn(`Hierarchical port '${port}' of sheet '${def.name}' connects to nothing inside the sheet`);
67402
67704
  }
67403
67705
  }
67404
- for (const flag of def.body.power_flags ?? []) outFlags.push({ ...flag, net: resolve8(flag.net) });
67706
+ for (const flag of def.body.power_flags ?? []) outFlags.push({ ...flag, net: resolve9(flag.net) });
67405
67707
  instances.push({ path, sheet: def.name, refs });
67406
- await elaborateAll(def.instances ?? [], path, offset, resolve8, stack);
67708
+ await elaborateAll(def.instances ?? [], path, offset, resolve9, stack);
67407
67709
  };
67408
67710
  await elaborateAll(h.instances ?? [], "", { x: 0, y: 0 }, (n) => n, []);
67409
67711
  const nets = {};
@@ -68958,12 +69260,12 @@ var init_enclosure = __esm({
68958
69260
  // src/wasm/ecad-diff.ts
68959
69261
  import { createRequire } from "node:module";
68960
69262
  import { fileURLToPath } from "node:url";
68961
- import { dirname as dirname3, resolve as resolve4 } from "node:path";
69263
+ import { dirname as dirname3, resolve as resolve5 } from "node:path";
68962
69264
  function load() {
68963
69265
  if (cached2 !== void 0) return cached2;
68964
69266
  try {
68965
69267
  const here = dirname3(fileURLToPath(import.meta.url));
68966
- const pkg = resolve4(here, "../../../../crates/vcad-ecad-diff-wasm/pkg/vcad_ecad_diff_wasm.js");
69268
+ const pkg = resolve5(here, "../../../../crates/vcad-ecad-diff-wasm/pkg/vcad_ecad_diff_wasm.js");
68967
69269
  const req = createRequire(import.meta.url);
68968
69270
  cached2 = req(pkg);
68969
69271
  } catch {
@@ -82845,7 +83147,7 @@ import { execFile } from "child_process";
82845
83147
  import {
82846
83148
  existsSync as existsSync4,
82847
83149
  mkdtempSync as mkdtempSync2,
82848
- readdirSync as readdirSync2,
83150
+ readdirSync as readdirSync3,
82849
83151
  readFileSync as readFileSync8,
82850
83152
  rmSync,
82851
83153
  writeFileSync as writeFileSync5
@@ -82903,7 +83205,7 @@ function listEvalTasks(args) {
82903
83205
  const tasksDir = join6(mechevalRoot, "tasks");
82904
83206
  const suiteFilter = args.suite ? String(args.suite).toUpperCase() : null;
82905
83207
  const tasks2 = [];
82906
- for (const file of readdirSync2(tasksDir).sort()) {
83208
+ for (const file of readdirSync3(tasksDir).sort()) {
82907
83209
  if (!file.endsWith(".json")) continue;
82908
83210
  try {
82909
83211
  const raw = JSON.parse(readFileSync8(join6(tasksDir, file), "utf8"));
@@ -87291,14 +87593,14 @@ var init_acoustics = __esm({
87291
87593
  // src/tools/import.ts
87292
87594
  import { createHash as createHash6 } from "node:crypto";
87293
87595
  import { readFileSync as readFileSync9, existsSync as existsSync5, statSync as statSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync7 } from "node:fs";
87294
- import { basename, join as join7, resolve as resolve5, sep as sep3 } from "node:path";
87596
+ import { basename, join as join7, resolve as resolve6, sep as sep3 } from "node:path";
87295
87597
  function exportRoot() {
87296
87598
  return process.env.VCAD_MCP_EXPORT_DIR ?? process.cwd();
87297
87599
  }
87298
87600
  function isWithinExportRoot(path) {
87299
87601
  if (path.includes("\0")) return false;
87300
- const root = resolve5(exportRoot());
87301
- const abs = resolve5(path);
87602
+ const root = resolve6(exportRoot());
87603
+ const abs = resolve6(path);
87302
87604
  return abs === root || abs.startsWith(root + sep3);
87303
87605
  }
87304
87606
  function stepNodePath(bytes, diskPath, label) {
@@ -87309,7 +87611,7 @@ function stepNodePath(bytes, diskPath, label) {
87309
87611
  mkdirSync3(dir, { recursive: true });
87310
87612
  const file = join7(dir, `${digest}.step`);
87311
87613
  if (!existsSync5(file)) writeFileSync7(file, bytes);
87312
- return { path: resolve5(file), portable: true };
87614
+ return { path: resolve6(file), portable: true };
87313
87615
  } catch {
87314
87616
  return { path: `step:${digest}/${label}`, portable: false };
87315
87617
  }
@@ -87385,7 +87687,7 @@ function readStepInput(input) {
87385
87687
  if (stat.size > MAX_STEP_BYTES) {
87386
87688
  throw new Error(`STEP file exceeds ${MAX_STEP_BYTES} byte limit`);
87387
87689
  }
87388
- return { bytes: readFileSync9(filepath), diskPath: resolve5(filepath) };
87690
+ return { bytes: readFileSync9(filepath), diskPath: resolve6(filepath) };
87389
87691
  }
87390
87692
  function skippedFaceSummary(report, warnings) {
87391
87693
  const skipped = report.reduce((sum, s) => sum + s.skipped_faces.length, 0);
@@ -87558,11 +87860,11 @@ var init_import = __esm({
87558
87860
 
87559
87861
  // src/tools/import-urdf.ts
87560
87862
  import { readFileSync as readFileSync10, existsSync as existsSync6, statSync as statSync3 } from "node:fs";
87561
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute5, resolve as resolve6, sep as sep4 } from "node:path";
87863
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute5, resolve as resolve7, sep as sep4 } from "node:path";
87562
87864
  function resolveMeshPath(filename, urdfDir, packageRoots, confineRoot) {
87563
87865
  const confined = (p) => {
87564
- const abs = resolve6(p);
87565
- const root = resolve6(confineRoot);
87866
+ const abs = resolve7(p);
87867
+ const root = resolve7(confineRoot);
87566
87868
  const rootSep = root.endsWith(sep4) ? root : root + sep4;
87567
87869
  if (abs !== root && !abs.startsWith(rootSep)) return null;
87568
87870
  return existsSync6(abs) && statSync3(abs).isFile() ? abs : null;
@@ -87571,14 +87873,14 @@ function resolveMeshPath(filename, urdfDir, packageRoots, confineRoot) {
87571
87873
  const pkgMatch = filename.match(/^package:\/\/([^/]+)\/?(.*)$/);
87572
87874
  if (pkgMatch) {
87573
87875
  for (const root of packageRoots) {
87574
- const hit = confined(resolve6(root, pkgMatch[1], pkgMatch[2]));
87876
+ const hit = confined(resolve7(root, pkgMatch[1], pkgMatch[2]));
87575
87877
  if (hit) return hit;
87576
87878
  }
87577
87879
  return null;
87578
87880
  }
87579
87881
  const stripped = filename.startsWith("file://") ? filename.slice("file://".length) : filename;
87580
87882
  if (isAbsolute5(stripped)) return confined(stripped);
87581
- if (urdfDir) return confined(resolve6(urdfDir, stripped));
87883
+ if (urdfDir) return confined(resolve7(urdfDir, stripped));
87582
87884
  return null;
87583
87885
  }
87584
87886
  function inlineMeshImports2(doc, engine, urdfDir, packageRoots, confineRoot) {
@@ -90821,13 +91123,13 @@ ${issues.map((i) => i.problem).join("\n")}`
90821
91123
  }
90822
91124
  async function hasFfmpeg() {
90823
91125
  if (ffmpegAvailable !== null) return ffmpegAvailable;
90824
- ffmpegAvailable = await new Promise((resolve8) => {
91126
+ ffmpegAvailable = await new Promise((resolve9) => {
90825
91127
  try {
90826
91128
  const p = spawn("ffmpeg", ["-version"], { stdio: "ignore" });
90827
- p.on("error", () => resolve8(false));
90828
- p.on("exit", (code) => resolve8(code === 0));
91129
+ p.on("error", () => resolve9(false));
91130
+ p.on("exit", (code) => resolve9(code === 0));
90829
91131
  } catch {
90830
- resolve8(false);
91132
+ resolve9(false);
90831
91133
  }
90832
91134
  });
90833
91135
  return ffmpegAvailable;
@@ -90866,13 +91168,13 @@ function startMp4Encoder(width, height, fps) {
90866
91168
  const exitError = (code) => new Error(
90867
91169
  `ffmpeg exited ${code}: ${Buffer.concat(errChunks).toString().slice(-400)}`
90868
91170
  );
90869
- const done = new Promise((resolve8, reject) => {
91171
+ const done = new Promise((resolve9, reject) => {
90870
91172
  p.on("error", (e) => {
90871
91173
  failure2 = e;
90872
91174
  reject(e);
90873
91175
  });
90874
91176
  p.on("close", (code) => {
90875
- if (code === 0) resolve8(Buffer.concat(out));
91177
+ if (code === 0) resolve9(Buffer.concat(out));
90876
91178
  else {
90877
91179
  failure2 ??= exitError(code);
90878
91180
  reject(failure2);
@@ -90883,15 +91185,15 @@ function startMp4Encoder(width, height, fps) {
90883
91185
  });
90884
91186
  return {
90885
91187
  writeFrame(rgba) {
90886
- return new Promise((resolve8, reject) => {
91188
+ return new Promise((resolve9, reject) => {
90887
91189
  if (failure2) return reject(failure2);
90888
91190
  if (p.exitCode !== null || p.stdin.destroyed) {
90889
91191
  return reject(failure2 ?? exitError(p.exitCode));
90890
91192
  }
90891
- if (p.stdin.write(rgba)) return resolve8();
91193
+ if (p.stdin.write(rgba)) return resolve9();
90892
91194
  const onDrain = () => {
90893
91195
  p.removeListener("close", onClose);
90894
- resolve8();
91196
+ resolve9();
90895
91197
  };
90896
91198
  const onClose = () => {
90897
91199
  p.stdin.removeListener("drain", onDrain);
@@ -92451,8 +92753,8 @@ var init_server3 = __esm({
92451
92753
  resolveLoonAssets(doc, { base_dir }, engine);
92452
92754
  });
92453
92755
  PKG_VERSION = (() => {
92454
- if ("0.11.0-main.159") {
92455
- return "0.11.0-main.159";
92756
+ if ("0.11.0-main.163") {
92757
+ return "0.11.0-main.163";
92456
92758
  }
92457
92759
  try {
92458
92760
  const req = createRequire3(import.meta.url);
@@ -92461,8 +92763,8 @@ var init_server3 = __esm({
92461
92763
  return "0.0.0";
92462
92764
  }
92463
92765
  })();
92464
- BUILD_SHA = "6ca36502681cbcd14c0248e16ea270d62193338f";
92465
- BUILD_TIME = "2026-11-11T16:44:44.291Z";
92766
+ BUILD_SHA = "816ee12b085c12b403a35ebfedb6c276e175a3af";
92767
+ BUILD_TIME = "2026-11-11T21:40:44.212Z";
92466
92768
  SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
92467
92769
  VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
92468
92770
  INSTANCE_ID = randomUUID5().slice(0, 8);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vcad/mcp",
3
- "version": "0.11.0-main.159",
3
+ "version": "0.11.0-main.163",
4
4
  "description": "vcad MCP server — parametric CAD + PCB design tools for AI agents (self-contained: bundled server + kernel WASM)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "homepage": "https://github.com/muni-works/vcad",
21
21
  "vcadBuild": {
22
- "sha": "6ca36502681cbcd14c0248e16ea270d62193338f",
23
- "builtAt": "2026-11-11T16:44:44.291Z"
22
+ "sha": "816ee12b085c12b403a35ebfedb6c276e175a3af",
23
+ "builtAt": "2026-11-11T21:40:44.212Z"
24
24
  }
25
25
  }
package/skill/SKILL.md CHANGED
@@ -81,6 +81,28 @@ Call `server_info` first.
81
81
  Tell the user and suggest restarting their agent (or updating vcad.app).
82
82
  - `durable: false` → sessions won't survive a restart; keep the loon source.
83
83
 
84
+ ## More than one vcad.app
85
+
86
+ Each running vcad.app registers itself in `~/.vcad/instances/<pid>.json`
87
+ (pid, port, open document, launch time); `~/.vcad/app.json` only points at
88
+ the newest. Every app-bridge result ends with `[vcad.app pid N · document]`
89
+ and carries `_meta["io.vcad/instance"]`: check it names the window you mean.
90
+ A `⚠ vcad.app CHANGED` notice means the app relaunched or another window took
91
+ over — re-check the open document before trusting earlier state.
92
+
93
+ Pick the window explicitly when more than one runs:
94
+
95
+ - `VCAD_APP_PID=<pid>` — that instance; fails loudly if it's gone.
96
+ - `VCAD_APP_URL=http://127.0.0.1:<port>/mcp` — that endpoint, verbatim.
97
+ - `VCAD_APP_DOCUMENT=<path>` — the instance with that file open.
98
+ - otherwise the newest, with a warning listing the others. The choice is
99
+ pinned for the client's lifetime; a later launch never steals it.
100
+ - `VCAD_APP=0` — `@vcad/mcp` doesn't link to any app.
101
+
102
+ **Never `pkill`/`killall` vcad.app by pattern.** Other sessions may be driving
103
+ other windows, and to them it looks like a crash. Quit only an instance you
104
+ launched, by its pid (`kill <pid>`, from `~/.vcad/instances/`).
105
+
84
106
  ## Ground rules
85
107
 
86
108
  - vcad has no hosted services. Never send the user to a website to view,