node-lab-mcp 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +238 -79
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -25315,7 +25315,7 @@ function getPartyInfo(partySocketOptions, defaultProtocol, defaultParams = {}) {
25315
25315
  host: rawHost,
25316
25316
  path: rawPath,
25317
25317
  protocol: rawProtocol,
25318
- room: room2,
25318
+ room,
25319
25319
  party,
25320
25320
  basePath,
25321
25321
  prefix,
@@ -25328,13 +25328,13 @@ function getPartyInfo(partySocketOptions, defaultProtocol, defaultParams = {}) {
25328
25328
  const name = party ?? "main";
25329
25329
  const path = rawPath ? `/${rawPath}` : "";
25330
25330
  const protocol = rawProtocol || (host.startsWith("localhost:") || host.startsWith("127.0.0.1:") || host.startsWith("192.168.") || host.startsWith("10.") || host.startsWith("172.") && host.split(".")[1] >= "16" && host.split(".")[1] <= "31" || host.startsWith("[::ffff:7f00:1]:") ? defaultProtocol : `${defaultProtocol}s`);
25331
- const baseUrl = `${protocol}://${host}/${basePath || `${prefix || "parties"}/${name}/${room2}`}${path}`;
25331
+ const baseUrl = `${protocol}://${host}/${basePath || `${prefix || "parties"}/${name}/${room}`}${path}`;
25332
25332
  const makeUrl = (query2 = {}) => `${baseUrl}?${new URLSearchParams([...Object.entries(defaultParams), ...Object.entries(query2).filter(valueIsNotNil)])}`;
25333
25333
  const urlProvider = typeof query === "function" ? async () => makeUrl(await query()) : makeUrl(query);
25334
25334
  return {
25335
25335
  host,
25336
25336
  path,
25337
- room: room2,
25337
+ room,
25338
25338
  name,
25339
25339
  protocol,
25340
25340
  partyUrl: baseUrl,
@@ -25386,11 +25386,11 @@ var PartySocket = class extends ReconnectingWebSocket {
25386
25386
  this.setWSProperties(wsOptions);
25387
25387
  }
25388
25388
  setWSProperties(wsOptions) {
25389
- const { _pk, _pkurl, name, room: room2, host, path, basePath } = wsOptions;
25389
+ const { _pk, _pkurl, name, room, host, path, basePath } = wsOptions;
25390
25390
  this._pk = _pk;
25391
25391
  this._pkurl = _pkurl;
25392
25392
  this.name = name;
25393
- this.room = room2;
25393
+ this.room = room;
25394
25394
  this.host = host;
25395
25395
  this.path = path;
25396
25396
  this.basePath = basePath;
@@ -25453,14 +25453,34 @@ function getWSOptions(partySocketOptions) {
25453
25453
  // party-bridge.ts
25454
25454
  import { randomUUID as randomUUID2 } from "node:crypto";
25455
25455
  var PartyBridge = class {
25456
- constructor(host, room2) {
25456
+ constructor(host, room) {
25457
25457
  this.host = host;
25458
- this.room = room2;
25459
- this.ps = new PartySocket({ host, room: room2 });
25460
- this.ps.addEventListener("message", (e) => this.onMessage(typeof e.data === "string" ? e.data : ""));
25461
- this.ps.addEventListener("open", () => process.stderr.write(`[party] joined room "${room2}" on ${host}
25458
+ if (room) this.openRoom(room);
25459
+ }
25460
+ ps = null;
25461
+ pending = /* @__PURE__ */ new Map();
25462
+ peers = 0;
25463
+ room;
25464
+ openRoom(room) {
25465
+ if (this.ps) {
25466
+ try {
25467
+ this.ps.close();
25468
+ } catch {
25469
+ }
25470
+ }
25471
+ for (const [id, p] of this.pending) {
25472
+ clearTimeout(p.timer);
25473
+ p.reject(new Error("relay room switched mid-command"));
25474
+ this.pending.delete(id);
25475
+ }
25476
+ this.peers = 0;
25477
+ this.room = room;
25478
+ const ps = new PartySocket({ host: this.host, room });
25479
+ this.ps = ps;
25480
+ ps.addEventListener("message", (e) => this.onMessage(typeof e.data === "string" ? e.data : ""));
25481
+ ps.addEventListener("open", () => process.stderr.write(`[party] joined room "${room}" on ${this.host}
25462
25482
  `));
25463
- this.ps.addEventListener("close", () => {
25483
+ ps.addEventListener("close", () => {
25464
25484
  this.peers = 0;
25465
25485
  for (const [id, p] of this.pending) {
25466
25486
  clearTimeout(p.timer);
@@ -25469,18 +25489,27 @@ var PartyBridge = class {
25469
25489
  }
25470
25490
  });
25471
25491
  }
25472
- ps;
25473
- pending = /* @__PURE__ */ new Map();
25474
- peers = 0;
25492
+ // GraphBridge: runtime room switching. The promise resolves immediately after the new socket is
25493
+ // requested the `open` event follows async; check status()/connected() to know when the editor pairs.
25494
+ async setRoom(room) {
25495
+ this.openRoom(room);
25496
+ }
25497
+ currentRoom() {
25498
+ return this.room;
25499
+ }
25475
25500
  // peers counts everyone in the room including us, so >1 means an editor has joined.
25476
25501
  connected() {
25477
- return this.ps.readyState === PartySocket.OPEN && this.peers > 1;
25502
+ return this.ps?.readyState === PartySocket.OPEN && this.peers > 1;
25478
25503
  }
25479
25504
  status() {
25505
+ if (!this.ps || !this.room) return `no relay room set \u2014 call set_room with a pairing code from the lab's MCP modal`;
25480
25506
  if (this.ps.readyState !== PartySocket.OPEN) return `connecting to relay ${this.host}\u2026`;
25481
25507
  return this.peers > 1 ? `editor connected via relay (room "${this.room}")` : `in room "${this.room}", waiting for the editor to pair`;
25482
25508
  }
25483
25509
  send(cmd, timeoutMs = 8e3) {
25510
+ if (!this.ps || !this.room) {
25511
+ return Promise.reject(new Error(`No relay room set. Call set_room with the pairing code from the lab's MCP modal.`));
25512
+ }
25484
25513
  if (this.ps.readyState !== PartySocket.OPEN) {
25485
25514
  return Promise.reject(new Error(`Not connected to the relay (${this.host}) yet \u2014 retry in a moment.`));
25486
25515
  }
@@ -25646,18 +25675,20 @@ var AcademySink = class extends BenchNode {
25646
25675
  }
25647
25676
  };
25648
25677
  var ACADEMY_IN = [
25649
- { type: 0 /* Power */, name: "POWER IN 1" },
25650
- { type: 0 /* Power */, name: "POWER IN 2" },
25651
- { type: 1 /* Number */, name: "DATA IN X" },
25652
- { type: 1 /* Number */, name: "DATA IN Y" },
25653
- { type: 1 /* Number */, name: "DATA IN Z" }
25678
+ { type: 1 /* Number */, name: "DATA IN X1" },
25679
+ { type: 1 /* Number */, name: "DATA IN X2" },
25680
+ { type: 1 /* Number */, name: "DATA IN X3" },
25681
+ { type: 1 /* Number */, name: "DATA IN X4" },
25682
+ { type: 1 /* Number */, name: "DATA IN X5" },
25683
+ { type: 1 /* Number */, name: "DATA IN X6" }
25654
25684
  ];
25655
25685
  var ACADEMY_OUT = [
25656
- { type: 0 /* Power */, name: "POWER OUT 1" },
25657
- { type: 0 /* Power */, name: "POWER OUT 2" },
25658
- { type: 1 /* Number */, name: "DATA OUT A" },
25659
- { type: 1 /* Number */, name: "DATA OUT B" },
25660
- { type: 1 /* Number */, name: "DATA OUT C" }
25686
+ { type: 1 /* Number */, name: "DATA OUT A1" },
25687
+ { type: 1 /* Number */, name: "DATA OUT A2" },
25688
+ { type: 1 /* Number */, name: "DATA OUT A3" },
25689
+ { type: 1 /* Number */, name: "DATA OUT A4" },
25690
+ { type: 1 /* Number */, name: "DATA OUT A5" },
25691
+ { type: 1 /* Number */, name: "DATA OUT A6" }
25661
25692
  ];
25662
25693
  var TwoInGate = class extends BenchNode {
25663
25694
  a;
@@ -26013,10 +26044,10 @@ var Button = class extends BenchNode {
26013
26044
  }
26014
26045
  };
26015
26046
  var Joystick = class extends BenchNode {
26016
- constructor(id, twoAxis) {
26047
+ constructor(id, axis) {
26017
26048
  super(id);
26018
- this.twoAxis = twoAxis;
26019
- if (twoAxis) {
26049
+ this.axis = axis;
26050
+ if (axis === "xy" || axis === "cross") {
26020
26051
  this.addPort(1 /* Number */, false, "X+");
26021
26052
  this.addPort(1 /* Number */, false, "X-");
26022
26053
  this.addPort(1 /* Number */, false, "Y+");
@@ -26028,6 +26059,9 @@ var Joystick = class extends BenchNode {
26028
26059
  }
26029
26060
  x = 0;
26030
26061
  y = 0;
26062
+ get twoAxis() {
26063
+ return this.axis === "xy" || this.axis === "cross";
26064
+ }
26031
26065
  evaluate() {
26032
26066
  const DZ = 0.08;
26033
26067
  const dz = (t) => {
@@ -26036,11 +26070,14 @@ var Joystick = class extends BenchNode {
26036
26070
  };
26037
26071
  const pos = (t) => Math.round(Math.max(0, dz(t)) * 65535);
26038
26072
  const neg = (t) => Math.round(Math.max(0, -dz(t)) * 65535);
26039
- if (this.twoAxis) {
26073
+ if (this.axis === "xy" || this.axis === "cross") {
26040
26074
  this.outputs[0].value = pos(this.x);
26041
26075
  this.outputs[1].value = neg(this.x);
26042
26076
  this.outputs[2].value = pos(this.y);
26043
26077
  this.outputs[3].value = neg(this.y);
26078
+ } else if (this.axis === "x") {
26079
+ this.outputs[0].value = pos(this.x);
26080
+ this.outputs[1].value = neg(this.x);
26044
26081
  } else {
26045
26082
  this.outputs[0].value = pos(this.y);
26046
26083
  this.outputs[1].value = neg(this.y);
@@ -26155,6 +26192,13 @@ var LightDisplay = class extends BenchNode {
26155
26192
  evaluate() {
26156
26193
  }
26157
26194
  };
26195
+ var Notes = class extends BenchNode {
26196
+ text = "# Notes\n\nDouble-click to edit. Supports **markdown** \u2014 headings, lists, `code`, [links](https://example.com).";
26197
+ width = 280;
26198
+ height = 200;
26199
+ evaluate() {
26200
+ }
26201
+ };
26158
26202
  var Oscilloscope = class extends BenchNode {
26159
26203
  input;
26160
26204
  samples = [];
@@ -26551,11 +26595,49 @@ var VectorRotate = class extends BenchNode {
26551
26595
  }
26552
26596
  };
26553
26597
  var SHIP_THRUST_ACCEL = 6;
26554
- var SHIP_ANG_ACCEL = 2.5;
26598
+ var SHIP_ANG_ACCEL = 0.625;
26555
26599
  var SHIP_DRAG = 0;
26556
26600
  var SHIP_DAMPER_MAX = 3;
26557
26601
  var SHIP_ANG_DAMPER_MAX = 3;
26558
- var ASTEROID_LOCK_RADIUS = 4.5;
26602
+ var COMMON_ELEMENTS = [26, 14, 29, 22, 28];
26603
+ var RARE_ELEMENTS = [79, 78, 48, 82, 47, 92, 74];
26604
+ var MAGNETOSPHERE_M = 2500;
26605
+ var STATION_SAFE_M = 500;
26606
+ function pickComposition(pool) {
26607
+ const n = Math.random() < 0.5 ? 2 : 3;
26608
+ const picked = [];
26609
+ while (picked.length < n) {
26610
+ const e = pool[Math.floor(Math.random() * pool.length)];
26611
+ if (!picked.includes(e)) picked.push(e);
26612
+ }
26613
+ const weights = picked.map(() => 0.2 + Math.random());
26614
+ const sum = weights.reduce((a, b) => a + b, 0);
26615
+ const comp = {};
26616
+ picked.forEach((e, i) => comp[e] = Math.round(weights[i] / sum * 100) / 100);
26617
+ const total = Object.values(comp).reduce((a, b) => a + b, 0);
26618
+ const delta = Math.round((1 - total) * 100) / 100;
26619
+ if (delta !== 0) comp[picked[0]] = Math.round((comp[picked[0]] + delta) * 100) / 100;
26620
+ return comp;
26621
+ }
26622
+ function makeAsteroidField() {
26623
+ const out = [];
26624
+ const TOTAL = 400;
26625
+ for (let i = 0; i < TOTAL; i++) {
26626
+ const inner = i < 48;
26627
+ const rMin = inner ? 100 : STATION_SAFE_M;
26628
+ const rMax = inner ? STATION_SAFE_M : MAGNETOSPHERE_M - 100;
26629
+ const r = Math.cbrt(rMin ** 3 + Math.random() * (rMax ** 3 - rMin ** 3));
26630
+ const u = Math.random() * 2 - 1, phi = Math.random() * Math.PI * 2;
26631
+ const s = Math.sqrt(1 - u * u);
26632
+ const pos = vec(r * s * Math.cos(phi), r * u, r * s * Math.sin(phi), 0);
26633
+ const t = Math.random();
26634
+ const radius = 13 + t * 7;
26635
+ const mass = Math.round(30 + t * 70);
26636
+ const comp = pickComposition(inner ? COMMON_ELEMENTS : RARE_ELEMENTS);
26637
+ out.push({ id: `a${i}`, pos, vel: vec(), radius, mass, comp });
26638
+ }
26639
+ return out;
26640
+ }
26559
26641
  var STATION_LOCK_RADIUS = 14;
26560
26642
  var BEAM_SLEW = 5 * Math.PI / 180;
26561
26643
  var BEAM_GIMBAL_LIMIT = 80 * Math.PI / 180;
@@ -26578,9 +26660,13 @@ var SHIP_STATE = {
26578
26660
  orient: vec(0, 0, 0, 1),
26579
26661
  dir: vec(0, 0, 1, 0),
26580
26662
  asteroid: vec(),
26663
+ asteroidField: [],
26581
26664
  station: vec(),
26582
26665
  lockedTarget: null,
26583
- beam: { aim: vec(0, 0, 1, 0), massRange: false, spectro: false }
26666
+ beam: { aim: vec(0, 0, 1, 0), massRange: false, spectro: false },
26667
+ respawnAt: 0,
26668
+ magnetosphere: MAGNETOSPHERE_M,
26669
+ stationSafe: STATION_SAFE_M
26584
26670
  };
26585
26671
  var ShipSim = class {
26586
26672
  pos = vec(100, 0, 0, 0);
@@ -26591,10 +26677,11 @@ var ShipSim = class {
26591
26677
  // quaternion
26592
26678
  angVel = vec();
26593
26679
  // body-frame angular velocity (rad/s) — persists, so the ship coasts its spin
26594
- // ── world, station = origin (0,0,0). Ship starts at (100,0,0), so the station sits 100 m to port and the
26595
- // asteroid (86,25,30) is ~30 m ahead / 14 m to port / 25 m up of the ship's start. ──
26596
- asteroid = { pos: vec(86, 25, 30, 0), vel: vec(), mass: 30, comp: { 26: 0.9, 14: 0.1 } };
26597
- // 30 T · 90% Fe · 10% Si
26680
+ // ── world, station = origin (0,0,0). Ship starts at (100,0,0). 100 procedurally placed asteroids
26681
+ // (asteroidField) live around the station, inner shell (60–500 m) = common ores, outer shell
26682
+ // (500–2500 m) = rare. `asteroid` is the currently-active scan target (mutated by LOCK BEAM). ──
26683
+ asteroidField = makeAsteroidField();
26684
+ asteroid = this.asteroidField[0];
26598
26685
  station = { pos: vec(0, 0, 0, 0) };
26599
26686
  lockMode = 0;
26600
26687
  // 1 CLEAR · 2 LASER(mass/rock) · 3 PROCESSING PLANT(station) · 4 SHOP · 5 MAILING GUN · 6 CUSTOM (0 = none)
@@ -26628,20 +26715,22 @@ var ShipSim = class {
26628
26715
  const a = this.beam.aim;
26629
26716
  const d = qrotate(this.orient, qnormalize({ x: a.x, y: a.y, z: a.z, w: 0 }));
26630
26717
  let bestT = Infinity;
26631
- let best = null;
26632
- const ta = raySphereHit(this.pos, d, this.asteroid.pos, ASTEROID_LOCK_RADIUS);
26633
- if (ta !== null && ta < bestT) {
26634
- bestT = ta;
26635
- best = { pos: this.asteroid.pos, vel: this.asteroid.vel, kind: "asteroid" };
26718
+ let bestAst = null;
26719
+ for (const ast of this.asteroidField) {
26720
+ const t = raySphereHit(this.pos, d, ast.pos, ast.radius);
26721
+ if (t !== null && t < bestT) {
26722
+ bestT = t;
26723
+ bestAst = ast;
26724
+ }
26636
26725
  }
26637
26726
  const ts = raySphereHit(this.pos, d, this.station.pos, STATION_LOCK_RADIUS);
26638
26727
  if (ts !== null && ts < bestT) {
26639
- bestT = ts;
26640
- best = { pos: this.station.pos, vel: vec(), kind: "station" };
26641
- }
26642
- if (best) {
26643
- this.lockedTarget = { pos: { ...best.pos }, vel: { ...best.vel } };
26644
- this.lockKind = best.kind;
26728
+ this.lockedTarget = { pos: { ...this.station.pos }, vel: vec() };
26729
+ this.lockKind = "station";
26730
+ } else if (bestAst) {
26731
+ this.asteroid = bestAst;
26732
+ this.lockedTarget = { pos: { ...bestAst.pos }, vel: vec() };
26733
+ this.lockKind = "asteroid";
26645
26734
  } else {
26646
26735
  this.lockedTarget = null;
26647
26736
  this.lockKind = null;
@@ -26663,7 +26752,7 @@ var ShipSim = class {
26663
26752
  this.lockKind = "station";
26664
26753
  }
26665
26754
  /**
26666
- * LOCK MODE selects the target type. 6 CUSTOM tracks the CUSTOM TARGET vector live; the others apply on
26755
+ * LOCK MODE selects the target type. 6 CUSTOM tracks the CUSTOM TGT vector live; the others apply on
26667
26756
  * *change* (1 CLEAR wipes, 2 LASER → asteroid/rock, 3–5 → station), so a manual LOCK BEAM/STATION press can
26668
26757
  * override in between. 0 = no command (unwired) → leaves the current lock alone.
26669
26758
  */
@@ -26711,6 +26800,11 @@ var ShipSim = class {
26711
26800
  this.pos.x += this.vel.x * dt;
26712
26801
  this.pos.y += this.vel.y * dt;
26713
26802
  this.pos.z += this.vel.z * dt;
26803
+ const r = Math.hypot(this.pos.x, this.pos.y, this.pos.z);
26804
+ if (r > MAGNETOSPHERE_M) {
26805
+ this.reset();
26806
+ SHIP_STATE.respawnAt = Date.now();
26807
+ }
26714
26808
  const ta = SHIP_ANG_ACCEL;
26715
26809
  this.angVel.x += torqueBody.x * ta * dt;
26716
26810
  this.angVel.y += torqueBody.y * ta * dt;
@@ -26737,6 +26831,7 @@ var ShipSim = class {
26737
26831
  SHIP_STATE.orient = this.orient;
26738
26832
  SHIP_STATE.dir = this.direction();
26739
26833
  SHIP_STATE.asteroid = this.asteroid.pos;
26834
+ SHIP_STATE.asteroidField = this.asteroidField;
26740
26835
  SHIP_STATE.station = this.station.pos;
26741
26836
  SHIP_STATE.lockedTarget = this.lockedTarget ? this.lockedTarget.pos : null;
26742
26837
  SHIP_STATE.beam = this.beam;
@@ -26771,24 +26866,32 @@ var ProximitySensor = class extends BenchNode {
26771
26866
  this.out = this.addPort(1 /* Number */, false, "PROXIMITY");
26772
26867
  }
26773
26868
  evaluate() {
26774
- const p = SHIP_SIM.pos, a = SHIP_SIM.asteroid.pos, s = SHIP_SIM.station.pos;
26775
- const da = Math.hypot(p.x - a.x, p.y - a.y, p.z - a.z) - ASTEROID_LOCK_RADIUS;
26776
- const ds = Math.hypot(p.x - s.x, p.y - s.y, p.z - s.z) - STATION_LOCK_RADIUS;
26777
- const d = Math.max(0, Math.min(da, ds));
26869
+ const p = SHIP_SIM.pos, s = SHIP_SIM.station.pos;
26870
+ let best = Math.hypot(p.x - s.x, p.y - s.y, p.z - s.z) - STATION_LOCK_RADIUS;
26871
+ for (const ast of SHIP_SIM.asteroidField) {
26872
+ const d2 = Math.hypot(p.x - ast.pos.x, p.y - ast.pos.y, p.z - ast.pos.z) - ast.radius;
26873
+ if (d2 < best) best = d2;
26874
+ }
26875
+ const d = Math.max(0, best);
26778
26876
  this.out.value = d >= PROXIMITY_RANGE ? 0 : clampData((1 - d / PROXIMITY_RANGE) * DATA_MAX);
26779
26877
  }
26780
26878
  };
26781
26879
  var SHIP_IN_CONTROLS = [
26782
26880
  "THRUST",
26783
26881
  "REVERSE",
26882
+ // Order: X+, X-, Y+, Y- so a 2-axis joystick wires straight through (RIGHT→X+, LEFT→X-, UP→Y+, DOWN→Y-).
26883
+ "TRANSLATE RIGHT",
26884
+ "TRANSLATE LEFT",
26784
26885
  "TRANSLATE UP",
26785
26886
  "TRANSLATE DOWN",
26887
+ // YAW first then PITCH so X+/X- / Y+/Y- on a 2-axis joystick map straight onto YAW LEFT / YAW
26888
+ // RIGHT / PITCH UP / PITCH DOWN without crossed cables.
26889
+ "YAW RIGHT",
26890
+ "YAW LEFT",
26786
26891
  "PITCH UP",
26787
26892
  "PITCH DOWN",
26788
- "YAW LEFT",
26789
- "YAW RIGHT",
26790
- "ROLL LEFT",
26791
26893
  "ROLL RIGHT",
26894
+ "ROLL LEFT",
26792
26895
  "LINEAR DAMPER",
26793
26896
  "ANGULAR DAMPER"
26794
26897
  ];
@@ -26803,17 +26906,19 @@ var ShipIn = class extends BenchNode {
26803
26906
  evaluate() {
26804
26907
  }
26805
26908
  refresh() {
26909
+ const FWD = 1, REV = 0.5, STRAFE = 0.5;
26806
26910
  const thrustBody = vec(
26807
- 0,
26808
- this.ctl("TRANSLATE UP") - this.ctl("TRANSLATE DOWN"),
26809
- this.ctl("THRUST") - this.ctl("REVERSE"),
26911
+ (this.ctl("TRANSLATE LEFT") - this.ctl("TRANSLATE RIGHT")) * STRAFE,
26912
+ // camera looks +Z; +X is camera-left, so RIGHT must push body −X
26913
+ (this.ctl("TRANSLATE UP") - this.ctl("TRANSLATE DOWN")) * STRAFE,
26914
+ this.ctl("THRUST") * FWD - this.ctl("REVERSE") * REV,
26810
26915
  0
26811
26916
  );
26812
26917
  const torqueBody = vec(
26813
26918
  this.ctl("PITCH DOWN") - this.ctl("PITCH UP"),
26814
26919
  // pitch about X (DOWN − UP: nose-up is −X)
26815
- this.ctl("YAW RIGHT") - this.ctl("YAW LEFT"),
26816
- // yaw about Y
26920
+ this.ctl("YAW LEFT") - this.ctl("YAW RIGHT"),
26921
+ // yaw about Y (LEFT yaws nose left)
26817
26922
  this.ctl("ROLL RIGHT") - this.ctl("ROLL LEFT"),
26818
26923
  // roll about Z
26819
26924
  0
@@ -26824,7 +26929,7 @@ var ShipIn = class extends BenchNode {
26824
26929
  var SENSORS_IN_PORTS = [
26825
26930
  { type: 1 /* Number */, name: "LOCK MODE" },
26826
26931
  // 1 CLEAR … 6 CUSTOM
26827
- { type: 3 /* Vector */, name: "CUSTOM TARGET" },
26932
+ { type: 3 /* Vector */, name: "CUSTOM TGT" },
26828
26933
  // used when LOCK MODE = 6
26829
26934
  { type: 1 /* Number */, name: "BEAM X+" },
26830
26935
  { type: 1 /* Number */, name: "BEAM X-" },
@@ -26865,7 +26970,7 @@ var SensorsIn = class extends BenchNode {
26865
26970
  evaluate() {
26866
26971
  }
26867
26972
  refresh() {
26868
- SHIP_SIM.applyLockMode(Math.round(this.val("LOCK MODE")), this.vecIn("CUSTOM TARGET"));
26973
+ SHIP_SIM.applyLockMode(Math.round(this.val("LOCK MODE")), this.vecIn("CUSTOM TGT"));
26869
26974
  const yawRate = (this.val("BEAM X-") - this.val("BEAM X+")) / DATA_MAX;
26870
26975
  const pitchRate = (this.val("BEAM Y+") - this.val("BEAM Y-")) / DATA_MAX;
26871
26976
  SHIP_SIM.aimBeam(yawRate, pitchRate, Math.round(this.val("ELEMENT")), 0.02);
@@ -26898,13 +27003,22 @@ var SensorsOut = class extends BenchNode {
26898
27003
  this.out("LOCKED").value = 0;
26899
27004
  }
26900
27005
  const b = SHIP_SIM.beam;
26901
- const a = SHIP_SIM.asteroid;
26902
27006
  const bd = qrotate(SHIP_SIM.orient, qnormalize({ x: b.aim.x, y: b.aim.y, z: b.aim.z, w: 0 }));
26903
- const hit = raySphereHit(SHIP_SIM.pos, bd, a.pos, ASTEROID_LOCK_RADIUS);
26904
- this.out("MASS").value = b.massRange && hit !== null ? clampData(a.mass) : 0;
26905
- this.out("SCAN RANGE").value = b.massRange && hit !== null ? clampData(hit) : 0;
26906
- this.out("COMP %").value = b.spectro && hit !== null ? clampData((a.comp[b.element] ?? 0) * DATA_MAX) : 0;
26907
- this.out("SPECTRO POS").vec = { ...b.aim };
27007
+ let bestT = Infinity;
27008
+ let bestAst = null;
27009
+ for (const ast of SHIP_SIM.asteroidField) {
27010
+ const t = raySphereHit(SHIP_SIM.pos, bd, ast.pos, ast.radius);
27011
+ if (t !== null && t < bestT) {
27012
+ bestT = t;
27013
+ bestAst = ast;
27014
+ }
27015
+ }
27016
+ const aHit = bestAst;
27017
+ const hit = bestAst ? bestT : null;
27018
+ this.out("MASS").value = b.massRange && aHit !== null ? clampData(aHit.mass) : 0;
27019
+ this.out("SCAN RANGE").value = b.massRange && aHit !== null && hit !== null ? clampData(hit) : 0;
27020
+ this.out("COMP %").value = b.spectro && aHit !== null ? clampData((aHit.comp[b.element] ?? 0) * DATA_MAX) : 0;
27021
+ this.out("SPECTRO POS").vec = vec(b.aim.x * DATA_MAX, b.aim.y * DATA_MAX, b.aim.z * DATA_MAX, 0);
26908
27022
  }
26909
27023
  };
26910
27024
  var NODE_LIBRARY = [
@@ -26962,9 +27076,11 @@ var NODE_LIBRARY = [
26962
27076
  { kind: "slider", label: "SLIDER", group: "Controls", nodeType: "analog", make: (id) => new AnalogControl(id, false) },
26963
27077
  { kind: "button1", label: "BUTTON", group: "Controls", nodeType: "button", make: (id) => new Button(id, 1) },
26964
27078
  { kind: "button4", label: "4-BUTTON", group: "Controls", nodeType: "button", make: (id) => new Button(id, 4) },
26965
- { kind: "joy1", label: "1-AXIS JOYSTICK", group: "Controls", nodeType: "joystick", make: (id) => new Joystick(id, false) },
26966
- { kind: "joy2", label: "2-AXIS JOYSTICK", group: "Controls", nodeType: "joystick", make: (id) => new Joystick(id, true) },
26967
- { kind: "throttle", label: "THROTTLE", group: "Controls", nodeType: "throttle", make: (id) => new Joystick(id, false) },
27079
+ { kind: "joy1", label: "1-AXIS JOYSTICK Y", group: "Controls", nodeType: "joystick", make: (id) => new Joystick(id, "y") },
27080
+ { kind: "joy1x", label: "1-AXIS JOYSTICK X", group: "Controls", nodeType: "joystick", make: (id) => new Joystick(id, "x") },
27081
+ { kind: "joy2", label: "2-AXIS JOYSTICK", group: "Controls", nodeType: "joystick", make: (id) => new Joystick(id, "xy") },
27082
+ { kind: "joy2cross", label: "2-AXIS JOYSTICK +", group: "Controls", nodeType: "joystick", make: (id) => new Joystick(id, "cross") },
27083
+ { kind: "throttle", label: "THROTTLE", group: "Controls", nodeType: "throttle", make: (id) => new Joystick(id, "y") },
26968
27084
  { kind: "valuedisp", label: "VALUE DISPLAY", group: "Displays", nodeType: "readout", make: (id) => new ValueDisplay(id) },
26969
27085
  { kind: "elementdisp", label: "ELEMENT DISPLAY", group: "Displays", nodeType: "elementdisp", make: (id) => new ElementDisplay(id) },
26970
27086
  { kind: "counter", label: "COUNTER", group: "Displays", nodeType: "counter", make: (id) => new Counter(id) },
@@ -26973,6 +27089,7 @@ var NODE_LIBRARY = [
26973
27089
  { kind: "linearlight", label: "LINEAR LIGHT", group: "Displays", nodeType: "light", make: (id) => new LightDisplay(id, "linear") },
26974
27090
  { kind: "scope", label: "OSCILLOSCOPE", group: "Displays", nodeType: "scope", make: (id) => new Oscilloscope(id) },
26975
27091
  { kind: "speaker", label: "SPEAKER", group: "Displays", nodeType: "speaker", make: (id) => new Speaker(id) },
27092
+ { kind: "notes", label: "NOTE", group: "Helpers", nodeType: "notes", make: (id) => new Notes(id) },
26976
27093
  { kind: "sine", label: "SINE WAVE", group: "Signal", nodeType: "sine", make: (id) => new SineWave(id) },
26977
27094
  { kind: "triggered", label: "TRIGGERED", group: "Signal", make: (id) => new Triggered(id) },
26978
27095
  { kind: "proximity", label: "PROXIMITY SENSOR", group: "Sensors", make: (id) => new ProximitySensor(id) }
@@ -27096,17 +27213,23 @@ function gradeHeadless(spec, inputs, expected, ticks = 16) {
27096
27213
  }
27097
27214
 
27098
27215
  // server.ts
27099
- var room = process.env.NODE_LAB_ROOM;
27216
+ var initialRoom = process.env.NODE_LAB_ROOM;
27217
+ var useLocal = process.env.NODE_LAB_LOCAL === "1";
27100
27218
  var bridge;
27101
- if (room) {
27102
- const host = process.env.NODE_LAB_PARTY_HOST ?? DEFAULT_PARTY_HOST;
27103
- bridge = new PartyBridge(host, room);
27104
- process.stderr.write(`[node-lab-mcp] relay mode \u2014 room "${room}" @ ${host}
27105
- `);
27106
- } else {
27219
+ if (useLocal) {
27107
27220
  const local = new Bridge();
27108
27221
  local.start(BRIDGE_PORT);
27109
27222
  bridge = local;
27223
+ process.stderr.write(`[node-lab-mcp] local-WS mode \u2014 ws://localhost:${BRIDGE_PORT}
27224
+ `);
27225
+ } else {
27226
+ const host = process.env.NODE_LAB_PARTY_HOST ?? DEFAULT_PARTY_HOST;
27227
+ bridge = new PartyBridge(host, initialRoom);
27228
+ process.stderr.write(
27229
+ initialRoom ? `[node-lab-mcp] relay mode \u2014 room "${initialRoom}" @ ${host}
27230
+ ` : `[node-lab-mcp] relay mode (no room yet) \u2014 agent must call set_room with the pairing code @ ${host}
27231
+ `
27232
+ );
27110
27233
  }
27111
27234
  var server = new McpServer({ name: "node-lab", version: "0.1.0" });
27112
27235
  var json = (data) => ({ content: [{ type: "text", text: JSON.stringify(data, null, 2) }] });
@@ -27114,6 +27237,30 @@ var fail = (e) => ({
27114
27237
  content: [{ type: "text", text: `ERROR: ${e instanceof Error ? e.message : String(e)}` }],
27115
27238
  isError: true
27116
27239
  });
27240
+ server.registerTool(
27241
+ "set_room",
27242
+ {
27243
+ description: `Switch the PartyKit relay room this MCP connects to. Pair with any browser tab on the fly without restarting the MCP server. Pass the 4-4 pairing code from the lab's MCP modal (e.g. "138n-f2le"). Returns the new status \u2014 call bridge_status again after a short delay to confirm the editor joined.`,
27244
+ inputSchema: { room: external_exports.string() }
27245
+ },
27246
+ async ({ room }) => {
27247
+ try {
27248
+ if (!bridge.setRoom) throw new Error("set_room is only available in relay mode. Start the MCP without NODE_LAB_LOCAL=1.");
27249
+ await bridge.setRoom(room);
27250
+ return json({ room, status: bridge.status() });
27251
+ } catch (e) {
27252
+ return fail(e);
27253
+ }
27254
+ }
27255
+ );
27256
+ server.registerTool(
27257
+ "get_room",
27258
+ {
27259
+ description: "Read the current PartyKit relay room id (the 4-4 pairing code), plus the bridge status.",
27260
+ inputSchema: {}
27261
+ },
27262
+ async () => json({ room: bridge.currentRoom?.() ?? null, status: bridge.status() })
27263
+ );
27117
27264
  server.registerTool(
27118
27265
  "bridge_status",
27119
27266
  {
@@ -27207,6 +27354,18 @@ live(
27207
27354
  { nodeId: external_exports.string(), port: external_exports.string(), value: external_exports.number() },
27208
27355
  (a) => ({ cmd: "set_input", nodeId: a.nodeId, port: a.port, value: a.value })
27209
27356
  );
27357
+ live(
27358
+ "move_node",
27359
+ "Reposition a node on the canvas in flow-space coordinates (the same units that get_graph reports under position.x/y). Works on every node, fixtures included. Wires re-route automatically.",
27360
+ { id: external_exports.string(), x: external_exports.number(), y: external_exports.number() },
27361
+ (a) => ({ cmd: "move_node", id: a.id, x: a.x, y: a.y })
27362
+ );
27363
+ live(
27364
+ "set_collapsed",
27365
+ "Collapse a node down to just its header strip (collapsed=true) or restore the full card (collapsed=false). Purely a UI state \u2014 wiring, values, and engine behavior are unchanged.",
27366
+ { id: external_exports.string(), collapsed: external_exports.boolean() },
27367
+ (a) => ({ cmd: "set_collapsed", id: a.id, collapsed: a.collapsed })
27368
+ );
27210
27369
  live(
27211
27370
  "clear_graph",
27212
27371
  "Wipe all player-added nodes and cables on the live canvas back to the bare fixed walls.",
@@ -27221,5 +27380,5 @@ live(
27221
27380
  );
27222
27381
  var transport = new StdioServerTransport();
27223
27382
  await server.connect(transport);
27224
- process.stderr.write(`[node-lab-mcp] ready \u2014 ${room ? `relay room "${room}"` : `local bridge ws://localhost:${BRIDGE_PORT}`}
27383
+ process.stderr.write(`[node-lab-mcp] ready \u2014 ${bridge.status()}
27225
27384
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-lab-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "MCP server to drive the IFR Node Lab graph from any MCP client (Claude / Codex / Gemini …) — locally or, via a PartyKit relay, against the deployed app.",
5
5
  "type": "module",
6
6
  "bin": {