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.
- package/dist/index.js +238 -79
- 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
|
|
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}/${
|
|
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
|
|
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
|
|
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 =
|
|
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,
|
|
25456
|
+
constructor(host, room) {
|
|
25457
25457
|
this.host = host;
|
|
25458
|
-
this.room
|
|
25459
|
-
|
|
25460
|
-
|
|
25461
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25473
|
-
|
|
25474
|
-
|
|
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
|
|
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:
|
|
25650
|
-
{ type:
|
|
25651
|
-
{ type: 1 /* Number */, name: "DATA IN
|
|
25652
|
-
{ type: 1 /* Number */, name: "DATA IN
|
|
25653
|
-
{ type: 1 /* Number */, name: "DATA IN
|
|
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:
|
|
25657
|
-
{ type:
|
|
25658
|
-
{ type: 1 /* Number */, name: "DATA OUT
|
|
25659
|
-
{ type: 1 /* Number */, name: "DATA OUT
|
|
25660
|
-
{ type: 1 /* Number */, name: "DATA OUT
|
|
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,
|
|
26047
|
+
constructor(id, axis) {
|
|
26017
26048
|
super(id);
|
|
26018
|
-
this.
|
|
26019
|
-
if (
|
|
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.
|
|
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 =
|
|
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
|
|
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)
|
|
26595
|
-
//
|
|
26596
|
-
|
|
26597
|
-
|
|
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
|
|
26632
|
-
const
|
|
26633
|
-
|
|
26634
|
-
bestT
|
|
26635
|
-
|
|
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
|
-
|
|
26640
|
-
|
|
26641
|
-
}
|
|
26642
|
-
|
|
26643
|
-
this.lockedTarget = { pos: { ...
|
|
26644
|
-
this.lockKind =
|
|
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
|
|
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,
|
|
26775
|
-
|
|
26776
|
-
|
|
26777
|
-
|
|
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
|
-
|
|
26808
|
-
|
|
26809
|
-
this.ctl("
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
26904
|
-
|
|
26905
|
-
|
|
26906
|
-
|
|
26907
|
-
|
|
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,
|
|
26966
|
-
{ kind: "
|
|
26967
|
-
{ kind: "
|
|
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
|
|
27216
|
+
var initialRoom = process.env.NODE_LAB_ROOM;
|
|
27217
|
+
var useLocal = process.env.NODE_LAB_LOCAL === "1";
|
|
27100
27218
|
var bridge;
|
|
27101
|
-
if (
|
|
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 ${
|
|
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.
|
|
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": {
|