@chrrxs/robloxstudio-mcp-inspector 2.20.0 → 2.22.0

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 CHANGED
@@ -51,7 +51,26 @@ var init_bridge_service = __esm({
51
51
  // Keyed by pluginSessionId (the per-plugin GUID).
52
52
  instances = /* @__PURE__ */ new Map();
53
53
  instanceAliases = /* @__PURE__ */ new Map();
54
+ instanceRegisteredListeners = /* @__PURE__ */ new Set();
54
55
  requestTimeout = 3e4;
56
+ onInstanceRegistered(listener) {
57
+ this.instanceRegisteredListeners.add(listener);
58
+ for (const instance of this.getPublicInstances()) {
59
+ try {
60
+ listener(instance);
61
+ } catch {
62
+ }
63
+ }
64
+ return () => this.instanceRegisteredListeners.delete(listener);
65
+ }
66
+ notifyInstanceRegistered(instance) {
67
+ for (const listener of this.instanceRegisteredListeners) {
68
+ try {
69
+ listener(instance);
70
+ } catch {
71
+ }
72
+ }
73
+ }
55
74
  canonicalInstanceId(instanceId, placeId) {
56
75
  return publishedInstanceId(placeId) ?? instanceId;
57
76
  }
@@ -157,7 +176,7 @@ var init_bridge_service = __esm({
157
176
  }
158
177
  };
159
178
  }
160
- this.instances.set(pluginSessionId, {
179
+ const registered = {
161
180
  pluginSessionId,
162
181
  instanceId,
163
182
  role: assignedRole,
@@ -171,7 +190,9 @@ var init_bridge_service = __esm({
171
190
  versionMismatch,
172
191
  lastActivity: Date.now(),
173
192
  connectedAt: prior?.connectedAt ?? Date.now()
174
- });
193
+ };
194
+ this.instances.set(pluginSessionId, registered);
195
+ this.notifyInstanceRegistered(toPublic(registered));
175
196
  return { ok: true, assignedRole, instanceId };
176
197
  }
177
198
  unregisterInstance(pluginSessionId) {
@@ -369,7 +390,7 @@ var init_bridge_service = __esm({
369
390
  async sendRequest(endpoint, data, targetInstanceId, targetRole, timeoutMs = this.requestTimeout) {
370
391
  const requestId = uuidv4();
371
392
  const effectiveTimeoutMs = Math.max(1, timeoutMs);
372
- return new Promise((resolve4, reject) => {
393
+ return new Promise((resolve5, reject) => {
373
394
  const timeoutId = setTimeout(() => {
374
395
  if (this.pendingRequests.has(requestId)) {
375
396
  this.pendingRequests.delete(requestId);
@@ -384,7 +405,7 @@ var init_bridge_service = __esm({
384
405
  targetRole,
385
406
  timestamp: Date.now(),
386
407
  inFlight: false,
387
- resolve: resolve4,
408
+ resolve: resolve5,
388
409
  reject,
389
410
  timeoutId,
390
411
  timeoutMs: effectiveTimeoutMs
@@ -653,9 +674,57 @@ var init_mcp_compat = __esm({
653
674
  }
654
675
  });
655
676
 
677
+ // ../core/dist/auth.js
678
+ import { randomBytes, createHash, timingSafeEqual } from "crypto";
679
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
680
+ import { join, dirname } from "path";
681
+ import { homedir } from "os";
682
+ function authTokenFilePath() {
683
+ return join(homedir(), ".robloxstudio-mcp", "auth-token");
684
+ }
685
+ function resolveAuthToken() {
686
+ const noAuth = (process.env.ROBLOX_STUDIO_NO_AUTH || "").toLowerCase();
687
+ if (noAuth === "1" || noAuth === "true") {
688
+ return { source: "disabled" };
689
+ }
690
+ const envToken = process.env.ROBLOX_STUDIO_AUTH_TOKEN?.trim();
691
+ if (envToken) {
692
+ return { token: envToken, source: "env" };
693
+ }
694
+ const filePath = authTokenFilePath();
695
+ try {
696
+ if (existsSync(filePath)) {
697
+ const existing = readFileSync(filePath, "utf8").trim();
698
+ if (existing) {
699
+ return { token: existing, source: "file", filePath };
700
+ }
701
+ }
702
+ const fresh = randomBytes(32).toString("hex");
703
+ mkdirSync(dirname(filePath), { recursive: true, mode: 448 });
704
+ writeFileSync(filePath, fresh + "\n", { encoding: "utf8", mode: 384 });
705
+ try {
706
+ chmodSync(filePath, 384);
707
+ } catch {
708
+ }
709
+ return { token: fresh, source: "file", filePath };
710
+ } catch (err2) {
711
+ console.error(`[auth] Could not read/create ${filePath} (${err2 instanceof Error ? err2.message : err2}). Using an in-memory token for this process; set ROBLOX_STUDIO_AUTH_TOKEN to share one across sessions.`);
712
+ return { token: randomBytes(32).toString("hex"), source: "file" };
713
+ }
714
+ }
715
+ function tokensMatch(provided, expected) {
716
+ const a = createHash("sha256").update(provided).digest();
717
+ const b = createHash("sha256").update(expected).digest();
718
+ return timingSafeEqual(a, b);
719
+ }
720
+ var init_auth = __esm({
721
+ "../core/dist/auth.js"() {
722
+ "use strict";
723
+ }
724
+ });
725
+
656
726
  // ../core/dist/http-server.js
657
727
  import express from "express";
658
- import cors from "cors";
659
728
  import http from "http";
660
729
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
661
730
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -708,7 +777,7 @@ function requiredClosedLineRange(body, toolName) {
708
777
  }
709
778
  return { startLine: parsed.startLine, endLine: parsed.endLine };
710
779
  }
711
- function createHttpServer(tools, bridge, allowedTools, serverConfig) {
780
+ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
712
781
  const app = express();
713
782
  let mcpServerActive = false;
714
783
  let lastMCPActivity = 0;
@@ -738,7 +807,49 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
738
807
  const isPluginConnected = () => {
739
808
  return bridge.getInstances().length > 0;
740
809
  };
741
- app.use(cors());
810
+ const allowedOrigins = new Set(security?.allowedOrigins ?? []);
811
+ app.use((req, res, next) => {
812
+ const origin = req.headers.origin;
813
+ if (typeof origin !== "string" || origin === "") {
814
+ next();
815
+ return;
816
+ }
817
+ if (!allowedOrigins.has(origin)) {
818
+ res.status(403).json({
819
+ error: "forbidden_origin",
820
+ message: `Cross-origin requests are not allowed from ${origin}. Set ROBLOX_STUDIO_ALLOWED_ORIGINS to allowlist specific origins.`
821
+ });
822
+ return;
823
+ }
824
+ res.setHeader("Access-Control-Allow-Origin", origin);
825
+ res.setHeader("Vary", "Origin");
826
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
827
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-MCP-Auth, Mcp-Protocol-Version");
828
+ if (req.method === "OPTIONS") {
829
+ res.status(204).end();
830
+ return;
831
+ }
832
+ next();
833
+ });
834
+ const authToken = security?.authToken;
835
+ const authRequired = (path6) => path6 === "/mcp" || path6.startsWith("/mcp/") || path6 === "/proxy" || path6 === "/instances" || path6 === "/unregister-instance-id";
836
+ app.use((req, res, next) => {
837
+ if (!authToken || !authRequired(req.path)) {
838
+ next();
839
+ return;
840
+ }
841
+ const headerToken = req.headers["x-mcp-auth"];
842
+ const bearer = typeof req.headers.authorization === "string" && req.headers.authorization.startsWith("Bearer ") ? req.headers.authorization.slice("Bearer ".length) : void 0;
843
+ const provided = typeof headerToken === "string" && headerToken !== "" ? headerToken : bearer;
844
+ if (provided !== void 0 && tokensMatch(provided, authToken)) {
845
+ next();
846
+ return;
847
+ }
848
+ res.status(401).json({
849
+ error: "unauthorized",
850
+ message: 'Missing or invalid auth token. Send it as "X-MCP-Auth: <token>" or "Authorization: Bearer <token>". ' + (security?.authTokenHint ?? "The token is in ~/.robloxstudio-mcp/auth-token (or ROBLOX_STUDIO_AUTH_TOKEN).")
851
+ });
852
+ });
742
853
  app.use(express.json({ limit: "50mb" }));
743
854
  app.use(express.urlencoded({ limit: "50mb", extended: true }));
744
855
  app.get("/health", (req, res) => {
@@ -801,11 +912,11 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
801
912
  pluginVariant: typeof pluginVariant === "string" ? pluginVariant : "unknown",
802
913
  serverVersion: serverConfig?.version ?? ""
803
914
  });
804
- } catch (err) {
915
+ } catch (err2) {
805
916
  res.status(500).json({
806
917
  success: false,
807
918
  error: "ready_registration_exception",
808
- message: err instanceof Error ? err.message : String(err),
919
+ message: err2 instanceof Error ? err2.message : String(err2),
809
920
  request: requestContext
810
921
  });
811
922
  return;
@@ -956,8 +1067,8 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
956
1067
  try {
957
1068
  const response = await bridge.sendRequest(endpoint, data, targetInstanceId, targetRole);
958
1069
  res.json({ response });
959
- } catch (err) {
960
- res.status(500).json({ error: err.message || "Proxy request failed" });
1070
+ } catch (err2) {
1071
+ res.status(500).json({ error: err2.message || "Proxy request failed" });
961
1072
  }
962
1073
  });
963
1074
  if (serverConfig) {
@@ -1069,19 +1180,19 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
1069
1180
  return app;
1070
1181
  }
1071
1182
  function listenWithRetry(app, host, startPort, maxAttempts = 5) {
1072
- return new Promise(async (resolve4, reject) => {
1183
+ return new Promise(async (resolve5, reject) => {
1073
1184
  for (let i = 0; i < maxAttempts; i++) {
1074
1185
  const port = startPort + i;
1075
1186
  try {
1076
1187
  const server = await bindPort(app, host, port);
1077
- resolve4({ server, port });
1188
+ resolve5({ server, port });
1078
1189
  return;
1079
- } catch (err) {
1080
- if (err.code === "EADDRINUSE") {
1190
+ } catch (err2) {
1191
+ if (err2.code === "EADDRINUSE") {
1081
1192
  console.error(`Port ${port} in use, trying next...`);
1082
1193
  continue;
1083
1194
  }
1084
- reject(err);
1195
+ reject(err2);
1085
1196
  return;
1086
1197
  }
1087
1198
  }
@@ -1089,16 +1200,16 @@ function listenWithRetry(app, host, startPort, maxAttempts = 5) {
1089
1200
  });
1090
1201
  }
1091
1202
  function bindPort(app, host, port) {
1092
- return new Promise((resolve4, reject) => {
1203
+ return new Promise((resolve5, reject) => {
1093
1204
  const server = http.createServer(app);
1094
- const onError = (err) => {
1205
+ const onError = (err2) => {
1095
1206
  server.removeListener("error", onError);
1096
- reject(err);
1207
+ reject(err2);
1097
1208
  };
1098
1209
  server.once("error", onError);
1099
1210
  server.listen(port, host, () => {
1100
1211
  server.removeListener("error", onError);
1101
- resolve4(server);
1212
+ resolve5(server);
1102
1213
  });
1103
1214
  });
1104
1215
  }
@@ -1108,7 +1219,9 @@ var init_http_server = __esm({
1108
1219
  "use strict";
1109
1220
  init_bridge_service();
1110
1221
  init_mcp_compat();
1222
+ init_auth();
1111
1223
  TOOL_HANDLERS = {
1224
+ get_roblox_skills: (tools, body) => tools.getRobloxSkills(body.action, body.name),
1112
1225
  get_roblox_docs: (tools, body) => tools.getRobloxDocs(body.name, body.doc_type, body.section),
1113
1226
  get_file_tree: (tools, body) => tools.getFileTree(body.path, body.instance_id),
1114
1227
  search_files: (tools, body) => tools.searchFiles(body.query, body.searchType, body.instance_id),
@@ -1275,8 +1388,844 @@ var init_studio_client = __esm({
1275
1388
  }
1276
1389
  });
1277
1390
 
1391
+ // ../core/dist/tools/build-interpreter.js
1392
+ import * as acorn from "acorn";
1393
+ function isPlainObject(value) {
1394
+ if (value === null || typeof value !== "object")
1395
+ return false;
1396
+ const proto = Object.getPrototypeOf(value);
1397
+ return proto === Object.prototype || proto === null;
1398
+ }
1399
+ function runRestrictedScript(code, globals, options) {
1400
+ const timeoutMs = options?.timeoutMs ?? 1e4;
1401
+ const maxOps = options?.maxOps ?? 2e7;
1402
+ const deadline = Date.now() + timeoutMs;
1403
+ let ops = 0;
1404
+ let ast;
1405
+ try {
1406
+ ast = acorn.parse(code, { ecmaVersion: 2022, sourceType: "script" });
1407
+ } catch (err2) {
1408
+ throw new Error(`Syntax error: ${err2.message}`);
1409
+ }
1410
+ const globalScope = new Scope(void 0, true);
1411
+ for (const [name, value] of Object.entries(globals)) {
1412
+ globalScope.declare(name, value, "const");
1413
+ }
1414
+ function tick() {
1415
+ ops++;
1416
+ if (ops > maxOps) {
1417
+ throw new ScriptTimeoutError(`Operation budget exceeded (${maxOps} ops)`);
1418
+ }
1419
+ if ((ops & 8191) === 0 && Date.now() > deadline) {
1420
+ throw new ScriptTimeoutError(`Execution timed out after ${timeoutMs}ms`);
1421
+ }
1422
+ }
1423
+ function assertPropAllowed(prop) {
1424
+ const name = typeof prop === "symbol" ? String(prop) : String(prop);
1425
+ if (FORBIDDEN_PROPS.has(name)) {
1426
+ throw new Error(`Access to property "${name}" is not allowed`);
1427
+ }
1428
+ return name;
1429
+ }
1430
+ function getMember(obj, prop) {
1431
+ if (obj === null || obj === void 0) {
1432
+ throw new Error(`Cannot read property "${String(prop)}" of ${obj}`);
1433
+ }
1434
+ const name = assertPropAllowed(prop);
1435
+ if (Array.isArray(obj)) {
1436
+ if (name === "length")
1437
+ return obj.length;
1438
+ const idx = Number(name);
1439
+ if (Number.isInteger(idx) && idx >= 0)
1440
+ return obj[idx];
1441
+ if (ARRAY_MEMBERS.has(name))
1442
+ return obj[name];
1443
+ throw new Error(`Array method "${name}" is not allowed`);
1444
+ }
1445
+ if (typeof obj === "string") {
1446
+ if (name === "length")
1447
+ return obj.length;
1448
+ const idx = Number(name);
1449
+ if (Number.isInteger(idx) && idx >= 0)
1450
+ return obj[idx];
1451
+ if (STRING_MEMBERS.has(name))
1452
+ return obj[name];
1453
+ throw new Error(`String method "${name}" is not allowed`);
1454
+ }
1455
+ if (typeof obj === "number") {
1456
+ if (NUMBER_MEMBERS.has(name))
1457
+ return obj[name];
1458
+ throw new Error(`Number method "${name}" is not allowed`);
1459
+ }
1460
+ if (isPlainObject(obj)) {
1461
+ if (Object.prototype.hasOwnProperty.call(obj, name))
1462
+ return obj[name];
1463
+ return void 0;
1464
+ }
1465
+ throw new Error(`Property access on this value type is not allowed`);
1466
+ }
1467
+ function setMember(obj, prop, value) {
1468
+ if (obj === null || obj === void 0) {
1469
+ throw new Error(`Cannot set property "${String(prop)}" of ${obj}`);
1470
+ }
1471
+ const name = assertPropAllowed(prop);
1472
+ if (Object.isFrozen(obj)) {
1473
+ throw new Error(`Cannot assign to property "${name}" of a frozen object`);
1474
+ }
1475
+ if (Array.isArray(obj)) {
1476
+ const idx = Number(name);
1477
+ if (Number.isInteger(idx) && idx >= 0 || name === "length") {
1478
+ obj[name] = value;
1479
+ return;
1480
+ }
1481
+ throw new Error(`Cannot set property "${name}" on an array`);
1482
+ }
1483
+ if (isPlainObject(obj)) {
1484
+ obj[name] = value;
1485
+ return;
1486
+ }
1487
+ throw new Error(`Property assignment on this value type is not allowed`);
1488
+ }
1489
+ function callFunction(fn, thisArg, args, desc) {
1490
+ if (typeof fn !== "function") {
1491
+ throw new Error(`${desc} is not a function`);
1492
+ }
1493
+ return fn.apply(thisArg, args);
1494
+ }
1495
+ function bindPattern(pattern, value, scope, kind) {
1496
+ switch (pattern.type) {
1497
+ case "Identifier":
1498
+ scope.declare(pattern.name, value, kind);
1499
+ return;
1500
+ case "AssignmentPattern":
1501
+ bindPattern(pattern.left, value === void 0 ? evaluate(pattern.right, scope) : value, scope, kind);
1502
+ return;
1503
+ case "ArrayPattern": {
1504
+ if (value === null || value === void 0)
1505
+ throw new Error("Cannot destructure null/undefined");
1506
+ let i = 0;
1507
+ for (const el of pattern.elements) {
1508
+ if (el === null) {
1509
+ i++;
1510
+ continue;
1511
+ }
1512
+ if (el.type === "RestElement") {
1513
+ bindPattern(el.argument, Array.isArray(value) ? value.slice(i) : [], scope, kind);
1514
+ break;
1515
+ }
1516
+ bindPattern(el, getMember(value, i), scope, kind);
1517
+ i++;
1518
+ }
1519
+ return;
1520
+ }
1521
+ case "ObjectPattern": {
1522
+ if (value === null || value === void 0)
1523
+ throw new Error("Cannot destructure null/undefined");
1524
+ const used = /* @__PURE__ */ new Set();
1525
+ for (const propNode of pattern.properties) {
1526
+ if (propNode.type === "RestElement") {
1527
+ const rest = {};
1528
+ if (isPlainObject(value)) {
1529
+ for (const k of Object.keys(value)) {
1530
+ if (!used.has(k) && !FORBIDDEN_PROPS.has(k))
1531
+ rest[k] = value[k];
1532
+ }
1533
+ }
1534
+ bindPattern(propNode.argument, rest, scope, kind);
1535
+ continue;
1536
+ }
1537
+ const key = propNode.computed ? String(evaluate(propNode.key, scope)) : propNode.key.type === "Identifier" ? propNode.key.name : String(propNode.key.value);
1538
+ used.add(key);
1539
+ bindPattern(propNode.value, getMember(value, key), scope, kind);
1540
+ }
1541
+ return;
1542
+ }
1543
+ default:
1544
+ throw new Error(`Unsupported binding pattern: ${pattern.type}`);
1545
+ }
1546
+ }
1547
+ function makeFunction(node, closureScope) {
1548
+ return (...args) => {
1549
+ tick();
1550
+ const fnScope = new Scope(closureScope, true);
1551
+ node.params.forEach((param, i) => {
1552
+ if (param.type === "RestElement") {
1553
+ bindPattern(param.argument, args.slice(i), fnScope, "let");
1554
+ } else {
1555
+ bindPattern(param, args[i], fnScope, "let");
1556
+ }
1557
+ });
1558
+ try {
1559
+ if (node.body.type === "BlockStatement") {
1560
+ executeBlock(node.body.body, new Scope(fnScope));
1561
+ return void 0;
1562
+ }
1563
+ return evaluate(node.body, fnScope);
1564
+ } catch (signal) {
1565
+ if (signal instanceof ReturnSignal)
1566
+ return signal.value;
1567
+ throw signal;
1568
+ }
1569
+ };
1570
+ }
1571
+ function hoistFunctions(statements, scope) {
1572
+ for (const stmt of statements) {
1573
+ if (stmt.type === "FunctionDeclaration" && stmt.id) {
1574
+ if (stmt.async || stmt.generator)
1575
+ throw new Error("async/generator functions are not supported");
1576
+ scope.declare(stmt.id.name, makeFunction(stmt, scope), "let");
1577
+ }
1578
+ }
1579
+ }
1580
+ function executeBlock(statements, scope) {
1581
+ hoistFunctions(statements, scope);
1582
+ for (const stmt of statements) {
1583
+ execute(stmt, scope);
1584
+ }
1585
+ }
1586
+ function execute(node, scope) {
1587
+ tick();
1588
+ switch (node.type) {
1589
+ case "ExpressionStatement":
1590
+ evaluate(node.expression, scope);
1591
+ return;
1592
+ case "VariableDeclaration":
1593
+ for (const decl of node.declarations) {
1594
+ const value = decl.init ? evaluate(decl.init, scope) : void 0;
1595
+ bindPattern(decl.id, value, scope, node.kind);
1596
+ }
1597
+ return;
1598
+ case "FunctionDeclaration":
1599
+ return;
1600
+ // hoisted by executeBlock
1601
+ case "BlockStatement":
1602
+ executeBlock(node.body, new Scope(scope));
1603
+ return;
1604
+ case "EmptyStatement":
1605
+ return;
1606
+ case "IfStatement":
1607
+ if (evaluate(node.test, scope)) {
1608
+ execute(node.consequent, scope);
1609
+ } else if (node.alternate) {
1610
+ execute(node.alternate, scope);
1611
+ }
1612
+ return;
1613
+ case "ForStatement": {
1614
+ const forScope = new Scope(scope);
1615
+ if (node.init) {
1616
+ if (node.init.type === "VariableDeclaration")
1617
+ execute(node.init, forScope);
1618
+ else
1619
+ evaluate(node.init, forScope);
1620
+ }
1621
+ while (node.test === null || evaluate(node.test, forScope)) {
1622
+ tick();
1623
+ try {
1624
+ execute(node.body, new Scope(forScope));
1625
+ } catch (signal) {
1626
+ if (signal instanceof BreakSignal)
1627
+ break;
1628
+ if (!(signal instanceof ContinueSignal))
1629
+ throw signal;
1630
+ }
1631
+ if (node.update)
1632
+ evaluate(node.update, forScope);
1633
+ }
1634
+ return;
1635
+ }
1636
+ case "ForOfStatement": {
1637
+ const iterable = evaluate(node.right, scope);
1638
+ if (!Array.isArray(iterable) && typeof iterable !== "string") {
1639
+ throw new Error("for...of only supports arrays and strings");
1640
+ }
1641
+ for (const item of iterable) {
1642
+ tick();
1643
+ const iterScope = new Scope(scope);
1644
+ if (node.left.type === "VariableDeclaration") {
1645
+ bindPattern(node.left.declarations[0].id, item, iterScope, node.left.kind);
1646
+ } else {
1647
+ assignTo(node.left, item, iterScope);
1648
+ }
1649
+ try {
1650
+ execute(node.body, iterScope);
1651
+ } catch (signal) {
1652
+ if (signal instanceof BreakSignal)
1653
+ break;
1654
+ if (!(signal instanceof ContinueSignal))
1655
+ throw signal;
1656
+ }
1657
+ }
1658
+ return;
1659
+ }
1660
+ case "ForInStatement": {
1661
+ const obj = evaluate(node.right, scope);
1662
+ const keys = isPlainObject(obj) ? Object.keys(obj) : Array.isArray(obj) ? obj.map((_, i) => String(i)) : [];
1663
+ for (const key of keys) {
1664
+ tick();
1665
+ const iterScope = new Scope(scope);
1666
+ if (node.left.type === "VariableDeclaration") {
1667
+ bindPattern(node.left.declarations[0].id, key, iterScope, node.left.kind);
1668
+ } else {
1669
+ assignTo(node.left, key, iterScope);
1670
+ }
1671
+ try {
1672
+ execute(node.body, iterScope);
1673
+ } catch (signal) {
1674
+ if (signal instanceof BreakSignal)
1675
+ break;
1676
+ if (!(signal instanceof ContinueSignal))
1677
+ throw signal;
1678
+ }
1679
+ }
1680
+ return;
1681
+ }
1682
+ case "WhileStatement":
1683
+ while (evaluate(node.test, scope)) {
1684
+ tick();
1685
+ try {
1686
+ execute(node.body, new Scope(scope));
1687
+ } catch (signal) {
1688
+ if (signal instanceof BreakSignal)
1689
+ break;
1690
+ if (!(signal instanceof ContinueSignal))
1691
+ throw signal;
1692
+ }
1693
+ }
1694
+ return;
1695
+ case "DoWhileStatement":
1696
+ do {
1697
+ tick();
1698
+ try {
1699
+ execute(node.body, new Scope(scope));
1700
+ } catch (signal) {
1701
+ if (signal instanceof BreakSignal)
1702
+ break;
1703
+ if (!(signal instanceof ContinueSignal))
1704
+ throw signal;
1705
+ }
1706
+ } while (evaluate(node.test, scope));
1707
+ return;
1708
+ case "BreakStatement":
1709
+ throw new BreakSignal(node.label?.name);
1710
+ case "ContinueStatement":
1711
+ throw new ContinueSignal(node.label?.name);
1712
+ case "ReturnStatement":
1713
+ throw new ReturnSignal(node.argument ? evaluate(node.argument, scope) : void 0);
1714
+ case "ThrowStatement":
1715
+ throw evaluate(node.argument, scope);
1716
+ case "TryStatement": {
1717
+ try {
1718
+ execute(node.block, scope);
1719
+ } catch (err2) {
1720
+ if (err2 instanceof BreakSignal || err2 instanceof ContinueSignal || err2 instanceof ReturnSignal)
1721
+ throw err2;
1722
+ if (err2 instanceof ScriptTimeoutError)
1723
+ throw err2;
1724
+ if (node.handler) {
1725
+ const catchScope = new Scope(scope);
1726
+ if (node.handler.param) {
1727
+ const message = err2 instanceof Error ? { message: err2.message } : err2;
1728
+ bindPattern(node.handler.param, message, catchScope, "let");
1729
+ }
1730
+ execute(node.handler.body, catchScope);
1731
+ }
1732
+ } finally {
1733
+ if (node.finalizer)
1734
+ execute(node.finalizer, scope);
1735
+ }
1736
+ return;
1737
+ }
1738
+ case "SwitchStatement": {
1739
+ const disc = evaluate(node.discriminant, scope);
1740
+ const switchScope = new Scope(scope);
1741
+ let matched = false;
1742
+ try {
1743
+ for (const caseNode of node.cases) {
1744
+ if (!matched) {
1745
+ if (caseNode.test === null)
1746
+ matched = true;
1747
+ else if (evaluate(caseNode.test, switchScope) === disc)
1748
+ matched = true;
1749
+ }
1750
+ if (matched) {
1751
+ for (const stmt of caseNode.consequent)
1752
+ execute(stmt, switchScope);
1753
+ }
1754
+ }
1755
+ } catch (signal) {
1756
+ if (!(signal instanceof BreakSignal))
1757
+ throw signal;
1758
+ }
1759
+ return;
1760
+ }
1761
+ case "LabeledStatement":
1762
+ try {
1763
+ execute(node.body, scope);
1764
+ } catch (signal) {
1765
+ if (signal instanceof BreakSignal && signal.label === node.label.name)
1766
+ return;
1767
+ throw signal;
1768
+ }
1769
+ return;
1770
+ default:
1771
+ throw new Error(`Unsupported statement: ${node.type}`);
1772
+ }
1773
+ }
1774
+ function assignTo(target, value, scope) {
1775
+ if (target.type === "Identifier") {
1776
+ scope.set(target.name, value);
1777
+ } else if (target.type === "MemberExpression") {
1778
+ const obj = evaluate(target.object, scope);
1779
+ const prop = target.computed ? evaluate(target.property, scope) : target.property.name;
1780
+ setMember(obj, prop, value);
1781
+ } else {
1782
+ throw new Error(`Unsupported assignment target: ${target.type}`);
1783
+ }
1784
+ }
1785
+ function evaluate(node, scope) {
1786
+ tick();
1787
+ switch (node.type) {
1788
+ case "Literal":
1789
+ if (node.regex)
1790
+ throw new Error("Regular expression literals are not supported");
1791
+ return node.value;
1792
+ case "TemplateLiteral": {
1793
+ let out = "";
1794
+ node.quasis.forEach((quasi, i) => {
1795
+ out += quasi.value.cooked ?? "";
1796
+ if (i < node.expressions.length)
1797
+ out += String(evaluate(node.expressions[i], scope));
1798
+ });
1799
+ return out;
1800
+ }
1801
+ case "Identifier":
1802
+ return scope.get(node.name);
1803
+ case "ArrayExpression": {
1804
+ const arr = [];
1805
+ for (const el of node.elements) {
1806
+ if (el === null) {
1807
+ arr.length++;
1808
+ continue;
1809
+ }
1810
+ if (el.type === "SpreadElement") {
1811
+ const spread = evaluate(el.argument, scope);
1812
+ if (!Array.isArray(spread) && typeof spread !== "string") {
1813
+ throw new Error("Spread only supports arrays and strings");
1814
+ }
1815
+ for (const item of spread)
1816
+ arr.push(item);
1817
+ } else {
1818
+ arr.push(evaluate(el, scope));
1819
+ }
1820
+ }
1821
+ return arr;
1822
+ }
1823
+ case "ObjectExpression": {
1824
+ const obj = {};
1825
+ for (const prop of node.properties) {
1826
+ if (prop.type === "SpreadElement") {
1827
+ const spread = evaluate(prop.argument, scope);
1828
+ if (isPlainObject(spread)) {
1829
+ for (const k of Object.keys(spread)) {
1830
+ if (!FORBIDDEN_PROPS.has(k))
1831
+ obj[k] = spread[k];
1832
+ }
1833
+ }
1834
+ continue;
1835
+ }
1836
+ if (prop.kind !== "init")
1837
+ throw new Error("Getters/setters are not supported");
1838
+ const key = prop.computed ? String(evaluate(prop.key, scope)) : prop.key.type === "Identifier" ? prop.key.name : String(prop.key.value);
1839
+ assertPropAllowed(key);
1840
+ obj[key] = evaluate(prop.value, scope);
1841
+ }
1842
+ return obj;
1843
+ }
1844
+ case "ArrowFunctionExpression":
1845
+ case "FunctionExpression":
1846
+ if (node.async || node.generator)
1847
+ throw new Error("async/generator functions are not supported");
1848
+ return makeFunction(node, scope);
1849
+ case "UnaryExpression": {
1850
+ if (node.operator === "typeof" && node.argument.type === "Identifier" && !scope.has(node.argument.name)) {
1851
+ return "undefined";
1852
+ }
1853
+ if (node.operator === "delete") {
1854
+ if (node.argument.type !== "MemberExpression")
1855
+ return true;
1856
+ const obj = evaluate(node.argument.object, scope);
1857
+ const prop = node.argument.computed ? evaluate(node.argument.property, scope) : node.argument.property.name;
1858
+ const name = assertPropAllowed(prop);
1859
+ if (isPlainObject(obj) && !Object.isFrozen(obj)) {
1860
+ delete obj[name];
1861
+ return true;
1862
+ }
1863
+ throw new Error("delete is only allowed on plain objects");
1864
+ }
1865
+ const arg = evaluate(node.argument, scope);
1866
+ switch (node.operator) {
1867
+ case "-":
1868
+ return -arg;
1869
+ case "+":
1870
+ return +arg;
1871
+ case "!":
1872
+ return !arg;
1873
+ case "~":
1874
+ return ~arg;
1875
+ case "typeof":
1876
+ return typeof arg;
1877
+ case "void":
1878
+ return void 0;
1879
+ default:
1880
+ throw new Error(`Unsupported unary operator: ${node.operator}`);
1881
+ }
1882
+ }
1883
+ case "UpdateExpression": {
1884
+ const old = node.argument.type === "Identifier" ? scope.get(node.argument.name) : getMember(evaluate(node.argument.object, scope), node.argument.computed ? evaluate(node.argument.property, scope) : node.argument.property.name);
1885
+ const next = node.operator === "++" ? old + 1 : old - 1;
1886
+ assignTo(node.argument, next, scope);
1887
+ return node.prefix ? next : old;
1888
+ }
1889
+ case "BinaryExpression": {
1890
+ if (node.operator === "in") {
1891
+ const key = assertPropAllowed(evaluate(node.left, scope));
1892
+ const obj = evaluate(node.right, scope);
1893
+ if (isPlainObject(obj))
1894
+ return Object.prototype.hasOwnProperty.call(obj, key);
1895
+ if (Array.isArray(obj))
1896
+ return Number(key) >= 0 && Number(key) < obj.length;
1897
+ throw new Error('"in" is only supported on plain objects and arrays');
1898
+ }
1899
+ if (node.operator === "instanceof")
1900
+ throw new Error("instanceof is not supported");
1901
+ const l = evaluate(node.left, scope);
1902
+ const r = evaluate(node.right, scope);
1903
+ switch (node.operator) {
1904
+ case "+":
1905
+ return l + r;
1906
+ case "-":
1907
+ return l - r;
1908
+ case "*":
1909
+ return l * r;
1910
+ case "/":
1911
+ return l / r;
1912
+ case "%":
1913
+ return l % r;
1914
+ case "**":
1915
+ return l ** r;
1916
+ case "==":
1917
+ return l == r;
1918
+ // eslint-disable-line eqeqeq
1919
+ case "!=":
1920
+ return l != r;
1921
+ // eslint-disable-line eqeqeq
1922
+ case "===":
1923
+ return l === r;
1924
+ case "!==":
1925
+ return l !== r;
1926
+ case "<":
1927
+ return l < r;
1928
+ case "<=":
1929
+ return l <= r;
1930
+ case ">":
1931
+ return l > r;
1932
+ case ">=":
1933
+ return l >= r;
1934
+ case "&":
1935
+ return l & r;
1936
+ case "|":
1937
+ return l | r;
1938
+ case "^":
1939
+ return l ^ r;
1940
+ case "<<":
1941
+ return l << r;
1942
+ case ">>":
1943
+ return l >> r;
1944
+ case ">>>":
1945
+ return l >>> r;
1946
+ default:
1947
+ throw new Error(`Unsupported binary operator: ${node.operator}`);
1948
+ }
1949
+ }
1950
+ case "LogicalExpression": {
1951
+ const l = evaluate(node.left, scope);
1952
+ if (node.operator === "&&")
1953
+ return l ? evaluate(node.right, scope) : l;
1954
+ if (node.operator === "||")
1955
+ return l ? l : evaluate(node.right, scope);
1956
+ if (node.operator === "??")
1957
+ return l ?? evaluate(node.right, scope);
1958
+ throw new Error(`Unsupported logical operator: ${node.operator}`);
1959
+ }
1960
+ case "AssignmentExpression": {
1961
+ let value;
1962
+ if (node.operator === "=") {
1963
+ value = evaluate(node.right, scope);
1964
+ } else {
1965
+ const current = node.left.type === "Identifier" ? scope.get(node.left.name) : getMember(evaluate(node.left.object, scope), node.left.computed ? evaluate(node.left.property, scope) : node.left.property.name);
1966
+ const rhs = () => evaluate(node.right, scope);
1967
+ switch (node.operator) {
1968
+ case "+=":
1969
+ value = current + rhs();
1970
+ break;
1971
+ case "-=":
1972
+ value = current - rhs();
1973
+ break;
1974
+ case "*=":
1975
+ value = current * rhs();
1976
+ break;
1977
+ case "/=":
1978
+ value = current / rhs();
1979
+ break;
1980
+ case "%=":
1981
+ value = current % rhs();
1982
+ break;
1983
+ case "**=":
1984
+ value = current ** rhs();
1985
+ break;
1986
+ case "&=":
1987
+ value = current & rhs();
1988
+ break;
1989
+ case "|=":
1990
+ value = current | rhs();
1991
+ break;
1992
+ case "^=":
1993
+ value = current ^ rhs();
1994
+ break;
1995
+ case "<<=":
1996
+ value = current << rhs();
1997
+ break;
1998
+ case ">>=":
1999
+ value = current >> rhs();
2000
+ break;
2001
+ case ">>>=":
2002
+ value = current >>> rhs();
2003
+ break;
2004
+ case "&&=":
2005
+ value = current ? rhs() : current;
2006
+ break;
2007
+ case "||=":
2008
+ value = current ? current : rhs();
2009
+ break;
2010
+ case "??=":
2011
+ value = current ?? rhs();
2012
+ break;
2013
+ default:
2014
+ throw new Error(`Unsupported assignment operator: ${node.operator}`);
2015
+ }
2016
+ }
2017
+ assignTo(node.left, value, scope);
2018
+ return value;
2019
+ }
2020
+ case "ConditionalExpression":
2021
+ return evaluate(node.test, scope) ? evaluate(node.consequent, scope) : evaluate(node.alternate, scope);
2022
+ case "CallExpression": {
2023
+ const args = [];
2024
+ for (const argNode of node.arguments) {
2025
+ if (argNode.type === "SpreadElement") {
2026
+ const spread = evaluate(argNode.argument, scope);
2027
+ if (!Array.isArray(spread) && typeof spread !== "string") {
2028
+ throw new Error("Spread only supports arrays and strings");
2029
+ }
2030
+ for (const item of spread)
2031
+ args.push(item);
2032
+ } else {
2033
+ args.push(evaluate(argNode, scope));
2034
+ }
2035
+ }
2036
+ if (node.callee.type === "MemberExpression") {
2037
+ const obj = evaluate(node.callee.object, scope);
2038
+ if (node.optional && (obj === null || obj === void 0))
2039
+ return void 0;
2040
+ const prop = node.callee.computed ? evaluate(node.callee.property, scope) : node.callee.property.name;
2041
+ const method = getMember(obj, prop);
2042
+ if (node.callee.optional && (method === null || method === void 0))
2043
+ return void 0;
2044
+ return callFunction(method, obj, args, `${String(prop)}`);
2045
+ }
2046
+ const fn = evaluate(node.callee, scope);
2047
+ if (node.optional && (fn === null || fn === void 0))
2048
+ return void 0;
2049
+ const desc = node.callee.type === "Identifier" ? node.callee.name : "expression";
2050
+ return callFunction(fn, void 0, args, desc);
2051
+ }
2052
+ case "MemberExpression": {
2053
+ const obj = evaluate(node.object, scope);
2054
+ if (node.optional && (obj === null || obj === void 0))
2055
+ return void 0;
2056
+ const prop = node.computed ? evaluate(node.property, scope) : node.property.name;
2057
+ return getMember(obj, prop);
2058
+ }
2059
+ case "ChainExpression":
2060
+ return evaluate(node.expression, scope);
2061
+ case "SequenceExpression": {
2062
+ let result;
2063
+ for (const expr of node.expressions)
2064
+ result = evaluate(expr, scope);
2065
+ return result;
2066
+ }
2067
+ case "NewExpression":
2068
+ throw new Error('"new" is not supported in build code');
2069
+ case "ThisExpression":
2070
+ throw new Error('"this" is not supported in build code');
2071
+ case "AwaitExpression":
2072
+ throw new Error("await is not supported in build code");
2073
+ case "TaggedTemplateExpression":
2074
+ throw new Error("Tagged templates are not supported in build code");
2075
+ default:
2076
+ throw new Error(`Unsupported expression: ${node.type}`);
2077
+ }
2078
+ }
2079
+ executeBlock(ast.body, globalScope);
2080
+ }
2081
+ var ScriptTimeoutError, BreakSignal, ContinueSignal, ReturnSignal, FORBIDDEN_PROPS, ARRAY_MEMBERS, STRING_MEMBERS, NUMBER_MEMBERS, Scope;
2082
+ var init_build_interpreter = __esm({
2083
+ "../core/dist/tools/build-interpreter.js"() {
2084
+ "use strict";
2085
+ ScriptTimeoutError = class extends Error {
2086
+ constructor(message) {
2087
+ super(message);
2088
+ this.name = "ScriptTimeoutError";
2089
+ }
2090
+ };
2091
+ BreakSignal = class {
2092
+ label;
2093
+ constructor(label) {
2094
+ this.label = label;
2095
+ }
2096
+ };
2097
+ ContinueSignal = class {
2098
+ label;
2099
+ constructor(label) {
2100
+ this.label = label;
2101
+ }
2102
+ };
2103
+ ReturnSignal = class {
2104
+ value;
2105
+ constructor(value) {
2106
+ this.value = value;
2107
+ }
2108
+ };
2109
+ FORBIDDEN_PROPS = /* @__PURE__ */ new Set([
2110
+ "__proto__",
2111
+ "constructor",
2112
+ "prototype",
2113
+ "__defineGetter__",
2114
+ "__defineSetter__",
2115
+ "__lookupGetter__",
2116
+ "__lookupSetter__",
2117
+ "caller",
2118
+ "callee",
2119
+ "arguments",
2120
+ "bind",
2121
+ "call",
2122
+ "apply"
2123
+ ]);
2124
+ ARRAY_MEMBERS = /* @__PURE__ */ new Set([
2125
+ "length",
2126
+ "push",
2127
+ "pop",
2128
+ "shift",
2129
+ "unshift",
2130
+ "slice",
2131
+ "splice",
2132
+ "indexOf",
2133
+ "lastIndexOf",
2134
+ "includes",
2135
+ "join",
2136
+ "map",
2137
+ "filter",
2138
+ "forEach",
2139
+ "reduce",
2140
+ "reduceRight",
2141
+ "concat",
2142
+ "reverse",
2143
+ "sort",
2144
+ "find",
2145
+ "findIndex",
2146
+ "some",
2147
+ "every",
2148
+ "flat",
2149
+ "flatMap",
2150
+ "fill"
2151
+ ]);
2152
+ STRING_MEMBERS = /* @__PURE__ */ new Set([
2153
+ "length",
2154
+ "charAt",
2155
+ "charCodeAt",
2156
+ "codePointAt",
2157
+ "indexOf",
2158
+ "lastIndexOf",
2159
+ "includes",
2160
+ "startsWith",
2161
+ "endsWith",
2162
+ "slice",
2163
+ "substring",
2164
+ "toUpperCase",
2165
+ "toLowerCase",
2166
+ "trim",
2167
+ "trimStart",
2168
+ "trimEnd",
2169
+ "split",
2170
+ "repeat",
2171
+ "padStart",
2172
+ "padEnd",
2173
+ "concat",
2174
+ "at",
2175
+ "replace",
2176
+ "replaceAll"
2177
+ ]);
2178
+ NUMBER_MEMBERS = /* @__PURE__ */ new Set(["toFixed", "toPrecision", "toString"]);
2179
+ Scope = class {
2180
+ parent;
2181
+ isFunctionScope;
2182
+ vars = /* @__PURE__ */ new Map();
2183
+ consts = /* @__PURE__ */ new Set();
2184
+ constructor(parent, isFunctionScope = false) {
2185
+ this.parent = parent;
2186
+ this.isFunctionScope = isFunctionScope;
2187
+ }
2188
+ declare(name, value, kind) {
2189
+ if (kind === "var" && !this.isFunctionScope && this.parent) {
2190
+ this.parent.declare(name, value, kind);
2191
+ return;
2192
+ }
2193
+ this.vars.set(name, value);
2194
+ if (kind === "const")
2195
+ this.consts.add(name);
2196
+ }
2197
+ resolve(name) {
2198
+ if (this.vars.has(name))
2199
+ return this;
2200
+ return this.parent?.resolve(name);
2201
+ }
2202
+ globalScope() {
2203
+ return this.parent ? this.parent.globalScope() : this;
2204
+ }
2205
+ has(name) {
2206
+ return this.resolve(name) !== void 0;
2207
+ }
2208
+ get(name) {
2209
+ const s = this.resolve(name);
2210
+ if (!s)
2211
+ throw new Error(`${name} is not defined`);
2212
+ return s.vars.get(name);
2213
+ }
2214
+ set(name, value) {
2215
+ const s = this.resolve(name);
2216
+ if (!s) {
2217
+ this.globalScope().vars.set(name, value);
2218
+ return;
2219
+ }
2220
+ if (s.consts.has(name))
2221
+ throw new Error(`Assignment to constant variable "${name}"`);
2222
+ s.vars.set(name, value);
2223
+ }
2224
+ };
2225
+ }
2226
+ });
2227
+
1278
2228
  // ../core/dist/tools/build-executor.js
1279
- import * as vm from "vm";
1280
2229
  function createSeededRng(seed) {
1281
2230
  let s = seed;
1282
2231
  return () => {
@@ -1626,6 +2575,7 @@ function runBuildExecutor(code, palette, seed, options) {
1626
2575
  ]);
1627
2576
  }
1628
2577
  const rng = createSeededRng(seed ?? 42);
2578
+ const safeMath = Object.freeze(Object.create(null, Object.getOwnPropertyDescriptors(Math)));
1629
2579
  const sandbox = {
1630
2580
  part: partFn,
1631
2581
  rpart: rpartFn,
@@ -1642,25 +2592,21 @@ function runBuildExecutor(code, palette, seed, options) {
1642
2592
  column: columnFn,
1643
2593
  pew: pewFn,
1644
2594
  fence: fenceFn,
1645
- Math,
2595
+ Math: safeMath,
1646
2596
  GRID_SIZE: 1,
1647
2597
  rng,
1648
- console: { log: () => {
2598
+ console: Object.freeze({ log: () => {
1649
2599
  }, warn: () => {
1650
2600
  }, error: () => {
1651
- } }
2601
+ } })
1652
2602
  };
1653
- const context = vm.createContext(sandbox, {
1654
- codeGeneration: { strings: false, wasm: false }
1655
- });
1656
- const script = new vm.Script(code, { filename: "build-generator.js" });
1657
2603
  try {
1658
- script.runInContext(context, { timeout });
1659
- } catch (err) {
1660
- if (err.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
2604
+ runRestrictedScript(code, sandbox, { timeoutMs: timeout });
2605
+ } catch (err2) {
2606
+ if (err2 instanceof ScriptTimeoutError) {
1661
2607
  throw new Error(`Build code execution timed out after ${timeout}ms`);
1662
2608
  }
1663
- throw new Error(`Build code execution error: ${err.message}`);
2609
+ throw new Error(`Build code execution error: ${err2?.message ?? String(err2)}`);
1664
2610
  }
1665
2611
  if (parts.length === 0) {
1666
2612
  throw new Error("Build code produced no parts. Make sure to call part(), wall(), floor(), etc.");
@@ -1672,6 +2618,7 @@ var DEFAULT_TIMEOUT, DEFAULT_MAX_PARTS, VALID_SHAPES;
1672
2618
  var init_build_executor = __esm({
1673
2619
  "../core/dist/tools/build-executor.js"() {
1674
2620
  "use strict";
2621
+ init_build_interpreter();
1675
2622
  DEFAULT_TIMEOUT = 1e4;
1676
2623
  DEFAULT_MAX_PARTS = 1e4;
1677
2624
  VALID_SHAPES = /* @__PURE__ */ new Set(["Block", "Wedge", "Cylinder", "Ball", "CornerWedge"]);
@@ -1918,7 +2865,7 @@ var init_opencloud_client = __esm({
1918
2865
  if (result.error) {
1919
2866
  throw new Error(`Asset upload failed: ${result.error.message}`);
1920
2867
  }
1921
- await new Promise((resolve4) => setTimeout(resolve4, intervalMs));
2868
+ await new Promise((resolve5) => setTimeout(resolve5, intervalMs));
1922
2869
  }
1923
2870
  throw new Error(`Asset upload timed out after ${maxAttempts * intervalMs / 1e3}s. Operation ID: ${operationId}`);
1924
2871
  }
@@ -2039,7 +2986,7 @@ function isRecord(value) {
2039
2986
  const record = value;
2040
2987
  return record.version === REGISTRY_VERSION && typeof record.recordId === "string" && typeof record.source === "string" && typeof record.exe === "string" && Array.isArray(record.args) && typeof record.launchedAt === "number" && typeof record.bootId === "string";
2041
2988
  }
2042
- var REGISTRY_VERSION, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, EVENT_RETENTION_DAYS, ManagedInstanceRegistry;
2989
+ var REGISTRY_VERSION, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, EVENT_RETENTION_DAYS, TERMINAL_RECORD_RETENTION_MS, ManagedInstanceRegistry;
2043
2990
  var init_managed_instance_registry = __esm({
2044
2991
  "../core/dist/managed-instance-registry.js"() {
2045
2992
  "use strict";
@@ -2048,6 +2995,7 @@ var init_managed_instance_registry = __esm({
2048
2995
  LOCK_RETRY_MS = 25;
2049
2996
  LOCK_TIMEOUT_MS = 5e3;
2050
2997
  EVENT_RETENTION_DAYS = 2;
2998
+ TERMINAL_RECORD_RETENTION_MS = 24 * 60 * 60 * 1e3;
2051
2999
  ManagedInstanceRegistry = class {
2052
3000
  dir;
2053
3001
  constructor(dir = defaultManagedInstanceRegistryDir()) {
@@ -2075,12 +3023,21 @@ var init_managed_instance_registry = __esm({
2075
3023
  findAnyByInstanceId(instanceId) {
2076
3024
  return this.withLock(() => this.readRecordsUnlocked().find((record) => record.instanceId === instanceId));
2077
3025
  }
3026
+ findAnyByRecordId(recordId, options) {
3027
+ return this.withLock(() => {
3028
+ this.sweepUnlocked(options);
3029
+ return this.readRecordsUnlocked().find((record) => record.recordId === recordId);
3030
+ });
3031
+ }
2078
3032
  listOpen(options) {
2079
3033
  return this.withLock(() => {
2080
3034
  this.sweepUnlocked(options);
2081
3035
  return this.readOpenRecordsUnlocked();
2082
3036
  });
2083
3037
  }
3038
+ listOpenUnchecked() {
3039
+ return this.withLock(() => this.readOpenRecordsUnlocked());
3040
+ }
2084
3041
  markClosed(recordId, closedAt = Date.now()) {
2085
3042
  this.withLock(() => {
2086
3043
  const record = this.readRecordUnlocked(recordId);
@@ -2252,41 +3209,52 @@ var init_managed_instance_registry = __esm({
2252
3209
  }, now);
2253
3210
  continue;
2254
3211
  }
2255
- if (parsed.closedAt !== void 0) {
2256
- fs.rmSync(file, { force: true });
2257
- this.appendEventUnlocked({
2258
- event: "registry_pruned_closed_record",
2259
- recordId: parsed.recordId,
2260
- instanceId: parsed.instanceId,
2261
- source: parsed.source,
2262
- reason: "closed_at_present",
2263
- action: "deleted_record"
2264
- }, now);
3212
+ const terminalAt = parsed.closedAt ?? parsed.exitedAt;
3213
+ if (terminalAt !== void 0) {
3214
+ if (now - terminalAt > TERMINAL_RECORD_RETENTION_MS) {
3215
+ fs.rmSync(file, { force: true });
3216
+ this.appendEventUnlocked({
3217
+ event: "registry_pruned_terminal_record",
3218
+ recordId: parsed.recordId,
3219
+ instanceId: parsed.instanceId,
3220
+ source: parsed.source,
3221
+ reason: "terminal_retention_expired",
3222
+ action: "deleted_record"
3223
+ }, now);
3224
+ }
2265
3225
  continue;
2266
3226
  }
2267
3227
  if (parsed.bootId !== options.currentBootId) {
2268
3228
  this.cleanupRecord(options, parsed);
2269
- fs.rmSync(file, { force: true });
3229
+ parsed.state = parsed.state === "failed" ? "failed" : "exited";
3230
+ parsed.exitedAt = now;
3231
+ parsed.closedAt = now;
3232
+ parsed.failureReason ??= "Studio process belongs to a previous host boot.";
3233
+ this.writeRecordUnlocked(parsed);
2270
3234
  this.appendEventUnlocked({
2271
- event: "registry_pruned_previous_boot",
3235
+ event: "registry_marked_previous_boot_exited",
2272
3236
  recordId: parsed.recordId,
2273
3237
  instanceId: parsed.instanceId,
2274
3238
  source: parsed.source,
2275
3239
  reason: "boot_id_changed",
2276
- action: "deleted_record_and_cleaned_baseplate"
3240
+ action: "marked_exited_and_cleaned_baseplate"
2277
3241
  }, now);
2278
3242
  continue;
2279
3243
  }
2280
3244
  if (options.isProcessRunning && (parsed.nativeProcessId || parsed.spawnPid) && !options.isProcessRunning(parsed)) {
2281
3245
  this.cleanupRecord(options, parsed);
2282
- fs.rmSync(file, { force: true });
3246
+ parsed.state = parsed.state === "failed" ? "failed" : "exited";
3247
+ parsed.exitedAt = now;
3248
+ parsed.closedAt = now;
3249
+ parsed.failureReason ??= parsed.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.";
3250
+ this.writeRecordUnlocked(parsed);
2283
3251
  this.appendEventUnlocked({
2284
- event: "registry_pruned_stale_process",
3252
+ event: "registry_marked_process_exited",
2285
3253
  recordId: parsed.recordId,
2286
3254
  instanceId: parsed.instanceId,
2287
3255
  source: parsed.source,
2288
3256
  reason: "pid_not_running",
2289
- action: "deleted_record_and_cleaned_baseplate"
3257
+ action: "marked_exited_and_cleaned_baseplate"
2290
3258
  }, now);
2291
3259
  }
2292
3260
  }
@@ -2297,7 +3265,7 @@ var init_managed_instance_registry = __esm({
2297
3265
 
2298
3266
  // ../core/dist/studio-instance-manager.js
2299
3267
  import { execFileSync, spawn } from "child_process";
2300
- import { copyFileSync, existsSync, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, rmSync as rmSync2, statSync as statSync2 } from "fs";
3268
+ import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync, rmSync as rmSync2, statSync as statSync2 } from "fs";
2301
3269
  import { randomUUID } from "crypto";
2302
3270
  import * as os2 from "os";
2303
3271
  import * as path2 from "path";
@@ -2312,14 +3280,14 @@ function isWsl() {
2312
3280
  if (process.platform !== "linux")
2313
3281
  return false;
2314
3282
  try {
2315
- return /microsoft|wsl/i.test(readFileSync2("/proc/version", "utf8"));
3283
+ return /microsoft|wsl/i.test(readFileSync3("/proc/version", "utf8"));
2316
3284
  } catch {
2317
3285
  return false;
2318
3286
  }
2319
3287
  }
2320
3288
  function powershell(script) {
2321
3289
  return run("powershell.exe", ["-NoProfile", "-Command", script], {
2322
- cwd: isWsl() && existsSync("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
3290
+ cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
2323
3291
  });
2324
3292
  }
2325
3293
  function windowsLocalAppData() {
@@ -2329,7 +3297,7 @@ function windowsLocalAppData() {
2329
3297
  return void 0;
2330
3298
  try {
2331
3299
  return run("cmd.exe", ["/c", "echo %LOCALAPPDATA%"], {
2332
- cwd: existsSync("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
3300
+ cwd: existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
2333
3301
  });
2334
3302
  } catch {
2335
3303
  return void 0;
@@ -2341,10 +3309,67 @@ function toWslPath(windowsPath) {
2341
3309
  return run("wslpath", ["-u", windowsPath]);
2342
3310
  }
2343
3311
  function toStudioLaunchArg(arg) {
2344
- if (!isWsl() || !path2.isAbsolute(arg) || !existsSync(arg))
3312
+ if (!isWsl() || !path2.isAbsolute(arg) || !existsSync2(arg))
2345
3313
  return arg;
2346
3314
  return run("wslpath", ["-w", arg]);
2347
3315
  }
3316
+ function powershellStringLiteral(value) {
3317
+ return `'${value.replace(/'/g, "''")}'`;
3318
+ }
3319
+ function quoteWindowsCommandLineArg(value) {
3320
+ if (value.length > 0 && !/[\s"]/u.test(value))
3321
+ return value;
3322
+ let quoted = '"';
3323
+ let backslashes = 0;
3324
+ for (const char of value) {
3325
+ if (char === "\\") {
3326
+ backslashes += 1;
3327
+ continue;
3328
+ }
3329
+ if (char === '"') {
3330
+ quoted += "\\".repeat(backslashes * 2 + 1) + '"';
3331
+ backslashes = 0;
3332
+ continue;
3333
+ }
3334
+ quoted += "\\".repeat(backslashes) + char;
3335
+ backslashes = 0;
3336
+ }
3337
+ return `${quoted}${"\\".repeat(backslashes * 2)}"`;
3338
+ }
3339
+ function buildWindowsStudioStartScript(exe, args) {
3340
+ const windowsExe = toStudioLaunchArg(exe);
3341
+ const commandLine = args.map(quoteWindowsCommandLineArg).join(" ");
3342
+ return [
3343
+ "$psi = New-Object System.Diagnostics.ProcessStartInfo",
3344
+ `$psi.FileName = ${powershellStringLiteral(windowsExe)}`,
3345
+ `$psi.Arguments = ${powershellStringLiteral(commandLine)}`,
3346
+ // With UseShellExecute=false, Studio inherits the synchronous
3347
+ // powershell.exe invocation's stdout/stderr pipe handles under WSL. Those
3348
+ // handles keep execFileSync waiting until Studio exits even though
3349
+ // PowerShell already printed the PID. Shell execution prevents that
3350
+ // inheritance while Process.Start still returns the native Studio PID.
3351
+ "$psi.UseShellExecute = $true",
3352
+ "$studio = [System.Diagnostics.Process]::Start($psi)",
3353
+ 'if ($null -eq $studio) { throw "Roblox Studio process did not start." }'
3354
+ ].join("; ");
3355
+ }
3356
+ function spawnWindowsStudioFromWsl(exe, args) {
3357
+ const script = buildWindowsStudioStartScript(exe, args);
3358
+ const output = powershell(`${script}; [PSCustomObject]@{ pid = $studio.Id; started = $studio.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() } | ConvertTo-Json -Compress`);
3359
+ const parsed = JSON.parse(output);
3360
+ const nativePid = Number(parsed.pid);
3361
+ const nativeStartedAt = typeof parsed.started === "string" && /^\d+$/u.test(parsed.started) ? parsed.started : void 0;
3362
+ if (!Number.isSafeInteger(nativePid) || nativePid <= 0) {
3363
+ throw new Error(`Could not determine the Windows Studio process id from: ${nativePid}`);
3364
+ }
3365
+ return {
3366
+ pid: nativePid,
3367
+ nativePid,
3368
+ nativeStartedAt,
3369
+ unref: () => {
3370
+ }
3371
+ };
3372
+ }
2348
3373
  function resolveEntrypointDir() {
2349
3374
  const entrypoint = process.argv[1];
2350
3375
  if (!entrypoint)
@@ -2367,13 +3392,44 @@ function resolveBaseplateTemplatePath() {
2367
3392
  path2.join(process.cwd(), "assets", BASEPLATE_TEMPLATE_NAME)
2368
3393
  ];
2369
3394
  for (const candidate of candidates) {
2370
- if (existsSync(candidate))
3395
+ if (existsSync2(candidate))
2371
3396
  return candidate;
2372
3397
  }
2373
3398
  throw new Error(`Baseplate template not found. Expected ${BASEPLATE_TEMPLATE_NAME} in one of: ${candidates.join(", ")}`);
2374
3399
  }
3400
+ function isProcessAlive(pid) {
3401
+ try {
3402
+ process.kill(pid, 0);
3403
+ return true;
3404
+ } catch (err2) {
3405
+ return err2.code === "EPERM";
3406
+ }
3407
+ }
3408
+ function sweepStaleBaseplateFiles() {
3409
+ let entries;
3410
+ try {
3411
+ entries = readdirSync2(BASEPLATE_TEMP_DIR);
3412
+ } catch {
3413
+ return;
3414
+ }
3415
+ const cutoff = Date.now() - STALE_BASEPLATE_MAX_AGE_MS;
3416
+ for (const entry of entries) {
3417
+ const match = BASEPLATE_TEMP_SWEEP_NAME.exec(entry);
3418
+ if (!match)
3419
+ continue;
3420
+ if (Number(match[1]) !== process.pid && isProcessAlive(Number(match[1])))
3421
+ continue;
3422
+ const file = path2.join(BASEPLATE_TEMP_DIR, entry);
3423
+ try {
3424
+ if (statSync2(file).mtimeMs < cutoff)
3425
+ rmSync2(file, { force: true });
3426
+ } catch {
3427
+ }
3428
+ }
3429
+ }
2375
3430
  function createBaseplatePlaceFile() {
2376
- mkdirSync2(BASEPLATE_TEMP_DIR, { recursive: true });
3431
+ mkdirSync3(BASEPLATE_TEMP_DIR, { recursive: true });
3432
+ sweepStaleBaseplateFiles();
2377
3433
  const file = path2.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
2378
3434
  copyFileSync(resolveBaseplateTemplatePath(), file);
2379
3435
  return file;
@@ -2387,8 +3443,12 @@ function cleanupManagedBaseplateFiles(record) {
2387
3443
  return;
2388
3444
  if (!isGeneratedBaseplatePlaceFile(record.localPlaceFile))
2389
3445
  return;
2390
- rmSync2(record.localPlaceFile, { force: true });
2391
- rmSync2(`${record.localPlaceFile}.lock`, { force: true });
3446
+ for (const file of [record.localPlaceFile, `${record.localPlaceFile}.lock`]) {
3447
+ try {
3448
+ rmSync2(file, { force: true });
3449
+ } catch {
3450
+ }
3451
+ }
2392
3452
  }
2393
3453
  function prepareStudioLaunchOptions(options) {
2394
3454
  if (options.source !== "baseplate" || options.localPlaceFile)
@@ -2409,10 +3469,10 @@ function resolveStudioExe() {
2409
3469
  }
2410
3470
  const localAppData = windowsLocalAppData();
2411
3471
  const root = localAppData ? path2.join(toWslPath(localAppData), "Roblox", "Versions") : path2.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
2412
- if (!existsSync(root)) {
3472
+ if (!existsSync2(root)) {
2413
3473
  throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
2414
3474
  }
2415
- const candidates = readdirSync2(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync(candidate)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
3475
+ const candidates = readdirSync2(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
2416
3476
  if (candidates.length === 0) {
2417
3477
  throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
2418
3478
  }
@@ -2435,7 +3495,7 @@ function listStudioProcesses() {
2435
3495
  return [];
2436
3496
  let out = "";
2437
3497
  try {
2438
- out = powershell("Get-Process RobloxStudioBeta -ErrorAction SilentlyContinue | Select-Object Id,Name,Path,MainWindowTitle | ConvertTo-Json -Compress");
3498
+ out = powershell("Get-Process RobloxStudioBeta -ErrorAction SilentlyContinue | ForEach-Object { [PSCustomObject]@{ Id = $_.Id; Name = $_.Name; Path = $_.Path; MainWindowTitle = $_.MainWindowTitle; StartTimeUtcFileTime = $_.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() } } | ConvertTo-Json -Compress");
2439
3499
  } catch {
2440
3500
  return [];
2441
3501
  }
@@ -2447,7 +3507,7 @@ function listStudioProcesses() {
2447
3507
  function currentBootId() {
2448
3508
  if (process.platform === "linux") {
2449
3509
  try {
2450
- return readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
3510
+ return readFileSync3("/proc/sys/kernel/random/boot_id", "utf8").trim();
2451
3511
  } catch {
2452
3512
  }
2453
3513
  }
@@ -2499,12 +3559,12 @@ function buildStudioLaunchArgs(options) {
2499
3559
  }
2500
3560
  }
2501
3561
  function delay(ms) {
2502
- return new Promise((resolve4) => setTimeout(resolve4, ms));
3562
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
2503
3563
  }
2504
3564
  function basenameAny(filePath) {
2505
3565
  return path2.basename(filePath.replace(/\\/g, "/"));
2506
3566
  }
2507
- var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, StudioInstanceManager;
3567
+ var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, STALE_BASEPLATE_MAX_AGE_MS, BASEPLATE_TEMP_SWEEP_NAME, StudioInstanceManager;
2508
3568
  var init_studio_instance_manager = __esm({
2509
3569
  "../core/dist/studio-instance-manager.js"() {
2510
3570
  "use strict";
@@ -2512,9 +3572,13 @@ var init_studio_instance_manager = __esm({
2512
3572
  BASEPLATE_TEMP_DIR = path2.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
2513
3573
  BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
2514
3574
  BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
3575
+ STALE_BASEPLATE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
3576
+ BASEPLATE_TEMP_SWEEP_NAME = /^Baseplate-(\d+)-\d+\.rbxl(\.lock)?$/;
2515
3577
  StudioInstanceManager = class {
2516
3578
  managedByInstanceId = /* @__PURE__ */ new Map();
2517
3579
  pending = /* @__PURE__ */ new Set();
3580
+ monitors = /* @__PURE__ */ new Map();
3581
+ connectionTimers = /* @__PURE__ */ new Map();
2518
3582
  registry;
2519
3583
  processAdapter;
2520
3584
  constructor(options = {}) {
@@ -2523,6 +3587,9 @@ var init_studio_instance_manager = __esm({
2523
3587
  }
2524
3588
  list() {
2525
3589
  this.sweepRegistry();
3590
+ for (const record of [...this.managedByInstanceId.values(), ...this.pending]) {
3591
+ this.refresh(record);
3592
+ }
2526
3593
  const records = [...this.managedByInstanceId.values(), ...this.pending];
2527
3594
  for (const registryRecord of this.registry.listOpen(this.registrySweepOptions())) {
2528
3595
  const record = this.fromRegistryRecord(registryRecord);
@@ -2531,40 +3598,119 @@ var init_studio_instance_manager = __esm({
2531
3598
  }
2532
3599
  records.push(record);
2533
3600
  }
2534
- return records.filter((instance, index, all) => all.indexOf(instance) === index);
3601
+ return records.filter((record) => record.closedAt === void 0).filter((instance, index, all) => all.indexOf(instance) === index);
2535
3602
  }
2536
3603
  get(instanceId) {
2537
3604
  this.sweepRegistry();
2538
3605
  const memoryRecord = this.managedByInstanceId.get(instanceId);
2539
3606
  if (memoryRecord)
2540
- return memoryRecord;
2541
- const registryRecord = this.registry.findOpenByInstanceId(instanceId, this.registrySweepOptions());
2542
- return registryRecord ? this.fromRegistryRecord(registryRecord) : void 0;
3607
+ return this.refresh(memoryRecord);
3608
+ const registryRecord = this.registry.findAnyByInstanceId(instanceId);
3609
+ return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord)) : void 0;
3610
+ }
3611
+ getByLaunchId(launchId) {
3612
+ this.sweepRegistry();
3613
+ const memoryRecord = [...this.managedByInstanceId.values(), ...this.pending].find((record) => record.recordId === launchId);
3614
+ if (memoryRecord)
3615
+ return this.refresh(memoryRecord);
3616
+ const registryRecord = this.registry.findAnyByRecordId(launchId, this.registrySweepOptions());
3617
+ return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord)) : void 0;
3618
+ }
3619
+ pendingLaunches() {
3620
+ const now = Date.now();
3621
+ const records = [...this.pending];
3622
+ for (const registryRecord of this.registry.listOpenUnchecked()) {
3623
+ if (registryRecord.bootId !== this.getCurrentBootId())
3624
+ continue;
3625
+ if (records.some((record) => record.recordId === registryRecord.recordId))
3626
+ continue;
3627
+ records.push(this.fromRegistryRecord(registryRecord));
3628
+ }
3629
+ return records.filter((record) => record.instanceId === void 0).filter((record) => record.state === "launching").filter((record) => record.connectionDeadlineAt === void 0 || record.connectionDeadlineAt > now);
2543
3630
  }
2544
3631
  attachInstanceId(record, instanceId) {
3632
+ this.refresh(record);
3633
+ if (record.closedAt !== void 0 || record.state === "failed" || record.state === "exited")
3634
+ return;
3635
+ if (record.instanceId && record.instanceId !== instanceId)
3636
+ return;
2545
3637
  record.instanceId = instanceId;
3638
+ record.state = "connected";
3639
+ record.connectedAt = record.connectedAt ?? Date.now();
3640
+ this.clearConnectionTimer(record);
2546
3641
  this.pending.delete(record);
2547
3642
  this.managedByInstanceId.set(instanceId, record);
2548
- if (record.recordId) {
2549
- this.registry.attachInstanceId(record.recordId, instanceId);
3643
+ this.persist(record);
3644
+ }
3645
+ markFailed(record, reason) {
3646
+ this.refresh(record);
3647
+ if (record.closedAt !== void 0 || record.state !== "launching")
3648
+ return record;
3649
+ record.state = "failed";
3650
+ record.failedAt = Date.now();
3651
+ record.failureReason = reason;
3652
+ this.clearConnectionTimer(record);
3653
+ this.persist(record);
3654
+ return record;
3655
+ }
3656
+ refresh(record) {
3657
+ if (record.closedAt !== void 0)
3658
+ return record;
3659
+ const processId = record.nativeProcessId ?? record.spawnPid;
3660
+ const studioProcess = processId ? this.findProcessById(processId) : void 0;
3661
+ if (processId && (!studioProcess || !this.verifyProcessForRecord(record, studioProcess))) {
3662
+ return this.markProcessExited(record, void 0, studioProcess ? "Studio process identity changed; the retained PID was not reused." : record.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.");
3663
+ }
3664
+ if (record.state === "launching" && record.connectionDeadlineAt !== void 0 && Date.now() >= record.connectionDeadlineAt) {
3665
+ record.state = "failed";
3666
+ record.failedAt = Date.now();
3667
+ record.failureReason = "Studio launched, but the MCP plugin did not connect before timeout.";
3668
+ this.clearConnectionTimer(record);
3669
+ this.persist(record);
2550
3670
  }
3671
+ return record;
2551
3672
  }
2552
3673
  async launch(options) {
2553
3674
  this.sweepRegistry();
2554
3675
  const preparedOptions = prepareStudioLaunchOptions(options);
3676
+ const bootId = this.getCurrentBootId();
2555
3677
  const before = new Set(this.listStudioProcesses().map((proc2) => proc2.Id));
2556
3678
  const exe = this.processAdapter.resolveStudioExe?.() ?? resolveStudioExe();
2557
3679
  const args = buildStudioLaunchArgs(preparedOptions).map(toStudioLaunchArg);
2558
3680
  const spawnOptions = {
2559
- cwd: isWsl() && existsSync("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
3681
+ cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
2560
3682
  detached: true,
2561
3683
  stdio: "ignore"
2562
3684
  };
2563
- const proc = this.processAdapter.spawnStudio ? this.processAdapter.spawnStudio(exe, args, spawnOptions) : spawn(exe, args, spawnOptions);
2564
- proc.unref();
3685
+ let proc;
3686
+ try {
3687
+ if (this.processAdapter.spawnStudio) {
3688
+ proc = this.processAdapter.spawnStudio(exe, args, spawnOptions);
3689
+ } else if (isWsl()) {
3690
+ proc = spawnWindowsStudioFromWsl(exe, args);
3691
+ } else {
3692
+ const child = spawn(exe, args, spawnOptions);
3693
+ proc = {
3694
+ pid: child.pid,
3695
+ nativePid: child.pid,
3696
+ unref: () => child.unref(),
3697
+ onExit: (listener) => {
3698
+ child.once("exit", listener);
3699
+ },
3700
+ onError: (listener) => {
3701
+ child.once("error", listener);
3702
+ }
3703
+ };
3704
+ }
3705
+ } catch (error) {
3706
+ cleanupManagedBaseplateFiles({ source: preparedOptions.source, localPlaceFile: preparedOptions.localPlaceFile });
3707
+ throw error;
3708
+ }
2565
3709
  const record = {
2566
3710
  recordId: randomUUID(),
2567
3711
  source: options.source,
3712
+ nativeProcessId: proc.nativePid,
3713
+ nativeProcessStartedAt: proc.nativeStartedAt,
2568
3714
  spawnPid: proc.pid,
2569
3715
  exe,
2570
3716
  args,
@@ -2573,29 +3719,74 @@ var init_studio_instance_manager = __esm({
2573
3719
  placeVersion: preparedOptions.placeVersion,
2574
3720
  localPlaceFile: preparedOptions.localPlaceFile,
2575
3721
  launchedAt: Date.now(),
3722
+ connectionDeadlineAt: Date.now() + (options.connectionTimeoutMs ?? 12e4),
3723
+ state: "launching",
2576
3724
  ownerPid: process.pid,
2577
- bootId: this.getCurrentBootId(),
3725
+ bootId,
2578
3726
  deleteLocalPlaceFileOnClose: options.source === "baseplate"
2579
3727
  };
2580
3728
  this.pending.add(record);
2581
- this.registry.upsert(this.toRegistryRecord(record));
3729
+ try {
3730
+ this.persist(record);
3731
+ } catch (error) {
3732
+ this.pending.delete(record);
3733
+ const processId = record.nativeProcessId ?? record.spawnPid;
3734
+ let stopError;
3735
+ if (processId) {
3736
+ try {
3737
+ this.closeProcess(processId);
3738
+ } catch (caught) {
3739
+ stopError = caught;
3740
+ }
3741
+ }
3742
+ cleanupManagedBaseplateFiles(record);
3743
+ const detail = error instanceof Error ? error.message : String(error);
3744
+ const cleanupDetail = stopError ? ` The newly launched process could not be stopped: ${stopError instanceof Error ? stopError.message : String(stopError)}` : "";
3745
+ throw new Error(`Studio launched, but its managed-instance record could not be persisted: ${detail}.${cleanupDetail}`);
3746
+ }
3747
+ proc.unref();
3748
+ proc.onExit?.((code, signal) => {
3749
+ this.markProcessExited(record, code ?? void 0, signal ? `Studio process exited from signal ${signal}.` : record.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.");
3750
+ });
3751
+ proc.onError?.((error) => {
3752
+ this.markFailed(record, `Studio process failed to start: ${error.message}`);
3753
+ });
2582
3754
  const deadline = Date.now() + 5e3;
2583
3755
  while (Date.now() < deadline && record.nativeProcessId === void 0) {
2584
3756
  const created = this.listStudioProcesses().find((candidate) => !before.has(candidate.Id));
2585
3757
  if (created) {
2586
3758
  record.nativeProcessId = created.Id;
2587
- this.registry.upsert(this.toRegistryRecord(record));
3759
+ record.nativeProcessStartedAt = created.StartTimeUtcFileTime;
3760
+ this.persist(record);
2588
3761
  break;
2589
3762
  }
2590
3763
  await delay(250);
2591
3764
  }
2592
3765
  if (record.nativeProcessId === void 0 && process.platform !== "win32" && !isWsl()) {
2593
3766
  record.nativeProcessId = proc.pid;
2594
- this.registry.upsert(this.toRegistryRecord(record));
3767
+ this.persist(record);
3768
+ }
3769
+ if (record.nativeProcessId !== void 0 && record.nativeProcessStartedAt === void 0) {
3770
+ const nativeProcess = this.findProcessById(record.nativeProcessId);
3771
+ if (nativeProcess?.StartTimeUtcFileTime !== void 0) {
3772
+ record.nativeProcessStartedAt = nativeProcess.StartTimeUtcFileTime;
3773
+ this.persist(record);
3774
+ }
2595
3775
  }
3776
+ this.startMonitor(record);
2596
3777
  return record;
2597
3778
  }
3779
+ closeByLaunchId(launchId) {
3780
+ const record = this.getByLaunchId(launchId);
3781
+ if (!record)
3782
+ return { status: "not_found", launchId };
3783
+ if (record.closedAt !== void 0) {
3784
+ return { status: "already_closed", launchId, instanceId: record.instanceId };
3785
+ }
3786
+ return this.close(record);
3787
+ }
2598
3788
  closeByInstanceId(instanceId) {
3789
+ this.sweepRegistry();
2599
3790
  const memoryRecord = this.managedByInstanceId.get(instanceId);
2600
3791
  if (memoryRecord)
2601
3792
  return this.close(memoryRecord);
@@ -2606,33 +3797,24 @@ var init_studio_instance_manager = __esm({
2606
3797
  }
2607
3798
  if (registryRecord.closedAt !== void 0) {
2608
3799
  this.cleanupManagedRecord(registryRecord);
2609
- this.registry.delete(registryRecord.recordId);
2610
3800
  this.registry.logEvent({
2611
3801
  event: "registry_close_already_stopped",
2612
3802
  recordId: registryRecord.recordId,
2613
3803
  instanceId: registryRecord.instanceId,
2614
3804
  source: registryRecord.source,
2615
3805
  reason: "closed_at_present",
2616
- action: "deleted_record_and_cleaned_baseplate"
2617
- });
2618
- return { status: "already_closed", instanceId };
2619
- }
2620
- if (registryRecord.bootId !== this.getCurrentBootId()) {
2621
- this.cleanupManagedRecord(registryRecord);
2622
- this.registry.delete(registryRecord.recordId);
2623
- this.registry.logEvent({
2624
- event: "registry_pruned_previous_boot",
2625
- recordId: registryRecord.recordId,
2626
- instanceId: registryRecord.instanceId,
2627
- source: registryRecord.source,
2628
- reason: "boot_id_changed",
2629
- action: "deleted_record_and_cleaned_baseplate"
3806
+ action: "retained_terminal_record_and_cleaned_baseplate"
2630
3807
  });
2631
- return { status: "already_closed", instanceId };
3808
+ return { status: "already_closed", launchId: registryRecord.recordId, instanceId };
2632
3809
  }
2633
3810
  return this.close(this.fromRegistryRecord(registryRecord));
2634
3811
  }
2635
3812
  close(record) {
3813
+ this.stopMonitor(record);
3814
+ this.refresh(record);
3815
+ if (record.closedAt !== void 0) {
3816
+ return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
3817
+ }
2636
3818
  const processId = record.nativeProcessId ?? record.spawnPid;
2637
3819
  if (!processId) {
2638
3820
  throw new Error(`Cannot close ${record.instanceId ?? "Studio launch"} because its process id was not detected.`);
@@ -2640,8 +3822,7 @@ var init_studio_instance_manager = __esm({
2640
3822
  const studioProcess = this.findProcessById(processId);
2641
3823
  if (!studioProcess) {
2642
3824
  this.cleanupManagedRecord(record);
2643
- this.markClosedInMemory(record);
2644
- this.markClosedInRegistry(record);
3825
+ this.markProcessExited(record, void 0, record.failureReason);
2645
3826
  this.registry.logEvent({
2646
3827
  event: "registry_close_already_stopped",
2647
3828
  recordId: record.recordId,
@@ -2650,7 +3831,7 @@ var init_studio_instance_manager = __esm({
2650
3831
  reason: "pid_not_running",
2651
3832
  action: "marked_closed_and_cleaned_baseplate"
2652
3833
  });
2653
- return { status: "already_closed", instanceId: record.instanceId };
3834
+ return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
2654
3835
  }
2655
3836
  if (!this.verifyProcessForRecord(record, studioProcess)) {
2656
3837
  this.registry.logEvent({
@@ -2676,15 +3857,18 @@ var init_studio_instance_manager = __esm({
2676
3857
  action: "marked_closed_and_cleaned_baseplate"
2677
3858
  });
2678
3859
  this.cleanupManagedRecord(record);
2679
- this.markClosedInMemory(record);
2680
- this.markClosedInRegistry(record);
2681
- return { status: "already_closed", instanceId: record.instanceId };
2682
- }
2683
- record.closedAt = Date.now();
3860
+ this.markProcessExited(record, void 0, record.failureReason);
3861
+ return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
3862
+ }
3863
+ const closedAt = Date.now();
3864
+ record.closedAt = closedAt;
3865
+ record.exitedAt = record.exitedAt ?? closedAt;
3866
+ if (record.state !== "failed")
3867
+ record.state = "exited";
2684
3868
  this.cleanupManagedRecord(record);
2685
3869
  this.markClosedInMemory(record);
2686
- this.markClosedInRegistry(record);
2687
- return { status: "closed", instanceId: record.instanceId };
3870
+ this.persist(record);
3871
+ return { status: "closed", launchId: record.recordId, instanceId: record.instanceId };
2688
3872
  }
2689
3873
  closeConnectedInstance(instance) {
2690
3874
  const process2 = this.findProcessForConnectedInstance(instance);
@@ -2759,6 +3943,9 @@ var init_studio_instance_manager = __esm({
2759
3943
  const processName = `${studioProcess.Name ?? ""} ${studioProcess.Path ?? ""}`.toLowerCase();
2760
3944
  if (!processName.includes("robloxstudio"))
2761
3945
  return false;
3946
+ if (record.nativeProcessStartedAt !== void 0 && studioProcess.StartTimeUtcFileTime !== record.nativeProcessStartedAt) {
3947
+ return false;
3948
+ }
2762
3949
  const processId = record.nativeProcessId ?? record.spawnPid;
2763
3950
  if (record.spawnPid && record.spawnPid === processId && studioProcess.Id === processId)
2764
3951
  return true;
@@ -2784,10 +3971,64 @@ var init_studio_instance_manager = __esm({
2784
3971
  if (record.instanceId)
2785
3972
  this.managedByInstanceId.delete(record.instanceId);
2786
3973
  this.pending.delete(record);
3974
+ this.stopMonitor(record);
3975
+ }
3976
+ markProcessExited(record, exitCode, reason) {
3977
+ if (record.closedAt !== void 0)
3978
+ return record;
3979
+ const exitedAt = Date.now();
3980
+ record.exitedAt = exitedAt;
3981
+ record.closedAt = exitedAt;
3982
+ if (record.state !== "failed")
3983
+ record.state = "exited";
3984
+ if (exitCode !== void 0)
3985
+ record.exitCode = exitCode;
3986
+ if (reason)
3987
+ record.failureReason = reason;
3988
+ this.cleanupManagedRecord(record);
3989
+ this.markClosedInMemory(record);
3990
+ this.persist(record);
3991
+ return record;
3992
+ }
3993
+ startMonitor(record) {
3994
+ if (!record.recordId || record.closedAt !== void 0 || this.monitors.has(record.recordId))
3995
+ return;
3996
+ if (record.state === "launching" && record.connectionDeadlineAt !== void 0) {
3997
+ const timeout = setTimeout(() => {
3998
+ this.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
3999
+ }, Math.max(0, record.connectionDeadlineAt - Date.now()));
4000
+ if (typeof timeout === "object" && "unref" in timeout)
4001
+ timeout.unref();
4002
+ this.connectionTimers.set(record.recordId, timeout);
4003
+ }
4004
+ const timer = setInterval(() => {
4005
+ this.refresh(record);
4006
+ if (record.closedAt !== void 0)
4007
+ this.stopMonitor(record);
4008
+ }, 5e3);
4009
+ if (typeof timer === "object" && "unref" in timer)
4010
+ timer.unref();
4011
+ this.monitors.set(record.recordId, timer);
2787
4012
  }
2788
- markClosedInRegistry(record) {
2789
- if (record.recordId)
2790
- this.registry.markClosed(record.recordId, record.closedAt ?? Date.now());
4013
+ stopMonitor(record) {
4014
+ if (!record.recordId)
4015
+ return;
4016
+ const timer = this.monitors.get(record.recordId);
4017
+ if (timer)
4018
+ clearInterval(timer);
4019
+ this.monitors.delete(record.recordId);
4020
+ this.clearConnectionTimer(record);
4021
+ }
4022
+ clearConnectionTimer(record) {
4023
+ if (!record.recordId)
4024
+ return;
4025
+ const timer = this.connectionTimers.get(record.recordId);
4026
+ if (timer)
4027
+ clearTimeout(timer);
4028
+ this.connectionTimers.delete(record.recordId);
4029
+ }
4030
+ persist(record) {
4031
+ this.registry.upsert(this.toRegistryRecord(record));
2791
4032
  }
2792
4033
  toRegistryRecord(record) {
2793
4034
  if (!record.recordId)
@@ -2800,6 +4041,7 @@ var init_studio_instance_manager = __esm({
2800
4041
  instanceId: record.instanceId,
2801
4042
  source: record.source,
2802
4043
  nativeProcessId: record.nativeProcessId,
4044
+ nativeProcessStartedAt: record.nativeProcessStartedAt,
2803
4045
  spawnPid: record.spawnPid,
2804
4046
  exe: record.exe,
2805
4047
  args: record.args,
@@ -2809,18 +4051,26 @@ var init_studio_instance_manager = __esm({
2809
4051
  localPlaceFile: record.localPlaceFile,
2810
4052
  deleteLocalPlaceFileOnClose: record.deleteLocalPlaceFileOnClose,
2811
4053
  launchedAt: record.launchedAt,
2812
- attachedAt: record.instanceId ? Date.now() : void 0,
4054
+ attachedAt: record.connectedAt,
4055
+ connectionDeadlineAt: record.connectionDeadlineAt,
4056
+ state: record.state,
4057
+ failedAt: record.failedAt,
4058
+ exitedAt: record.exitedAt,
4059
+ exitCode: record.exitCode,
4060
+ failureReason: record.failureReason,
2813
4061
  closedAt: record.closedAt,
2814
4062
  ownerPid: record.ownerPid,
2815
4063
  bootId: record.bootId
2816
4064
  };
2817
4065
  }
2818
4066
  fromRegistryRecord(record) {
4067
+ const state = record.state ?? (record.closedAt !== void 0 ? "exited" : record.instanceId ? "connected" : "launching");
2819
4068
  return {
2820
4069
  recordId: record.recordId,
2821
4070
  source: record.source,
2822
4071
  instanceId: record.instanceId,
2823
4072
  nativeProcessId: record.nativeProcessId,
4073
+ nativeProcessStartedAt: record.nativeProcessStartedAt,
2824
4074
  spawnPid: record.spawnPid,
2825
4075
  exe: record.exe,
2826
4076
  args: record.args,
@@ -2829,6 +4079,13 @@ var init_studio_instance_manager = __esm({
2829
4079
  placeVersion: record.placeVersion,
2830
4080
  localPlaceFile: record.localPlaceFile,
2831
4081
  launchedAt: record.launchedAt,
4082
+ connectionDeadlineAt: record.connectionDeadlineAt ?? (state === "launching" ? record.launchedAt + 12e4 : void 0),
4083
+ state,
4084
+ connectedAt: record.attachedAt,
4085
+ failedAt: record.failedAt,
4086
+ exitedAt: record.exitedAt,
4087
+ exitCode: record.exitCode,
4088
+ failureReason: record.failureReason,
2832
4089
  closedAt: record.closedAt,
2833
4090
  ownerPid: record.ownerPid,
2834
4091
  bootId: record.bootId,
@@ -3014,6 +4271,982 @@ var init_image_decode = __esm({
3014
4271
  }
3015
4272
  });
3016
4273
 
4274
+ // ../../node_modules/fzstd/esm/index.mjs
4275
+ function decompress(dat, buf) {
4276
+ var bufs = [], nb = +!buf;
4277
+ var bt = 0, ol = 0;
4278
+ for (; dat.length; ) {
4279
+ var st = rzfh(dat, nb || buf);
4280
+ if (typeof st == "object") {
4281
+ if (nb) {
4282
+ buf = null;
4283
+ if (st.w.length == st.u) {
4284
+ bufs.push(buf = st.w);
4285
+ ol += st.u;
4286
+ }
4287
+ } else {
4288
+ bufs.push(buf);
4289
+ st.e = 0;
4290
+ }
4291
+ for (; !st.l; ) {
4292
+ var blk = rzb(dat, st, buf);
4293
+ if (!blk)
4294
+ err(5);
4295
+ if (buf)
4296
+ st.e = st.y;
4297
+ else {
4298
+ bufs.push(blk);
4299
+ ol += blk.length;
4300
+ cpw(st.w, 0, blk.length);
4301
+ st.w.set(blk, st.w.length - blk.length);
4302
+ }
4303
+ }
4304
+ bt = st.b + st.c * 4;
4305
+ } else
4306
+ bt = st;
4307
+ dat = dat.subarray(bt);
4308
+ }
4309
+ return cct(bufs, ol);
4310
+ }
4311
+ var ab, u8, u16, i16, i32, slc, fill, cpw, ec, err, rb, b4, rzfh, msb, rfse, rhu, dllt, dmlt, doct, b2bl, llb, llbl, mlb, mlbl, dhu, dhu4, rzb, cct;
4312
+ var init_esm = __esm({
4313
+ "../../node_modules/fzstd/esm/index.mjs"() {
4314
+ "use strict";
4315
+ ab = ArrayBuffer;
4316
+ u8 = Uint8Array;
4317
+ u16 = Uint16Array;
4318
+ i16 = Int16Array;
4319
+ i32 = Int32Array;
4320
+ slc = function(v, s, e) {
4321
+ if (u8.prototype.slice)
4322
+ return u8.prototype.slice.call(v, s, e);
4323
+ if (s == null || s < 0)
4324
+ s = 0;
4325
+ if (e == null || e > v.length)
4326
+ e = v.length;
4327
+ var n = new u8(e - s);
4328
+ n.set(v.subarray(s, e));
4329
+ return n;
4330
+ };
4331
+ fill = function(v, n, s, e) {
4332
+ if (u8.prototype.fill)
4333
+ return u8.prototype.fill.call(v, n, s, e);
4334
+ if (s == null || s < 0)
4335
+ s = 0;
4336
+ if (e == null || e > v.length)
4337
+ e = v.length;
4338
+ for (; s < e; ++s)
4339
+ v[s] = n;
4340
+ return v;
4341
+ };
4342
+ cpw = function(v, t, s, e) {
4343
+ if (u8.prototype.copyWithin)
4344
+ return u8.prototype.copyWithin.call(v, t, s, e);
4345
+ if (s == null || s < 0)
4346
+ s = 0;
4347
+ if (e == null || e > v.length)
4348
+ e = v.length;
4349
+ while (s < e) {
4350
+ v[t++] = v[s++];
4351
+ }
4352
+ };
4353
+ ec = [
4354
+ "invalid zstd data",
4355
+ "window size too large (>2046MB)",
4356
+ "invalid block type",
4357
+ "FSE accuracy too high",
4358
+ "match distance too far back",
4359
+ "unexpected EOF"
4360
+ ];
4361
+ err = function(ind, msg, nt) {
4362
+ var e = new Error(msg || ec[ind]);
4363
+ e.code = ind;
4364
+ if (Error.captureStackTrace)
4365
+ Error.captureStackTrace(e, err);
4366
+ if (!nt)
4367
+ throw e;
4368
+ return e;
4369
+ };
4370
+ rb = function(d, b, n) {
4371
+ var i = 0, o = 0;
4372
+ for (; i < n; ++i)
4373
+ o |= d[b++] << (i << 3);
4374
+ return o;
4375
+ };
4376
+ b4 = function(d, b) {
4377
+ return (d[b] | d[b + 1] << 8 | d[b + 2] << 16 | d[b + 3] << 24) >>> 0;
4378
+ };
4379
+ rzfh = function(dat, w) {
4380
+ var n3 = dat[0] | dat[1] << 8 | dat[2] << 16;
4381
+ if (n3 == 3126568 && dat[3] == 253) {
4382
+ var flg = dat[4];
4383
+ var ss = flg >> 5 & 1, cc = flg >> 2 & 1, df = flg & 3, fcf = flg >> 6;
4384
+ if (flg & 8)
4385
+ err(0);
4386
+ var bt = 6 - ss;
4387
+ var db = df == 3 ? 4 : df;
4388
+ var di = rb(dat, bt, db);
4389
+ bt += db;
4390
+ var fsb = fcf ? 1 << fcf : ss;
4391
+ var fss = rb(dat, bt, fsb) + (fcf == 1 && 256);
4392
+ var ws = fss;
4393
+ if (!ss) {
4394
+ var wb = 1 << 10 + (dat[5] >> 3);
4395
+ ws = wb + (wb >> 3) * (dat[5] & 7);
4396
+ }
4397
+ if (ws > 2145386496)
4398
+ err(1);
4399
+ var buf = new u8((w == 1 ? fss || ws : w ? 0 : ws) + 12);
4400
+ buf[0] = 1, buf[4] = 4, buf[8] = 8;
4401
+ return {
4402
+ b: bt + fsb,
4403
+ y: 0,
4404
+ l: 0,
4405
+ d: di,
4406
+ w: w && w != 1 ? w : buf.subarray(12),
4407
+ e: ws,
4408
+ o: new i32(buf.buffer, 0, 3),
4409
+ u: fss,
4410
+ c: cc,
4411
+ m: Math.min(131072, ws)
4412
+ };
4413
+ } else if ((n3 >> 4 | dat[3] << 20) == 25481893) {
4414
+ return b4(dat, 4) + 8;
4415
+ }
4416
+ err(0);
4417
+ };
4418
+ msb = function(val) {
4419
+ var bits = 0;
4420
+ for (; 1 << bits <= val; ++bits)
4421
+ ;
4422
+ return bits - 1;
4423
+ };
4424
+ rfse = function(dat, bt, mal) {
4425
+ var tpos = (bt << 3) + 4;
4426
+ var al = (dat[bt] & 15) + 5;
4427
+ if (al > mal)
4428
+ err(3);
4429
+ var sz = 1 << al;
4430
+ var probs = sz, sym = -1, re = -1, i = -1, ht = sz;
4431
+ var buf = new ab(512 + (sz << 2));
4432
+ var freq = new i16(buf, 0, 256);
4433
+ var dstate = new u16(buf, 0, 256);
4434
+ var nstate = new u16(buf, 512, sz);
4435
+ var bb1 = 512 + (sz << 1);
4436
+ var syms = new u8(buf, bb1, sz);
4437
+ var nbits = new u8(buf, bb1 + sz);
4438
+ while (sym < 255 && probs > 0) {
4439
+ var bits = msb(probs + 1);
4440
+ var cbt = tpos >> 3;
4441
+ var msk = (1 << bits + 1) - 1;
4442
+ var val = (dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16) >> (tpos & 7) & msk;
4443
+ var msk1fb = (1 << bits) - 1;
4444
+ var msv = msk - probs - 1;
4445
+ var sval = val & msk1fb;
4446
+ if (sval < msv)
4447
+ tpos += bits, val = sval;
4448
+ else {
4449
+ tpos += bits + 1;
4450
+ if (val > msk1fb)
4451
+ val -= msv;
4452
+ }
4453
+ freq[++sym] = --val;
4454
+ if (val == -1) {
4455
+ probs += val;
4456
+ syms[--ht] = sym;
4457
+ } else
4458
+ probs -= val;
4459
+ if (!val) {
4460
+ do {
4461
+ var rbt = tpos >> 3;
4462
+ re = (dat[rbt] | dat[rbt + 1] << 8) >> (tpos & 7) & 3;
4463
+ tpos += 2;
4464
+ sym += re;
4465
+ } while (re == 3);
4466
+ }
4467
+ }
4468
+ if (sym > 255 || probs)
4469
+ err(0);
4470
+ var sympos = 0;
4471
+ var sstep = (sz >> 1) + (sz >> 3) + 3;
4472
+ var smask = sz - 1;
4473
+ for (var s = 0; s <= sym; ++s) {
4474
+ var sf = freq[s];
4475
+ if (sf < 1) {
4476
+ dstate[s] = -sf;
4477
+ continue;
4478
+ }
4479
+ for (i = 0; i < sf; ++i) {
4480
+ syms[sympos] = s;
4481
+ do {
4482
+ sympos = sympos + sstep & smask;
4483
+ } while (sympos >= ht);
4484
+ }
4485
+ }
4486
+ if (sympos)
4487
+ err(0);
4488
+ for (i = 0; i < sz; ++i) {
4489
+ var ns = dstate[syms[i]]++;
4490
+ var nb = nbits[i] = al - msb(ns);
4491
+ nstate[i] = (ns << nb) - sz;
4492
+ }
4493
+ return [tpos + 7 >> 3, {
4494
+ b: al,
4495
+ s: syms,
4496
+ n: nbits,
4497
+ t: nstate
4498
+ }];
4499
+ };
4500
+ rhu = function(dat, bt) {
4501
+ var i = 0, wc = -1;
4502
+ var buf = new u8(292), hb = dat[bt];
4503
+ var hw = buf.subarray(0, 256);
4504
+ var rc = buf.subarray(256, 268);
4505
+ var ri = new u16(buf.buffer, 268);
4506
+ if (hb < 128) {
4507
+ var _a = rfse(dat, bt + 1, 6), ebt = _a[0], fdt = _a[1];
4508
+ bt += hb;
4509
+ var epos = ebt << 3;
4510
+ var lb = dat[bt];
4511
+ if (!lb)
4512
+ err(0);
4513
+ var st1 = 0, st2 = 0, btr1 = fdt.b, btr2 = btr1;
4514
+ var fpos = (++bt << 3) - 8 + msb(lb);
4515
+ for (; ; ) {
4516
+ fpos -= btr1;
4517
+ if (fpos < epos)
4518
+ break;
4519
+ var cbt = fpos >> 3;
4520
+ st1 += (dat[cbt] | dat[cbt + 1] << 8) >> (fpos & 7) & (1 << btr1) - 1;
4521
+ hw[++wc] = fdt.s[st1];
4522
+ fpos -= btr2;
4523
+ if (fpos < epos)
4524
+ break;
4525
+ cbt = fpos >> 3;
4526
+ st2 += (dat[cbt] | dat[cbt + 1] << 8) >> (fpos & 7) & (1 << btr2) - 1;
4527
+ hw[++wc] = fdt.s[st2];
4528
+ btr1 = fdt.n[st1];
4529
+ st1 = fdt.t[st1];
4530
+ btr2 = fdt.n[st2];
4531
+ st2 = fdt.t[st2];
4532
+ }
4533
+ if (++wc > 255)
4534
+ err(0);
4535
+ } else {
4536
+ wc = hb - 127;
4537
+ for (; i < wc; i += 2) {
4538
+ var byte = dat[++bt];
4539
+ hw[i] = byte >> 4;
4540
+ hw[i + 1] = byte & 15;
4541
+ }
4542
+ ++bt;
4543
+ }
4544
+ var wes = 0;
4545
+ for (i = 0; i < wc; ++i) {
4546
+ var wt = hw[i];
4547
+ if (wt > 11)
4548
+ err(0);
4549
+ wes += wt && 1 << wt - 1;
4550
+ }
4551
+ var mb = msb(wes) + 1;
4552
+ var ts = 1 << mb;
4553
+ var rem = ts - wes;
4554
+ if (rem & rem - 1)
4555
+ err(0);
4556
+ hw[wc++] = msb(rem) + 1;
4557
+ for (i = 0; i < wc; ++i) {
4558
+ var wt = hw[i];
4559
+ ++rc[hw[i] = wt && mb + 1 - wt];
4560
+ }
4561
+ var hbuf = new u8(ts << 1);
4562
+ var syms = hbuf.subarray(0, ts), nb = hbuf.subarray(ts);
4563
+ ri[mb] = 0;
4564
+ for (i = mb; i > 0; --i) {
4565
+ var pv = ri[i];
4566
+ fill(nb, i, pv, ri[i - 1] = pv + rc[i] * (1 << mb - i));
4567
+ }
4568
+ if (ri[0] != ts)
4569
+ err(0);
4570
+ for (i = 0; i < wc; ++i) {
4571
+ var bits = hw[i];
4572
+ if (bits) {
4573
+ var code = ri[bits];
4574
+ fill(syms, i, code, ri[bits] = code + (1 << mb - bits));
4575
+ }
4576
+ }
4577
+ return [bt, {
4578
+ n: nb,
4579
+ b: mb,
4580
+ s: syms
4581
+ }];
4582
+ };
4583
+ dllt = rfse(/* @__PURE__ */ new u8([
4584
+ 81,
4585
+ 16,
4586
+ 99,
4587
+ 140,
4588
+ 49,
4589
+ 198,
4590
+ 24,
4591
+ 99,
4592
+ 12,
4593
+ 33,
4594
+ 196,
4595
+ 24,
4596
+ 99,
4597
+ 102,
4598
+ 102,
4599
+ 134,
4600
+ 70,
4601
+ 146,
4602
+ 4
4603
+ ]), 0, 6)[1];
4604
+ dmlt = rfse(/* @__PURE__ */ new u8([
4605
+ 33,
4606
+ 20,
4607
+ 196,
4608
+ 24,
4609
+ 99,
4610
+ 140,
4611
+ 33,
4612
+ 132,
4613
+ 16,
4614
+ 66,
4615
+ 8,
4616
+ 33,
4617
+ 132,
4618
+ 16,
4619
+ 66,
4620
+ 8,
4621
+ 33,
4622
+ 68,
4623
+ 68,
4624
+ 68,
4625
+ 68,
4626
+ 68,
4627
+ 68,
4628
+ 68,
4629
+ 68,
4630
+ 36,
4631
+ 9
4632
+ ]), 0, 6)[1];
4633
+ doct = rfse(/* @__PURE__ */ new u8([
4634
+ 32,
4635
+ 132,
4636
+ 16,
4637
+ 66,
4638
+ 102,
4639
+ 70,
4640
+ 68,
4641
+ 68,
4642
+ 68,
4643
+ 68,
4644
+ 36,
4645
+ 73,
4646
+ 2
4647
+ ]), 0, 5)[1];
4648
+ b2bl = function(b, s) {
4649
+ var len = b.length, bl = new i32(len);
4650
+ for (var i = 0; i < len; ++i) {
4651
+ bl[i] = s;
4652
+ s += 1 << b[i];
4653
+ }
4654
+ return bl;
4655
+ };
4656
+ llb = /* @__PURE__ */ new u8((/* @__PURE__ */ new i32([
4657
+ 0,
4658
+ 0,
4659
+ 0,
4660
+ 0,
4661
+ 16843009,
4662
+ 50528770,
4663
+ 134678020,
4664
+ 202050057,
4665
+ 269422093
4666
+ ])).buffer, 0, 36);
4667
+ llbl = /* @__PURE__ */ b2bl(llb, 0);
4668
+ mlb = /* @__PURE__ */ new u8((/* @__PURE__ */ new i32([
4669
+ 0,
4670
+ 0,
4671
+ 0,
4672
+ 0,
4673
+ 0,
4674
+ 0,
4675
+ 0,
4676
+ 0,
4677
+ 16843009,
4678
+ 50528770,
4679
+ 117769220,
4680
+ 185207048,
4681
+ 252579084,
4682
+ 16
4683
+ ])).buffer, 0, 53);
4684
+ mlbl = /* @__PURE__ */ b2bl(mlb, 3);
4685
+ dhu = function(dat, out, hu) {
4686
+ var len = dat.length, ss = out.length, lb = dat[len - 1], msk = (1 << hu.b) - 1, eb = -hu.b;
4687
+ if (!lb)
4688
+ err(0);
4689
+ var st = 0, btr = hu.b, pos = (len << 3) - 8 + msb(lb) - btr, i = -1;
4690
+ for (; pos > eb && i < ss; ) {
4691
+ var cbt = pos >> 3;
4692
+ var val = (dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16) >> (pos & 7);
4693
+ st = (st << btr | val) & msk;
4694
+ out[++i] = hu.s[st];
4695
+ pos -= btr = hu.n[st];
4696
+ }
4697
+ if (pos != eb || i + 1 != ss)
4698
+ err(0);
4699
+ };
4700
+ dhu4 = function(dat, out, hu) {
4701
+ var bt = 6;
4702
+ var ss = out.length, sz1 = ss + 3 >> 2, sz2 = sz1 << 1, sz3 = sz1 + sz2;
4703
+ dhu(dat.subarray(bt, bt += dat[0] | dat[1] << 8), out.subarray(0, sz1), hu);
4704
+ dhu(dat.subarray(bt, bt += dat[2] | dat[3] << 8), out.subarray(sz1, sz2), hu);
4705
+ dhu(dat.subarray(bt, bt += dat[4] | dat[5] << 8), out.subarray(sz2, sz3), hu);
4706
+ dhu(dat.subarray(bt), out.subarray(sz3), hu);
4707
+ };
4708
+ rzb = function(dat, st, out) {
4709
+ var _a;
4710
+ var bt = st.b;
4711
+ var b0 = dat[bt], btype = b0 >> 1 & 3;
4712
+ st.l = b0 & 1;
4713
+ var sz = b0 >> 3 | dat[bt + 1] << 5 | dat[bt + 2] << 13;
4714
+ var ebt = (bt += 3) + sz;
4715
+ if (btype == 1) {
4716
+ if (bt >= dat.length)
4717
+ return;
4718
+ st.b = bt + 1;
4719
+ if (out) {
4720
+ fill(out, dat[bt], st.y, st.y += sz);
4721
+ return out;
4722
+ }
4723
+ return fill(new u8(sz), dat[bt]);
4724
+ }
4725
+ if (ebt > dat.length)
4726
+ return;
4727
+ if (btype == 0) {
4728
+ st.b = ebt;
4729
+ if (out) {
4730
+ out.set(dat.subarray(bt, ebt), st.y);
4731
+ st.y += sz;
4732
+ return out;
4733
+ }
4734
+ return slc(dat, bt, ebt);
4735
+ }
4736
+ if (btype == 2) {
4737
+ var b3 = dat[bt], lbt = b3 & 3, sf = b3 >> 2 & 3;
4738
+ var lss = b3 >> 4, lcs = 0, s4 = 0;
4739
+ if (lbt < 2) {
4740
+ if (sf & 1)
4741
+ lss |= dat[++bt] << 4 | (sf & 2 && dat[++bt] << 12);
4742
+ else
4743
+ lss = b3 >> 3;
4744
+ } else {
4745
+ s4 = sf;
4746
+ if (sf < 2)
4747
+ lss |= (dat[++bt] & 63) << 4, lcs = dat[bt] >> 6 | dat[++bt] << 2;
4748
+ else if (sf == 2)
4749
+ lss |= dat[++bt] << 4 | (dat[++bt] & 3) << 12, lcs = dat[bt] >> 2 | dat[++bt] << 6;
4750
+ else
4751
+ lss |= dat[++bt] << 4 | (dat[++bt] & 63) << 12, lcs = dat[bt] >> 6 | dat[++bt] << 2 | dat[++bt] << 10;
4752
+ }
4753
+ ++bt;
4754
+ var buf = out ? out.subarray(st.y, st.y + st.m) : new u8(st.m);
4755
+ var spl = buf.length - lss;
4756
+ if (lbt == 0)
4757
+ buf.set(dat.subarray(bt, bt += lss), spl);
4758
+ else if (lbt == 1)
4759
+ fill(buf, dat[bt++], spl);
4760
+ else {
4761
+ var hu = st.h;
4762
+ if (lbt == 2) {
4763
+ var hud = rhu(dat, bt);
4764
+ lcs += bt - (bt = hud[0]);
4765
+ st.h = hu = hud[1];
4766
+ } else if (!hu)
4767
+ err(0);
4768
+ (s4 ? dhu4 : dhu)(dat.subarray(bt, bt += lcs), buf.subarray(spl), hu);
4769
+ }
4770
+ var ns = dat[bt++];
4771
+ if (ns) {
4772
+ if (ns == 255)
4773
+ ns = (dat[bt++] | dat[bt++] << 8) + 32512;
4774
+ else if (ns > 127)
4775
+ ns = ns - 128 << 8 | dat[bt++];
4776
+ var scm = dat[bt++];
4777
+ if (scm & 3)
4778
+ err(0);
4779
+ var dts = [dmlt, doct, dllt];
4780
+ for (var i = 2; i > -1; --i) {
4781
+ var md = scm >> (i << 1) + 2 & 3;
4782
+ if (md == 1) {
4783
+ var rbuf = new u8([0, 0, dat[bt++]]);
4784
+ dts[i] = {
4785
+ s: rbuf.subarray(2, 3),
4786
+ n: rbuf.subarray(0, 1),
4787
+ t: new u16(rbuf.buffer, 0, 1),
4788
+ b: 0
4789
+ };
4790
+ } else if (md == 2) {
4791
+ _a = rfse(dat, bt, 9 - (i & 1)), bt = _a[0], dts[i] = _a[1];
4792
+ } else if (md == 3) {
4793
+ if (!st.t)
4794
+ err(0);
4795
+ dts[i] = st.t[i];
4796
+ }
4797
+ }
4798
+ var _b = st.t = dts, mlt = _b[0], oct = _b[1], llt = _b[2];
4799
+ var lb = dat[ebt - 1];
4800
+ if (!lb)
4801
+ err(0);
4802
+ var spos = (ebt << 3) - 8 + msb(lb) - llt.b, cbt = spos >> 3, oubt = 0;
4803
+ var lst = (dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << llt.b) - 1;
4804
+ cbt = (spos -= oct.b) >> 3;
4805
+ var ost = (dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << oct.b) - 1;
4806
+ cbt = (spos -= mlt.b) >> 3;
4807
+ var mst = (dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << mlt.b) - 1;
4808
+ for (++ns; --ns; ) {
4809
+ var llc = llt.s[lst];
4810
+ var lbtr = llt.n[lst];
4811
+ var mlc = mlt.s[mst];
4812
+ var mbtr = mlt.n[mst];
4813
+ var ofc = oct.s[ost];
4814
+ var obtr = oct.n[ost];
4815
+ cbt = (spos -= ofc) >> 3;
4816
+ var ofp = 1 << ofc;
4817
+ var off = ofp + ((dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16 | dat[cbt + 3] << 24) >>> (spos & 7) & ofp - 1);
4818
+ cbt = (spos -= mlb[mlc]) >> 3;
4819
+ var ml = mlbl[mlc] + ((dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16) >> (spos & 7) & (1 << mlb[mlc]) - 1);
4820
+ cbt = (spos -= llb[llc]) >> 3;
4821
+ var ll = llbl[llc] + ((dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16) >> (spos & 7) & (1 << llb[llc]) - 1);
4822
+ cbt = (spos -= lbtr) >> 3;
4823
+ lst = llt.t[lst] + ((dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << lbtr) - 1);
4824
+ cbt = (spos -= mbtr) >> 3;
4825
+ mst = mlt.t[mst] + ((dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << mbtr) - 1);
4826
+ cbt = (spos -= obtr) >> 3;
4827
+ ost = oct.t[ost] + ((dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << obtr) - 1);
4828
+ if (off > 3) {
4829
+ st.o[2] = st.o[1];
4830
+ st.o[1] = st.o[0];
4831
+ st.o[0] = off -= 3;
4832
+ } else {
4833
+ var idx = off - (ll != 0);
4834
+ if (idx) {
4835
+ off = idx == 3 ? st.o[0] - 1 : st.o[idx];
4836
+ if (idx > 1)
4837
+ st.o[2] = st.o[1];
4838
+ st.o[1] = st.o[0];
4839
+ st.o[0] = off;
4840
+ } else
4841
+ off = st.o[0];
4842
+ }
4843
+ for (var i = 0; i < ll; ++i) {
4844
+ buf[oubt + i] = buf[spl + i];
4845
+ }
4846
+ oubt += ll, spl += ll;
4847
+ var stin = oubt - off;
4848
+ if (stin < 0) {
4849
+ var len = -stin;
4850
+ var bs = st.e + stin;
4851
+ if (len > ml)
4852
+ len = ml;
4853
+ for (var i = 0; i < len; ++i) {
4854
+ buf[oubt + i] = st.w[bs + i];
4855
+ }
4856
+ oubt += len, ml -= len, stin = 0;
4857
+ }
4858
+ for (var i = 0; i < ml; ++i) {
4859
+ buf[oubt + i] = buf[stin + i];
4860
+ }
4861
+ oubt += ml;
4862
+ }
4863
+ if (oubt != spl) {
4864
+ while (spl < buf.length) {
4865
+ buf[oubt++] = buf[spl++];
4866
+ }
4867
+ } else
4868
+ oubt = buf.length;
4869
+ if (out)
4870
+ st.y += oubt;
4871
+ else
4872
+ buf = slc(buf, 0, oubt);
4873
+ } else if (out) {
4874
+ st.y += lss;
4875
+ if (spl) {
4876
+ for (var i = 0; i < lss; ++i) {
4877
+ buf[i] = buf[spl + i];
4878
+ }
4879
+ }
4880
+ } else if (spl)
4881
+ buf = slc(buf, spl);
4882
+ st.b = ebt;
4883
+ return buf;
4884
+ }
4885
+ err(2);
4886
+ };
4887
+ cct = function(bufs, ol) {
4888
+ if (bufs.length == 1)
4889
+ return bufs[0];
4890
+ var buf = new u8(ol);
4891
+ for (var i = 0, b = 0; i < bufs.length; ++i) {
4892
+ var chk = bufs[i];
4893
+ buf.set(chk, b);
4894
+ b += chk.length;
4895
+ }
4896
+ return buf;
4897
+ };
4898
+ }
4899
+ });
4900
+
4901
+ // ../core/dist/studio-skills.js
4902
+ import { createHash as createHash2 } from "crypto";
4903
+ import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
4904
+ import * as path4 from "path";
4905
+ function decompressLz4Block(input, outputLength) {
4906
+ const output = Buffer.allocUnsafe(outputLength);
4907
+ let inputOffset = 0;
4908
+ let outputOffset = 0;
4909
+ const readExtendedLength = (initial) => {
4910
+ let length = initial;
4911
+ if (initial !== 15)
4912
+ return length;
4913
+ let next = 255;
4914
+ while (next === 255) {
4915
+ if (inputOffset >= input.length) {
4916
+ throw new Error("Invalid LZ4 chunk: truncated extended length");
4917
+ }
4918
+ next = input[inputOffset++];
4919
+ length += next;
4920
+ }
4921
+ return length;
4922
+ };
4923
+ while (inputOffset < input.length) {
4924
+ const token = input[inputOffset++];
4925
+ const literalLength = readExtendedLength(token >>> 4);
4926
+ if (inputOffset + literalLength > input.length || outputOffset + literalLength > output.length) {
4927
+ throw new Error("Invalid LZ4 chunk: literal exceeds chunk boundary");
4928
+ }
4929
+ input.copy(output, outputOffset, inputOffset, inputOffset + literalLength);
4930
+ inputOffset += literalLength;
4931
+ outputOffset += literalLength;
4932
+ if (inputOffset === input.length)
4933
+ break;
4934
+ if (inputOffset + 2 > input.length) {
4935
+ throw new Error("Invalid LZ4 chunk: missing match offset");
4936
+ }
4937
+ const matchOffset = input.readUInt16LE(inputOffset);
4938
+ inputOffset += 2;
4939
+ if (matchOffset === 0 || matchOffset > outputOffset) {
4940
+ throw new Error("Invalid LZ4 chunk: invalid match offset");
4941
+ }
4942
+ const matchLength = readExtendedLength(token & 15) + 4;
4943
+ if (outputOffset + matchLength > output.length) {
4944
+ throw new Error("Invalid LZ4 chunk: match exceeds output boundary");
4945
+ }
4946
+ for (let index = 0; index < matchLength; index += 1) {
4947
+ output[outputOffset] = output[outputOffset - matchOffset];
4948
+ outputOffset += 1;
4949
+ }
4950
+ }
4951
+ if (outputOffset !== output.length) {
4952
+ throw new Error(`Invalid LZ4 chunk: produced ${outputOffset} bytes, expected ${output.length}`);
4953
+ }
4954
+ return output;
4955
+ }
4956
+ function decompressChunk(compressed, outputLength) {
4957
+ if (compressed.subarray(0, ZSTD_MAGIC.length).equals(ZSTD_MAGIC)) {
4958
+ const decompressed = Buffer.from(decompress(compressed));
4959
+ if (decompressed.length !== outputLength) {
4960
+ throw new Error(`Invalid Zstandard chunk: produced ${decompressed.length} bytes, expected ${outputLength}`);
4961
+ }
4962
+ return decompressed;
4963
+ }
4964
+ return decompressLz4Block(compressed, outputLength);
4965
+ }
4966
+ function readChunk(reader) {
4967
+ if (reader.remaining < RBXM_CHUNK_HEADER_BYTES) {
4968
+ throw new Error("Invalid Assistant.rbxm: truncated chunk header");
4969
+ }
4970
+ const type = reader.readBytes(4).toString("latin1");
4971
+ const compressedLength = reader.readUint32();
4972
+ const uncompressedLength = reader.readUint32();
4973
+ reader.skip(4);
4974
+ if (type === "END\0")
4975
+ return { type };
4976
+ if (compressedLength === 0) {
4977
+ return { type, content: reader.readBytes(uncompressedLength) };
4978
+ }
4979
+ return {
4980
+ type,
4981
+ content: decompressChunk(reader.readBytes(compressedLength), uncompressedLength)
4982
+ };
4983
+ }
4984
+ function parseFrontmatter(markdown) {
4985
+ const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
4986
+ if (!match)
4987
+ return void 0;
4988
+ const fields = /* @__PURE__ */ new Map();
4989
+ for (const line of match[1].split(/\r?\n/)) {
4990
+ const separator = line.indexOf(":");
4991
+ if (separator <= 0)
4992
+ continue;
4993
+ const key = line.slice(0, separator).trim().toLowerCase();
4994
+ let value = line.slice(separator + 1).trim();
4995
+ if (value.startsWith('"') && value.endsWith('"')) {
4996
+ try {
4997
+ value = JSON.parse(value);
4998
+ } catch {
4999
+ }
5000
+ } else if (value.startsWith("'") && value.endsWith("'")) {
5001
+ value = value.slice(1, -1).replace(/''/g, "'");
5002
+ }
5003
+ fields.set(key, value);
5004
+ }
5005
+ const name = fields.get("name")?.trim();
5006
+ if (!name)
5007
+ return void 0;
5008
+ return { name, description: fields.get("description")?.trim() ?? "" };
5009
+ }
5010
+ function canonicalBuiltInSkillName(sourceName) {
5011
+ return sourceName.toLowerCase().startsWith("rbx-") ? sourceName : `rbx-${sourceName}`;
5012
+ }
5013
+ function parseBuiltInStudioSkills(buffer) {
5014
+ if (buffer.length < RBXM_HEADER_BYTES || !buffer.subarray(0, RBXM_MAGIC.length).equals(RBXM_MAGIC)) {
5015
+ throw new Error("Invalid Assistant.rbxm: Roblox binary model header not found");
5016
+ }
5017
+ const reader = new ByteReader(buffer, "Assistant.rbxm");
5018
+ reader.skip(RBXM_MAGIC.length);
5019
+ reader.readUint16();
5020
+ reader.readInt32();
5021
+ reader.readInt32();
5022
+ reader.skip(8);
5023
+ const stringValueClasses = /* @__PURE__ */ new Map();
5024
+ let foundEnd = false;
5025
+ while (reader.remaining > 0) {
5026
+ const chunk = readChunk(reader);
5027
+ if (chunk.type === "END\0") {
5028
+ foundEnd = true;
5029
+ break;
5030
+ }
5031
+ if (!chunk.content)
5032
+ continue;
5033
+ const chunkReader = new ByteReader(chunk.content, `${chunk.type} chunk`);
5034
+ if (chunk.type === "INST") {
5035
+ const classId2 = chunkReader.readUint32();
5036
+ const className = chunkReader.readString();
5037
+ chunkReader.readUint8();
5038
+ const count = chunkReader.readUint32();
5039
+ chunkReader.skip(count * 4);
5040
+ if (className === "StringValue")
5041
+ stringValueClasses.set(classId2, { count });
5042
+ continue;
5043
+ }
5044
+ if (chunk.type !== "PROP")
5045
+ continue;
5046
+ const classId = chunkReader.readUint32();
5047
+ const propertyName = chunkReader.readString();
5048
+ const propertyType = chunkReader.readUint8();
5049
+ const classInfo = stringValueClasses.get(classId);
5050
+ if (!classInfo || propertyType !== STRING_PROPERTY_TYPE)
5051
+ continue;
5052
+ if (propertyName !== "Name" && propertyName !== "Value")
5053
+ continue;
5054
+ const values = [];
5055
+ for (let index = 0; index < classInfo.count; index += 1) {
5056
+ values.push(chunkReader.readString());
5057
+ }
5058
+ if (propertyName === "Name")
5059
+ classInfo.names = values;
5060
+ else
5061
+ classInfo.values = values;
5062
+ }
5063
+ if (!foundEnd)
5064
+ throw new Error("Invalid Assistant.rbxm: END chunk not found");
5065
+ const documents = [];
5066
+ for (const classInfo of stringValueClasses.values()) {
5067
+ if (!classInfo.names || !classInfo.values)
5068
+ continue;
5069
+ for (let index = 0; index < classInfo.count; index += 1) {
5070
+ const document = classInfo.names[index];
5071
+ if (document !== "SKILL" && document !== "SKILL-combined")
5072
+ continue;
5073
+ const content = classInfo.values[index];
5074
+ const frontmatter = parseFrontmatter(content);
5075
+ if (!frontmatter)
5076
+ continue;
5077
+ documents.push({
5078
+ document,
5079
+ sourceName: frontmatter.name,
5080
+ description: frontmatter.description,
5081
+ content
5082
+ });
5083
+ }
5084
+ }
5085
+ const grouped = /* @__PURE__ */ new Map();
5086
+ for (const document of documents) {
5087
+ const key = document.sourceName.toLowerCase();
5088
+ const group = grouped.get(key) ?? {};
5089
+ if (document.document === "SKILL-combined")
5090
+ group.combined = document;
5091
+ else
5092
+ group.base = document;
5093
+ grouped.set(key, group);
5094
+ }
5095
+ return [...grouped.values()].map((group) => {
5096
+ const selected = group.combined ?? group.base;
5097
+ return {
5098
+ name: canonicalBuiltInSkillName(selected.sourceName),
5099
+ sourceName: selected.sourceName,
5100
+ description: selected.description,
5101
+ document: selected.document,
5102
+ hasCombinedDocument: group.combined !== void 0,
5103
+ content: selected.content,
5104
+ contentLength: Buffer.byteLength(selected.content, "utf8"),
5105
+ contentSha256: createHash2("sha256").update(selected.content).digest("hex")
5106
+ };
5107
+ }).sort((left, right) => left.name.localeCompare(right.name));
5108
+ }
5109
+ function discoverNamedFile(root, fileName, depth = 0) {
5110
+ if (depth > MAX_DISCOVERY_DEPTH || !existsSync4(root))
5111
+ return [];
5112
+ const matches = [];
5113
+ let entries;
5114
+ try {
5115
+ entries = readdirSync3(root, { withFileTypes: true });
5116
+ } catch {
5117
+ return matches;
5118
+ }
5119
+ for (const entry of entries) {
5120
+ const entryPath = path4.join(root, entry.name);
5121
+ if (entry.isFile() && entry.name.toLowerCase() === fileName.toLowerCase()) {
5122
+ matches.push(entryPath);
5123
+ } else if (entry.isDirectory()) {
5124
+ matches.push(...discoverNamedFile(entryPath, fileName, depth + 1));
5125
+ }
5126
+ }
5127
+ return matches;
5128
+ }
5129
+ function resolveAssistantBundlePath(studioExe = resolveStudioExe()) {
5130
+ const override = process.env.ROBLOX_STUDIO_ASSISTANT_BUNDLE;
5131
+ if (override) {
5132
+ if (!existsSync4(override)) {
5133
+ throw new Error(`ROBLOX_STUDIO_ASSISTANT_BUNDLE does not exist: ${override}`);
5134
+ }
5135
+ return override;
5136
+ }
5137
+ const exeDirectory = path4.dirname(studioExe);
5138
+ const roots = [
5139
+ path4.join(exeDirectory, "BuiltInStandalonePlugins"),
5140
+ path4.resolve(exeDirectory, "..", "Resources", "BuiltInStandalonePlugins"),
5141
+ path4.resolve(exeDirectory, "..", "..", "OTAPlugins")
5142
+ ];
5143
+ const direct = path4.join(exeDirectory, "BuiltInStandalonePlugins", "Optimized_Embedded_Signature", "Assistant.rbxm");
5144
+ const candidates = /* @__PURE__ */ new Set();
5145
+ if (existsSync4(direct))
5146
+ candidates.add(direct);
5147
+ for (const root of roots) {
5148
+ for (const candidate of discoverNamedFile(root, "Assistant.rbxm"))
5149
+ candidates.add(candidate);
5150
+ }
5151
+ const newest = [...candidates].map((candidate) => ({ candidate, modifiedAt: statSync3(candidate).mtimeMs })).sort((left, right) => right.modifiedAt - left.modifiedAt)[0]?.candidate;
5152
+ if (!newest) {
5153
+ throw new Error(`Studio Assistant bundle not found for ${studioExe}. Set ROBLOX_STUDIO_ASSISTANT_BUNDLE to the installed Assistant.rbxm path.`);
5154
+ }
5155
+ return newest;
5156
+ }
5157
+ function loadBuiltInStudioSkills(bundlePath = resolveAssistantBundlePath()) {
5158
+ const stats = statSync3(bundlePath);
5159
+ if (cachedBundle?.path === bundlePath && cachedBundle.modifiedAt === stats.mtimeMs && cachedBundle.size === stats.size) {
5160
+ return cachedBundle.value;
5161
+ }
5162
+ const buffer = readFileSync5(bundlePath);
5163
+ const skills = parseBuiltInStudioSkills(buffer);
5164
+ if (skills.length === 0) {
5165
+ throw new Error(`No built-in skill documents found in ${bundlePath}`);
5166
+ }
5167
+ const studioVersion = bundlePath.split(path4.sep).find((segment) => /^version-[a-z0-9]+$/i.test(segment));
5168
+ const value = {
5169
+ bundlePath,
5170
+ bundleModifiedAt: stats.mtime.toISOString(),
5171
+ bundleSha256: createHash2("sha256").update(buffer).digest("hex"),
5172
+ studioVersion,
5173
+ skills
5174
+ };
5175
+ cachedBundle = { path: bundlePath, modifiedAt: stats.mtimeMs, size: stats.size, value };
5176
+ return value;
5177
+ }
5178
+ function findBuiltInStudioSkill(bundle, requestedName) {
5179
+ const wanted = requestedName.trim().toLowerCase();
5180
+ return bundle.skills.find((skill) => skill.name.toLowerCase() === wanted || skill.sourceName.toLowerCase() === wanted);
5181
+ }
5182
+ var RBXM_MAGIC, RBXM_HEADER_BYTES, RBXM_CHUNK_HEADER_BYTES, ZSTD_MAGIC, STRING_PROPERTY_TYPE, MAX_DISCOVERY_DEPTH, ByteReader, cachedBundle;
5183
+ var init_studio_skills = __esm({
5184
+ "../core/dist/studio-skills.js"() {
5185
+ "use strict";
5186
+ init_esm();
5187
+ init_studio_instance_manager();
5188
+ RBXM_MAGIC = Buffer.from("<roblox!\x89\xFF\r\n\n", "latin1");
5189
+ RBXM_HEADER_BYTES = 32;
5190
+ RBXM_CHUNK_HEADER_BYTES = 16;
5191
+ ZSTD_MAGIC = Buffer.from([40, 181, 47, 253]);
5192
+ STRING_PROPERTY_TYPE = 1;
5193
+ MAX_DISCOVERY_DEPTH = 6;
5194
+ ByteReader = class {
5195
+ bytes;
5196
+ label;
5197
+ offset = 0;
5198
+ constructor(bytes, label) {
5199
+ this.bytes = bytes;
5200
+ this.label = label;
5201
+ }
5202
+ get remaining() {
5203
+ return this.bytes.length - this.offset;
5204
+ }
5205
+ requireBytes(length) {
5206
+ if (!Number.isSafeInteger(length) || length < 0 || this.remaining < length) {
5207
+ throw new Error(`Invalid ${this.label}: requested ${length} bytes with ${this.remaining} remaining`);
5208
+ }
5209
+ }
5210
+ readUint8() {
5211
+ this.requireBytes(1);
5212
+ return this.bytes[this.offset++];
5213
+ }
5214
+ readUint16() {
5215
+ this.requireBytes(2);
5216
+ const value = this.bytes.readUInt16LE(this.offset);
5217
+ this.offset += 2;
5218
+ return value;
5219
+ }
5220
+ readUint32() {
5221
+ this.requireBytes(4);
5222
+ const value = this.bytes.readUInt32LE(this.offset);
5223
+ this.offset += 4;
5224
+ return value;
5225
+ }
5226
+ readInt32() {
5227
+ this.requireBytes(4);
5228
+ const value = this.bytes.readInt32LE(this.offset);
5229
+ this.offset += 4;
5230
+ return value;
5231
+ }
5232
+ readBytes(length) {
5233
+ this.requireBytes(length);
5234
+ const value = this.bytes.subarray(this.offset, this.offset + length);
5235
+ this.offset += length;
5236
+ return value;
5237
+ }
5238
+ skip(length) {
5239
+ this.requireBytes(length);
5240
+ this.offset += length;
5241
+ }
5242
+ readString() {
5243
+ const length = this.readUint32();
5244
+ return this.readBytes(length).toString("utf8");
5245
+ }
5246
+ };
5247
+ }
5248
+ });
5249
+
3017
5250
  // ../core/dist/jpeg-encoder.js
3018
5251
  function rgbaToJpeg(rgba, width, height, quality = 80) {
3019
5252
  if (width <= 0 || height <= 0)
@@ -4033,7 +6266,7 @@ var init_png_encoder = __esm({
4033
6266
  // ../core/dist/tools/index.js
4034
6267
  import * as fs3 from "fs";
4035
6268
  import * as os3 from "os";
4036
- import * as path4 from "path";
6269
+ import * as path5 from "path";
4037
6270
  function multiplayerStopDisabledBody() {
4038
6271
  return {
4039
6272
  success: false,
@@ -4058,7 +6291,7 @@ function encodeImageFromRgbaResponse(response, format, quality) {
4058
6291
  };
4059
6292
  }
4060
6293
  function sleep(ms) {
4061
- return new Promise((resolve4) => setTimeout(resolve4, ms));
6294
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
4062
6295
  }
4063
6296
  function errorMessage(error) {
4064
6297
  return error instanceof Error ? error.message : String(error);
@@ -4120,7 +6353,7 @@ function loadMicroProfilerBaseline(source, sourcePath) {
4120
6353
  if (typeof sourcePath !== "string" || sourcePath === "") {
4121
6354
  throw new Error("baseline_path must be a non-empty string when provided");
4122
6355
  }
4123
- const resolved = path4.resolve(sourcePath);
6356
+ const resolved = path5.resolve(sourcePath);
4124
6357
  const parsed = JSON.parse(fs3.readFileSync(resolved, "utf8"));
4125
6358
  const record = asRecord(parsed);
4126
6359
  if (!record)
@@ -4581,6 +6814,7 @@ var init_tools = __esm({
4581
6814
  init_studio_instance_manager();
4582
6815
  init_image_decode();
4583
6816
  init_roblox_docs();
6817
+ init_studio_skills();
4584
6818
  init_jpeg_encoder();
4585
6819
  init_png_encoder();
4586
6820
  MAX_INLINE_IMAGE_BYTES = 6e6;
@@ -4648,10 +6882,52 @@ var init_tools = __esm({
4648
6882
  this.openCloudClient = new OpenCloudClient();
4649
6883
  this.cookieClient = new RobloxCookieClient();
4650
6884
  this.instanceManager = new StudioInstanceManager();
6885
+ this.bridge.onInstanceRegistered((instance) => this._associateManagedEditConnection(instance));
4651
6886
  }
4652
6887
  _textResult(body) {
4653
6888
  return { content: [{ type: "text", text: JSON.stringify(body) }] };
4654
6889
  }
6890
+ async getRobloxSkills(action, name) {
6891
+ if (action !== "list" && action !== "get") {
6892
+ throw new Error('get_roblox_skills action must be "list" or "get"');
6893
+ }
6894
+ const bundle = loadBuiltInStudioSkills();
6895
+ const bundleMetadata = {
6896
+ source: "installed-studio-assistant",
6897
+ studioVersion: bundle.studioVersion,
6898
+ bundlePath: bundle.bundlePath,
6899
+ bundleModifiedAt: bundle.bundleModifiedAt,
6900
+ bundleSha256: bundle.bundleSha256
6901
+ };
6902
+ if (action === "list") {
6903
+ return this._textResult({
6904
+ action,
6905
+ ...bundleMetadata,
6906
+ count: bundle.skills.length,
6907
+ skills: bundle.skills.map((skill2) => ({
6908
+ name: skill2.name,
6909
+ sourceName: skill2.sourceName,
6910
+ description: skill2.description,
6911
+ document: skill2.document,
6912
+ hasCombinedDocument: skill2.hasCombinedDocument,
6913
+ contentLength: skill2.contentLength,
6914
+ contentSha256: skill2.contentSha256
6915
+ }))
6916
+ });
6917
+ }
6918
+ if (!name || typeof name !== "string" || !name.trim()) {
6919
+ throw new Error('get_roblox_skills action="get" requires a skill name from action="list"');
6920
+ }
6921
+ const skill = findBuiltInStudioSkill(bundle, name);
6922
+ if (!skill) {
6923
+ throw new Error(`Built-in Studio skill "${name}" was not found. Available skills: ` + bundle.skills.map((candidate) => candidate.name).join(", "));
6924
+ }
6925
+ return this._textResult({
6926
+ action,
6927
+ ...bundleMetadata,
6928
+ skill
6929
+ });
6930
+ }
4655
6931
  async getRobloxDocs(name, docType, section) {
4656
6932
  if (!name || typeof name !== "string") {
4657
6933
  throw new Error('get_roblox_docs requires a name (e.g. "ProximityPrompt")');
@@ -5004,8 +7280,8 @@ var init_tools = __esm({
5004
7280
  timedOut: true
5005
7281
  };
5006
7282
  }
5007
- async getFileTree(path5 = "", instance_id) {
5008
- const response = await this._callSingle("/api/file-tree", { path: path5 }, void 0, instance_id);
7283
+ async getFileTree(path6 = "", instance_id) {
7284
+ const response = await this._callSingle("/api/file-tree", { path: path6 }, void 0, instance_id);
5009
7285
  return {
5010
7286
  content: [
5011
7287
  {
@@ -5122,9 +7398,9 @@ var init_tools = __esm({
5122
7398
  ]
5123
7399
  };
5124
7400
  }
5125
- async getProjectStructure(path5, maxDepth, scriptsOnly, instance_id) {
7401
+ async getProjectStructure(path6, maxDepth, scriptsOnly, instance_id) {
5126
7402
  const response = await this._callSingle("/api/project-structure", {
5127
- path: path5,
7403
+ path: path6,
5128
7404
  maxDepth,
5129
7405
  scriptsOnly
5130
7406
  }, void 0, instance_id);
@@ -6048,8 +8324,8 @@ ${code}`
6048
8324
  const rawJson = mutable.raw_json;
6049
8325
  if (typeof rawJson === "string") {
6050
8326
  if (typeof outputPath === "string" && outputPath !== "") {
6051
- const resolvedOutputPath = path4.resolve(outputPath);
6052
- fs3.mkdirSync(path4.dirname(resolvedOutputPath), { recursive: true });
8327
+ const resolvedOutputPath = path5.resolve(outputPath);
8328
+ fs3.mkdirSync(path5.dirname(resolvedOutputPath), { recursive: true });
6053
8329
  fs3.writeFileSync(resolvedOutputPath, rawJson, "utf8");
6054
8330
  mutable.output_path = resolvedOutputPath;
6055
8331
  }
@@ -6111,8 +8387,8 @@ ${code}`
6111
8387
  const rawSnapshotBase64 = mutable.raw_snapshot_base64;
6112
8388
  if (typeof rawSnapshotBase64 === "string") {
6113
8389
  if (typeof outputPath === "string" && outputPath !== "") {
6114
- const resolvedOutputPath = path4.resolve(outputPath);
6115
- fs3.mkdirSync(path4.dirname(resolvedOutputPath), { recursive: true });
8390
+ const resolvedOutputPath = path5.resolve(outputPath);
8391
+ fs3.mkdirSync(path5.dirname(resolvedOutputPath), { recursive: true });
6116
8392
  fs3.writeFileSync(resolvedOutputPath, Buffer.from(rawSnapshotBase64, "base64"));
6117
8393
  mutable.output_path = resolvedOutputPath;
6118
8394
  }
@@ -6127,8 +8403,8 @@ ${code}`
6127
8403
  });
6128
8404
  }
6129
8405
  if (typeof summaryOutputPath === "string" && summaryOutputPath !== "") {
6130
- const resolvedSummaryPath = path4.resolve(summaryOutputPath);
6131
- fs3.mkdirSync(path4.dirname(resolvedSummaryPath), { recursive: true });
8406
+ const resolvedSummaryPath = path5.resolve(summaryOutputPath);
8407
+ fs3.mkdirSync(path5.dirname(resolvedSummaryPath), { recursive: true });
6132
8408
  fs3.writeFileSync(resolvedSummaryPath, JSON.stringify(mutable, null, 2), "utf8");
6133
8409
  mutable.summary_output_path = resolvedSummaryPath;
6134
8410
  }
@@ -6196,11 +8472,18 @@ ${code}`
6196
8472
  return record.placeId !== void 0 && instance.placeId === record.placeId;
6197
8473
  }
6198
8474
  if ((record.source === "baseplate" || record.source === "local_file") && record.localPlaceFile) {
6199
- const expectedName = path4.basename(record.localPlaceFile);
8475
+ const expectedName = path5.basename(record.localPlaceFile);
6200
8476
  return instance.placeName === expectedName || instance.dataModelName === expectedName;
6201
8477
  }
6202
8478
  return true;
6203
8479
  }
8480
+ _associateManagedEditConnection(instance) {
8481
+ if (instance.role !== "edit")
8482
+ return;
8483
+ const candidate = this.instanceManager.pendingLaunches().filter((record) => instance.connectedAt >= record.launchedAt - 1e3).filter((record) => this._matchesManagedLaunch(record, instance)).sort((a, b) => a.launchedAt - b.launchedAt)[0];
8484
+ if (candidate)
8485
+ this.instanceManager.attachInstanceId(candidate, instance.instanceId);
8486
+ }
6204
8487
  async _deriveUniverseId(placeId) {
6205
8488
  const response = await fetch(`https://apis.roblox.com/universes/v1/places/${placeId}/universe`);
6206
8489
  if (!response.ok) {
@@ -6216,6 +8499,10 @@ ${code}`
6216
8499
  async _waitForManagedEditConnection(record, beforeKeys, timeoutMs) {
6217
8500
  const deadline = Date.now() + timeoutMs;
6218
8501
  while (Date.now() < deadline) {
8502
+ this.instanceManager.refresh(record);
8503
+ if (record.state === "failed" || record.state === "exited" || record.closedAt !== void 0) {
8504
+ return void 0;
8505
+ }
6219
8506
  const candidates = this.bridge.getPublicInstances().filter((instance) => instance.role === "edit").filter((instance) => !beforeKeys.has(this._publicInstanceKey(instance))).filter((instance) => instance.connectedAt >= record.launchedAt - 1e3).filter((instance) => this._matchesManagedLaunch(record, instance)).sort((a, b) => b.connectedAt - a.connectedAt);
6220
8507
  if (candidates[0])
6221
8508
  return candidates[0];
@@ -6224,12 +8511,25 @@ ${code}`
6224
8511
  return void 0;
6225
8512
  }
6226
8513
  _managedStatus(record) {
8514
+ this.instanceManager.refresh(record);
6227
8515
  const connected = record.instanceId ? this.bridge.getPublicInstances().filter((instance) => instance.instanceId === record.instanceId) : [];
6228
8516
  return {
8517
+ launch_id: record.recordId,
6229
8518
  instance_id: record.instanceId,
8519
+ managed: true,
8520
+ state: record.state,
8521
+ pid: record.nativeProcessId ?? record.spawnPid,
8522
+ process_running: record.closedAt === void 0 && record.exitedAt === void 0,
6230
8523
  source: record.source,
8524
+ local_place_file: record.localPlaceFile,
6231
8525
  place_id: record.placeId,
6232
8526
  place_version: record.placeVersion,
8527
+ launched_at: new Date(record.launchedAt).toISOString(),
8528
+ connected_at: record.connectedAt ? new Date(record.connectedAt).toISOString() : void 0,
8529
+ failed_at: record.failedAt ? new Date(record.failedAt).toISOString() : void 0,
8530
+ exited_at: record.exitedAt ? new Date(record.exitedAt).toISOString() : void 0,
8531
+ exit_code: record.exitCode,
8532
+ failure_reason: record.failureReason,
6233
8533
  connected: connected.length > 0,
6234
8534
  roles: connected.map((instance) => instance.role).sort()
6235
8535
  };
@@ -6241,6 +8541,10 @@ ${code}`
6241
8541
  async manageInstance(request) {
6242
8542
  const action = request.action;
6243
8543
  const instance_id = typeof request.instance_id === "string" ? request.instance_id : void 0;
8544
+ const launch_id = typeof request.launch_id === "string" ? request.launch_id : void 0;
8545
+ if (instance_id && launch_id) {
8546
+ throw new Error("manage_instance accepts only one of instance_id or launch_id.");
8547
+ }
6244
8548
  if (action !== "launch" && action !== "close" && action !== "status" && action !== "list_place_versions") {
6245
8549
  throw new Error("manage_instance requires action=launch|close|status|list_place_versions");
6246
8550
  }
@@ -6268,18 +8572,26 @@ ${code}`
6268
8572
  return this._textResult(body);
6269
8573
  }
6270
8574
  if (action === "status") {
8575
+ if (launch_id) {
8576
+ const record2 = this.instanceManager.getByLaunchId(launch_id);
8577
+ if (!record2)
8578
+ return this._textResult({ error: "Launch is not managed.", launch_id });
8579
+ return this._textResult(this._managedStatus(record2));
8580
+ }
6271
8581
  if (instance_id) {
6272
8582
  const record2 = this.instanceManager.get(instance_id);
6273
8583
  const connected2 = this.bridge.getPublicInstances().filter((instance) => instance.instanceId === instance_id);
6274
8584
  if (!record2 && connected2.length === 0) {
6275
8585
  return this._textResult({ error: "Instance is not connected or managed.", instance_id });
6276
8586
  }
8587
+ if (record2)
8588
+ return this._textResult(this._managedStatus(record2));
6277
8589
  return this._textResult({
6278
8590
  instance_id,
6279
- managed: !!record2,
6280
- source: record2?.source,
6281
- place_id: record2?.placeId ?? connected2[0]?.placeId,
6282
- place_version: record2?.placeVersion,
8591
+ managed: false,
8592
+ state: "connected",
8593
+ place_id: connected2[0]?.placeId,
8594
+ connected: true,
6283
8595
  roles: connected2.map((instance) => instance.role).sort()
6284
8596
  });
6285
8597
  }
@@ -6295,14 +8607,32 @@ ${code}`
6295
8607
  }
6296
8608
  if (action === "close") {
6297
8609
  let record2;
8610
+ if (launch_id) {
8611
+ record2 = this.instanceManager.getByLaunchId(launch_id);
8612
+ if (!record2)
8613
+ return this._textResult({ error: "Launch is not managed.", launch_id });
8614
+ const connectedInstanceId = record2.instanceId;
8615
+ const closeResult2 = record2.closedAt === void 0 ? this.instanceManager.close(record2) : { status: "already_closed" };
8616
+ if (connectedInstanceId) {
8617
+ await this.bridge.unregisterInstanceIdEverywhere(connectedInstanceId);
8618
+ await sleep(500);
8619
+ await this.bridge.unregisterInstanceIdEverywhere(connectedInstanceId);
8620
+ }
8621
+ return this._textResult({
8622
+ ...this._managedStatus(record2),
8623
+ message: closeResult2.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
8624
+ });
8625
+ }
6298
8626
  if (instance_id) {
8627
+ const recordBeforeClose = this.instanceManager.get(instance_id);
6299
8628
  const managedClose = this.instanceManager.closeByInstanceId(instance_id);
6300
8629
  if (managedClose.status !== "not_found") {
6301
8630
  await this.bridge.unregisterInstanceIdEverywhere(instance_id);
6302
8631
  await sleep(500);
6303
8632
  await this.bridge.unregisterInstanceIdEverywhere(instance_id);
8633
+ const closedRecord = recordBeforeClose ?? (managedClose.launchId ? this.instanceManager.getByLaunchId(managedClose.launchId) : void 0);
6304
8634
  return this._textResult({
6305
- instance_id,
8635
+ ...closedRecord ? this._managedStatus(closedRecord) : { instance_id },
6306
8636
  message: managedClose.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
6307
8637
  });
6308
8638
  }
@@ -6349,7 +8679,7 @@ ${code}`
6349
8679
  await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
6350
8680
  }
6351
8681
  return this._textResult({
6352
- instance_id: record2.instanceId,
8682
+ ...this._managedStatus(record2),
6353
8683
  message: closeResult.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
6354
8684
  });
6355
8685
  }
@@ -6376,24 +8706,34 @@ ${code}`
6376
8706
  localPlaceFile,
6377
8707
  placeId,
6378
8708
  universeId,
6379
- placeVersion
8709
+ placeVersion,
8710
+ connectionTimeoutMs: timeoutMs
6380
8711
  });
6381
8712
  if (!waitForConnection) {
6382
- return this._textResult({ message: "Studio launch requested." });
8713
+ return this._textResult({
8714
+ ...this._managedStatus(record),
8715
+ message: "Studio launch requested."
8716
+ });
6383
8717
  }
6384
8718
  const connected = await this._waitForManagedEditConnection(record, beforeKeys, timeoutMs);
6385
8719
  if (!connected) {
6386
- try {
6387
- this.instanceManager.close(record);
6388
- } catch {
8720
+ if (record.state === "launching") {
8721
+ this.instanceManager.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
8722
+ }
8723
+ if (record.closedAt === void 0) {
8724
+ try {
8725
+ this.instanceManager.close(record);
8726
+ } catch {
8727
+ }
6389
8728
  }
6390
8729
  return this._textResult({
6391
- error: "Studio launched, but the MCP plugin did not connect before timeout."
8730
+ ...this._managedStatus(record),
8731
+ error: record.failureReason ?? "Studio launched, but the MCP plugin did not connect before timeout."
6392
8732
  });
6393
8733
  }
6394
8734
  this.instanceManager.attachInstanceId(record, connected.instanceId);
6395
8735
  return this._textResult({
6396
- instance_id: connected.instanceId,
8736
+ ...this._managedStatus(record),
6397
8737
  message: launchSource === "place_revision" ? `Studio opened place revision ${placeVersion}.` : "Studio opened."
6398
8738
  });
6399
8739
  }
@@ -6576,16 +8916,16 @@ ${code}`
6576
8916
  try {
6577
8917
  editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit");
6578
8918
  body.edit = editState;
6579
- } catch (err) {
6580
- body.edit = { error: err instanceof Error ? err.message : String(err) };
8919
+ } catch (err2) {
8920
+ body.edit = { error: err2 instanceof Error ? err2.message : String(err2) };
6581
8921
  }
6582
8922
  }
6583
8923
  if (server) {
6584
8924
  try {
6585
8925
  serverState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "server");
6586
8926
  body.server = serverState;
6587
- } catch (err) {
6588
- body.server = { error: err instanceof Error ? err.message : String(err) };
8927
+ } catch (err2) {
8928
+ body.server = { error: err2 instanceof Error ? err2.message : String(err2) };
6589
8929
  }
6590
8930
  }
6591
8931
  const session = editState?.session;
@@ -6911,14 +9251,14 @@ ${code}`
6911
9251
  };
6912
9252
  }
6913
9253
  static findProjectRoot(startDir) {
6914
- let dir = path4.resolve(startDir);
9254
+ let dir = path5.resolve(startDir);
6915
9255
  let previous = "";
6916
9256
  while (dir !== previous) {
6917
- if (fs3.existsSync(path4.join(dir, ".git")) || fs3.existsSync(path4.join(dir, "package.json"))) {
9257
+ if (fs3.existsSync(path5.join(dir, ".git")) || fs3.existsSync(path5.join(dir, "package.json"))) {
6918
9258
  return dir;
6919
9259
  }
6920
9260
  previous = dir;
6921
- dir = path4.dirname(dir);
9261
+ dir = path5.dirname(dir);
6922
9262
  }
6923
9263
  return null;
6924
9264
  }
@@ -6932,7 +9272,7 @@ ${code}`
6932
9272
  }
6933
9273
  }
6934
9274
  static ensureWritableDirectory(candidate, label) {
6935
- const resolved = path4.resolve(candidate);
9275
+ const resolved = path5.resolve(candidate);
6936
9276
  try {
6937
9277
  fs3.mkdirSync(resolved, { recursive: true });
6938
9278
  } catch (error) {
@@ -6953,11 +9293,11 @@ ${code}`
6953
9293
  if (_RobloxStudioTools._cachedLibraryPath)
6954
9294
  return _RobloxStudioTools._cachedLibraryPath;
6955
9295
  const overridePath = process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
6956
- const cwd = path4.resolve(process.cwd());
9296
+ const cwd = path5.resolve(process.cwd());
6957
9297
  const projectRoot = _RobloxStudioTools.findProjectRoot(cwd);
6958
- const homeLibraryPath = path4.join(os3.homedir(), ".robloxstudio-mcp", "build-library");
6959
- const projectLibraryPath = projectRoot ? path4.join(projectRoot, "build-library") : null;
6960
- const cwdLibraryPath = path4.join(cwd, "build-library");
9298
+ const homeLibraryPath = path5.join(os3.homedir(), ".robloxstudio-mcp", "build-library");
9299
+ const projectLibraryPath = projectRoot ? path5.join(projectRoot, "build-library") : null;
9300
+ const cwdLibraryPath = path5.join(cwd, "build-library");
6961
9301
  let result;
6962
9302
  if (overridePath) {
6963
9303
  result = _RobloxStudioTools.ensureWritableDirectory(overridePath, "override");
@@ -6971,12 +9311,12 @@ ${code}`
6971
9311
  }
6972
9312
  })());
6973
9313
  if (existing) {
6974
- result = path4.resolve(existing);
9314
+ result = path5.resolve(existing);
6975
9315
  } else if (projectLibraryPath) {
6976
9316
  try {
6977
9317
  result = _RobloxStudioTools.ensureWritableDirectory(projectLibraryPath, "project-root");
6978
- } catch (err) {
6979
- console.error(`Warning: could not create build-library at project root (${projectLibraryPath}): ${err.message}. Falling back to home directory.`);
9318
+ } catch (err2) {
9319
+ console.error(`Warning: could not create build-library at project root (${projectLibraryPath}): ${err2.message}. Falling back to home directory.`);
6980
9320
  result = _RobloxStudioTools.ensureWritableDirectory(homeLibraryPath, "home");
6981
9321
  }
6982
9322
  } else {
@@ -6998,8 +9338,8 @@ ${code}`
6998
9338
  if (response && response.success && response.buildData) {
6999
9339
  const buildData = response.buildData;
7000
9340
  const buildId = buildData.id || `${style}/exported`;
7001
- const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${buildId}.json`);
7002
- const dirPath = path4.dirname(filePath);
9341
+ const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${buildId}.json`);
9342
+ const dirPath = path5.dirname(filePath);
7003
9343
  if (!fs3.existsSync(dirPath)) {
7004
9344
  fs3.mkdirSync(dirPath, { recursive: true });
7005
9345
  }
@@ -7100,8 +9440,8 @@ ${code}`
7100
9440
  const normalizedParts = this.normalizeBuildParts(parts, new Set(Object.keys(normalizedPalette)));
7101
9441
  const computedBounds = bounds || this.computeBounds(normalizedParts);
7102
9442
  const buildData = { id, style, bounds: computedBounds, palette: normalizedPalette, parts: normalizedParts };
7103
- const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
7104
- const dirPath = path4.dirname(filePath);
9443
+ const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
9444
+ const dirPath = path5.dirname(filePath);
7105
9445
  if (!fs3.existsSync(dirPath)) {
7106
9446
  fs3.mkdirSync(dirPath, { recursive: true });
7107
9447
  }
@@ -7159,8 +9499,8 @@ ${code}`
7159
9499
  };
7160
9500
  if (seed !== void 0)
7161
9501
  buildData.generatorSeed = seed;
7162
- const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
7163
- const dirPath = path4.dirname(filePath);
9502
+ const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
9503
+ const dirPath = path5.dirname(filePath);
7164
9504
  if (!fs3.existsSync(dirPath)) {
7165
9505
  fs3.mkdirSync(dirPath, { recursive: true });
7166
9506
  }
@@ -7188,13 +9528,13 @@ ${code}`
7188
9528
  }
7189
9529
  let resolved;
7190
9530
  if (typeof buildData === "string") {
7191
- const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${buildData}.json`);
9531
+ const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${buildData}.json`);
7192
9532
  if (!fs3.existsSync(filePath)) {
7193
9533
  throw new Error(`Build not found in library: ${buildData}`);
7194
9534
  }
7195
9535
  resolved = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
7196
9536
  } else if (buildData.id && !buildData.parts) {
7197
- const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${buildData.id}.json`);
9537
+ const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${buildData.id}.json`);
7198
9538
  if (!fs3.existsSync(filePath)) {
7199
9539
  throw new Error(`Build not found in library: ${buildData.id}`);
7200
9540
  }
@@ -7221,13 +9561,13 @@ ${code}`
7221
9561
  const styles = style ? [style] : ["medieval", "modern", "nature", "scifi", "misc"];
7222
9562
  const builds = [];
7223
9563
  for (const s of styles) {
7224
- const dirPath = path4.join(libraryPath, s);
9564
+ const dirPath = path5.join(libraryPath, s);
7225
9565
  if (!fs3.existsSync(dirPath))
7226
9566
  continue;
7227
9567
  const files = fs3.readdirSync(dirPath).filter((f) => f.endsWith(".json"));
7228
9568
  for (const file of files) {
7229
9569
  try {
7230
- const content = fs3.readFileSync(path4.join(dirPath, file), "utf-8");
9570
+ const content = fs3.readFileSync(path5.join(dirPath, file), "utf-8");
7231
9571
  const data = JSON.parse(content);
7232
9572
  builds.push({
7233
9573
  id: data.id || `${s}/${file.replace(".json", "")}`,
@@ -7266,7 +9606,7 @@ ${code}`
7266
9606
  if (!id) {
7267
9607
  throw new Error("Build ID is required for get_build");
7268
9608
  }
7269
- const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
9609
+ const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
7270
9610
  if (!fs3.existsSync(filePath)) {
7271
9611
  throw new Error(`Build not found in library: ${id}`);
7272
9612
  }
@@ -7353,7 +9693,7 @@ ${code}`
7353
9693
  if (!buildId) {
7354
9694
  throw new Error(`Invalid ${validatedKeyPath}: model key "${modelKey}" is not defined in sceneData.models`);
7355
9695
  }
7356
- const filePath = path4.join(libraryPath, `${buildId}.json`);
9696
+ const filePath = path5.join(libraryPath, `${buildId}.json`);
7357
9697
  if (!fs3.existsSync(filePath)) {
7358
9698
  throw new Error(`Build not found in library: ${buildId}`);
7359
9699
  }
@@ -7701,7 +10041,7 @@ ${code}`
7701
10041
  throw new Error(`File not found: ${filePath}`);
7702
10042
  }
7703
10043
  const fileContent = fs3.readFileSync(filePath);
7704
- const fileName = path4.basename(filePath);
10044
+ const fileName = path5.basename(filePath);
7705
10045
  if (assetType === "Decal" && this.cookieClient.hasCookie()) {
7706
10046
  const result2 = await this.cookieClient.uploadDecal(fileContent, displayName, description || "");
7707
10047
  return {
@@ -7926,12 +10266,12 @@ ${code}`
7926
10266
  return { content: [{ type: "text", text: JSON.stringify({ error: "plugin returned no base64 payload" }) }] };
7927
10267
  }
7928
10268
  const bytes = Buffer.from(response.base64, "base64");
7929
- const resolved = path4.resolve(outputPath);
10269
+ const resolved = path5.resolve(outputPath);
7930
10270
  try {
7931
- fs3.mkdirSync(path4.dirname(resolved), { recursive: true });
10271
+ fs3.mkdirSync(path5.dirname(resolved), { recursive: true });
7932
10272
  fs3.writeFileSync(resolved, bytes);
7933
- } catch (err) {
7934
- return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err.message}` }) }] };
10273
+ } catch (err2) {
10274
+ return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err2.message}` }) }] };
7935
10275
  }
7936
10276
  return {
7937
10277
  content: [{
@@ -7962,11 +10302,11 @@ ${code}`
7962
10302
  let bytes;
7963
10303
  let sourceLabel;
7964
10304
  if (source.path !== void 0) {
7965
- const resolved = path4.resolve(source.path);
10305
+ const resolved = path5.resolve(source.path);
7966
10306
  try {
7967
10307
  bytes = fs3.readFileSync(resolved);
7968
- } catch (err) {
7969
- return { content: [{ type: "text", text: JSON.stringify({ error: `failed to read ${resolved}: ${err.message}` }) }] };
10308
+ } catch (err2) {
10309
+ return { content: [{ type: "text", text: JSON.stringify({ error: `failed to read ${resolved}: ${err2.message}` }) }] };
7970
10310
  }
7971
10311
  sourceLabel = resolved;
7972
10312
  } else if (source.url !== void 0) {
@@ -7995,15 +10335,15 @@ ${code}`
7995
10335
  return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url}: downloaded ${arr.byteLength} bytes exceeds ${MAX_IMPORT_BYTES} byte cap` }) }] };
7996
10336
  }
7997
10337
  bytes = Buffer.from(arr);
7998
- } catch (err) {
7999
- return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url} failed: ${err.message}` }) }] };
10338
+ } catch (err2) {
10339
+ return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url} failed: ${err2.message}` }) }] };
8000
10340
  }
8001
10341
  sourceLabel = source.url;
8002
10342
  } else {
8003
10343
  try {
8004
10344
  bytes = Buffer.from(source.base64, "base64");
8005
- } catch (err) {
8006
- return { content: [{ type: "text", text: JSON.stringify({ error: `base64 decode failed: ${err.message}` }) }] };
10345
+ } catch (err2) {
10346
+ return { content: [{ type: "text", text: JSON.stringify({ error: `base64 decode failed: ${err2.message}` }) }] };
8007
10347
  }
8008
10348
  sourceLabel = `base64(${bytes.length}B)`;
8009
10349
  }
@@ -8107,26 +10447,42 @@ var init_proxy_bridge_service = __esm({
8107
10447
  init_bridge_service();
8108
10448
  ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
8109
10449
  primaryBaseUrl;
10450
+ authToken;
8110
10451
  proxyInstanceId;
8111
10452
  proxyRequestTimeout = 3e4;
8112
10453
  cachedInstances = [];
8113
10454
  refreshTimer;
8114
10455
  static REFRESH_INTERVAL_MS = 1e3;
8115
- constructor(primaryBaseUrl) {
10456
+ constructor(primaryBaseUrl, authToken) {
8116
10457
  super();
8117
10458
  this.primaryBaseUrl = primaryBaseUrl;
10459
+ this.authToken = authToken;
8118
10460
  this.proxyInstanceId = uuidv42();
8119
10461
  this.refreshInstances();
8120
10462
  this.refreshTimer = setInterval(() => this.refreshInstances(), _ProxyBridgeService.REFRESH_INTERVAL_MS);
8121
10463
  }
10464
+ authHeaders(extra) {
10465
+ const headers = { ...extra };
10466
+ if (this.authToken)
10467
+ headers["X-MCP-Auth"] = this.authToken;
10468
+ return headers;
10469
+ }
8122
10470
  async refreshInstances() {
8123
10471
  try {
8124
- const res = await fetch(`${this.primaryBaseUrl}/instances`);
10472
+ const res = await fetch(`${this.primaryBaseUrl}/instances`, {
10473
+ headers: this.authHeaders()
10474
+ });
8125
10475
  if (!res.ok)
8126
10476
  return;
8127
10477
  const body = await res.json();
8128
10478
  if (Array.isArray(body.instances)) {
10479
+ const previousKeys = new Set(this.cachedInstances.map((instance) => `${instance.pluginSessionId}\0${instance.instanceId}\0${instance.role}\0${instance.connectedAt}`));
8129
10480
  this.cachedInstances = body.instances;
10481
+ for (const instance of body.instances) {
10482
+ const key = `${instance.pluginSessionId}\0${instance.instanceId}\0${instance.role}\0${instance.connectedAt}`;
10483
+ if (!previousKeys.has(key))
10484
+ this.notifyInstanceRegistered(toPublic(instance));
10485
+ }
8130
10486
  }
8131
10487
  } catch {
8132
10488
  }
@@ -8137,7 +10493,7 @@ var init_proxy_bridge_service = __esm({
8137
10493
  async unregisterInstanceIdEverywhere(instanceId) {
8138
10494
  const response = await fetch(`${this.primaryBaseUrl}/unregister-instance-id`, {
8139
10495
  method: "POST",
8140
- headers: { "Content-Type": "application/json" },
10496
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
8141
10497
  body: JSON.stringify({ instanceId })
8142
10498
  });
8143
10499
  if (!response.ok) {
@@ -8166,7 +10522,7 @@ var init_proxy_bridge_service = __esm({
8166
10522
  try {
8167
10523
  const response = await fetch(`${this.primaryBaseUrl}/proxy`, {
8168
10524
  method: "POST",
8169
- headers: { "Content-Type": "application/json" },
10525
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
8170
10526
  body: JSON.stringify({
8171
10527
  endpoint,
8172
10528
  data,
@@ -8186,12 +10542,12 @@ var init_proxy_bridge_service = __esm({
8186
10542
  throw new Error(result.error);
8187
10543
  }
8188
10544
  return result.response;
8189
- } catch (err) {
10545
+ } catch (err2) {
8190
10546
  clearTimeout(timeoutId);
8191
- if (err.name === "AbortError") {
10547
+ if (err2.name === "AbortError") {
8192
10548
  throw new Error("Proxy request timeout");
8193
10549
  }
8194
- throw err;
10550
+ throw err2;
8195
10551
  }
8196
10552
  }
8197
10553
  cleanupOldRequests() {
@@ -8211,6 +10567,7 @@ var init_server = __esm({
8211
10567
  "../core/dist/server.js"() {
8212
10568
  "use strict";
8213
10569
  init_http_server();
10570
+ init_auth();
8214
10571
  init_tools();
8215
10572
  init_bridge_service();
8216
10573
  init_proxy_bridge_service();
@@ -8280,14 +10637,28 @@ var init_server = __esm({
8280
10637
  }
8281
10638
  async run() {
8282
10639
  const basePort = process.env.ROBLOX_STUDIO_PORT ? parseInt(process.env.ROBLOX_STUDIO_PORT) : 58741;
8283
- const host = process.env.ROBLOX_STUDIO_HOST || "0.0.0.0";
10640
+ const host = process.env.ROBLOX_STUDIO_HOST?.trim() || "127.0.0.1";
10641
+ if (host !== "127.0.0.1" && host !== "localhost" && host !== "::1") {
10642
+ console.error(`WARNING: ROBLOX_STUDIO_HOST=${host} exposes the MCP bridge beyond this machine. Anyone who can reach the port and knows the auth token can control Roblox Studio.`);
10643
+ }
10644
+ const auth = resolveAuthToken();
10645
+ const security = {
10646
+ authToken: auth.token,
10647
+ authTokenHint: auth.source === "env" ? "The token comes from ROBLOX_STUDIO_AUTH_TOKEN." : auth.filePath ? `The token is in ${auth.filePath}.` : void 0,
10648
+ allowedOrigins: (process.env.ROBLOX_STUDIO_ALLOWED_ORIGINS || "").split(",").map((o) => o.trim()).filter((o) => o !== "")
10649
+ };
10650
+ if (auth.source === "disabled") {
10651
+ console.error("WARNING: ROBLOX_STUDIO_NO_AUTH is set - tool endpoints accept unauthenticated requests.");
10652
+ } else if (auth.filePath) {
10653
+ console.error(`Auth token loaded from ${auth.filePath} (HTTP clients must send X-MCP-Auth or Authorization: Bearer)`);
10654
+ }
8284
10655
  let bridgeMode = "primary";
8285
10656
  let httpHandle;
8286
10657
  let primaryApp;
8287
10658
  let boundPort = 0;
8288
10659
  let promotionInterval;
8289
10660
  try {
8290
- primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
10661
+ primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config, security);
8291
10662
  const result = await listenWithRetry(primaryApp, host, basePort, 1);
8292
10663
  httpHandle = result.server;
8293
10664
  boundPort = result.port;
@@ -8296,7 +10667,7 @@ var init_server = __esm({
8296
10667
  } catch {
8297
10668
  bridgeMode = "proxy";
8298
10669
  primaryApp = void 0;
8299
- const proxyBridge = new ProxyBridgeService(`http://localhost:${basePort}`);
10670
+ const proxyBridge = new ProxyBridgeService(`http://localhost:${basePort}`, auth.token);
8300
10671
  this.bridge = proxyBridge;
8301
10672
  this.tools = new RobloxStudioTools(this.bridge);
8302
10673
  console.error(`Port ${basePort} in use - entering proxy mode (forwarding to localhost:${basePort})`);
@@ -8304,7 +10675,7 @@ var init_server = __esm({
8304
10675
  promotionInterval = setInterval(async () => {
8305
10676
  const candidateBridge = new BridgeService();
8306
10677
  const candidateTools = new RobloxStudioTools(candidateBridge);
8307
- const candidateApp = createHttpServer(candidateTools, candidateBridge, this.allowedToolNames, this.config);
10678
+ const candidateApp = createHttpServer(candidateTools, candidateBridge, this.allowedToolNames, this.config, security);
8308
10679
  try {
8309
10680
  const result = await listenWithRetry(candidateApp, host, basePort, 1);
8310
10681
  const oldBridge = this.bridge;
@@ -8329,7 +10700,7 @@ var init_server = __esm({
8329
10700
  let legacyHandle;
8330
10701
  let legacyApp;
8331
10702
  if (boundPort !== LEGACY_PORT && bridgeMode === "primary") {
8332
- legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
10703
+ legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config, security);
8333
10704
  try {
8334
10705
  const result = await listenWithRetry(legacyApp, host, LEGACY_PORT, 1);
8335
10706
  legacyHandle = result.server;
@@ -9359,7 +11730,7 @@ var init_definitions = __esm({
9359
11730
  {
9360
11731
  name: "manage_instance",
9361
11732
  category: "write",
9362
- description: 'Launch, close, inspect, and find revisions for Studio instances. Use action="launch" with source="baseplate" for a blank place, or source="local_file" with local_place_file for a local place; neither uses place_id. Use action="list_place_versions" with place_id to retrieve version numbers through Open Cloud asset versions, then action="launch" with source="place_revision", place_id, and place_version to open an older revision. action="close" can close an MCP-managed instance or an explicitly connected edit instance by instance_id. action="launch" source="published_place" opens the latest published place and is blocked if that place_id is already connected; source="place_revision" is allowed because Studio opens explicit past revisions as anonymous local copies. Requires ROBLOX_OPEN_CLOUD_API_KEY with asset:read for list_place_versions.',
11733
+ description: 'Launch, close, inspect, and find revisions for Studio instances. Every launch returns launch_id, native pid, source, and lifecycle state; status and close accept launch_id before the plugin connects and instance_id after association. Use action="launch" with source="baseplate" for a blank place, or source="local_file" with local_place_file for a local place; neither uses place_id. Use action="list_place_versions" with place_id to retrieve version numbers through Open Cloud asset versions, then action="launch" with source="place_revision", place_id, and place_version to open an older revision. action="launch" source="published_place" opens the latest published place and is blocked if that place_id is already connected; source="place_revision" is allowed because Studio opens explicit past revisions as anonymous local copies. Requires ROBLOX_OPEN_CLOUD_API_KEY with asset:read for list_place_versions.',
9363
11734
  inputSchema: {
9364
11735
  type: "object",
9365
11736
  properties: {
@@ -9387,11 +11758,11 @@ var init_definitions = __esm({
9387
11758
  },
9388
11759
  wait_for_connection: {
9389
11760
  type: "boolean",
9390
- description: 'For action="launch": wait until the MCP plugin connects and return instance_id (default true).'
11761
+ description: 'For action="launch": wait until the MCP plugin connects and return instance_id (default true). false returns launch_id immediately and continues association/failure tracking asynchronously.'
9391
11762
  },
9392
11763
  timeout_ms: {
9393
11764
  type: "number",
9394
- description: 'For action="launch": max milliseconds to wait for plugin connection (default 120000).'
11765
+ description: 'For action="launch": max milliseconds for plugin connection (default 120000). The deadline also applies asynchronously when wait_for_connection=false.'
9395
11766
  },
9396
11767
  max_page_size: {
9397
11768
  type: "number",
@@ -9403,7 +11774,11 @@ var init_definitions = __esm({
9403
11774
  },
9404
11775
  instance_id: {
9405
11776
  type: "string",
9406
- description: 'For action="close" or action="status": Studio instance to inspect or close. close accepts MCP-managed instances and explicitly connected edit instances.'
11777
+ description: 'For action="close" or action="status": connected Studio instance to inspect or close. Mutually exclusive with launch_id.'
11778
+ },
11779
+ launch_id: {
11780
+ type: "string",
11781
+ description: 'For action="close" or action="status": opaque identifier returned by launch. Works before plugin connection and for retained terminal launch status. Mutually exclusive with instance_id.'
9407
11782
  }
9408
11783
  },
9409
11784
  required: ["action"]
@@ -10186,7 +12561,7 @@ CYLINDER AXIS: Roblox cylinders extend along the X axis. For upright cylinders,
10186
12561
  EXAMPLE - compact cabin (17 lines):
10187
12562
  room(0,0,0,8,4,6,"a","b","a")
10188
12563
  roof(0,4,0,8,6,"gable","c")
10189
- wall(-4,0,-2,4,0,-2,4,1,"a")
12564
+ wall(-4,-2,4,-2,4,1,"a")
10190
12565
  part(0,2,3,3,3,0.3,"a","Block",0.4)
10191
12566
  row(-2,0,-1,3,0,2,(i,cx,cy,cz)=>{pew(cx,0,cz,3,2,"d")})
10192
12567
  column(-3,0,-2,4,0.5,"a","b")
@@ -10981,6 +13356,27 @@ part(0,2,0,2,1,1,"b")`,
10981
13356
  required: ["pattern", "replacement"]
10982
13357
  }
10983
13358
  },
13359
+ // === Installed Studio Skills ===
13360
+ {
13361
+ name: "get_roblox_skills",
13362
+ category: "read",
13363
+ description: `List or retrieve Roblox-authored skills embedded in the locally installed Studio Assistant bundle. This reads the installed Assistant.rbxm directly, so it does not require a connected Studio place or Roblox's built-in MCP. Use action="list" to discover available names, then action="get" to retrieve the exact Markdown for one skill.`,
13364
+ inputSchema: {
13365
+ type: "object",
13366
+ properties: {
13367
+ action: {
13368
+ type: "string",
13369
+ enum: ["list", "get"],
13370
+ description: "List installed built-in skills or get one skill document."
13371
+ },
13372
+ name: {
13373
+ type: "string",
13374
+ description: 'Skill name returned by action="list". Required for action="get"; both canonical rbx-* and embedded source names are accepted.'
13375
+ }
13376
+ },
13377
+ required: ["action"]
13378
+ }
13379
+ },
10984
13380
  // === Documentation ===
10985
13381
  {
10986
13382
  name: "get_roblox_docs",
@@ -11164,15 +13560,15 @@ part(0,2,0,2,1,1,"b")`,
11164
13560
  });
11165
13561
 
11166
13562
  // ../core/dist/install-plugin-helpers.js
11167
- import { existsSync as existsSync4, readFileSync as readFileSync5, unlinkSync } from "fs";
13563
+ import { existsSync as existsSync6, readFileSync as readFileSync7, unlinkSync } from "fs";
11168
13564
  import { execSync } from "child_process";
11169
- import { join as join4 } from "path";
11170
- import { homedir as homedir4 } from "os";
13565
+ import { join as join6 } from "path";
13566
+ import { homedir as homedir5 } from "os";
11171
13567
  function isWSL() {
11172
13568
  if (process.platform !== "linux")
11173
13569
  return false;
11174
13570
  try {
11175
- const v = readFileSync5("/proc/version", "utf8");
13571
+ const v = readFileSync7("/proc/version", "utf8");
11176
13572
  return /microsoft|wsl/i.test(v);
11177
13573
  } catch {
11178
13574
  return false;
@@ -11190,7 +13586,7 @@ function getWindowsUserPluginsDir() {
11190
13586
  }).toString().trim();
11191
13587
  if (!linuxPath)
11192
13588
  return null;
11193
- return join4(linuxPath, "Roblox", "Plugins");
13589
+ return join6(linuxPath, "Roblox", "Plugins");
11194
13590
  } catch {
11195
13591
  return null;
11196
13592
  }
@@ -11199,7 +13595,7 @@ function getPluginsFolder() {
11199
13595
  if (process.env.MCP_PLUGINS_DIR)
11200
13596
  return process.env.MCP_PLUGINS_DIR;
11201
13597
  if (process.platform === "win32") {
11202
- return join4(process.env.LOCALAPPDATA || join4(homedir4(), "AppData", "Local"), "Roblox", "Plugins");
13598
+ return join6(process.env.LOCALAPPDATA || join6(homedir5(), "AppData", "Local"), "Roblox", "Plugins");
11203
13599
  }
11204
13600
  if (isWSL()) {
11205
13601
  const win = getWindowsUserPluginsDir();
@@ -11207,18 +13603,18 @@ function getPluginsFolder() {
11207
13603
  return win;
11208
13604
  console.warn("[install-plugin] WSL detected but could not resolve Windows %LOCALAPPDATA%. Falling back to ~/Documents/Roblox/Plugins/ - you will likely need to copy the rbxmx to /mnt/c/Users/<you>/AppData/Local/Roblox/Plugins/ manually. Set MCP_PLUGINS_DIR to skip detection.");
11209
13605
  }
11210
- return join4(homedir4(), "Documents", "Roblox", "Plugins");
13606
+ return join6(homedir5(), "Documents", "Roblox", "Plugins");
11211
13607
  }
11212
13608
  function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
11213
- const otherDest = join4(pluginsFolder, otherAssetName);
11214
- if (!existsSync4(otherDest))
13609
+ const otherDest = join6(pluginsFolder, otherAssetName);
13610
+ if (!existsSync6(otherDest))
11215
13611
  return;
11216
13612
  if (replace) {
11217
13613
  try {
11218
13614
  unlinkSync(otherDest);
11219
13615
  log(`Removed conflicting ${otherAssetName}.`);
11220
- } catch (err) {
11221
- warn(`[install-plugin] Could not remove ${otherDest}: ${err}. Continuing.`);
13616
+ } catch (err2) {
13617
+ warn(`[install-plugin] Could not remove ${otherDest}: ${err2}. Continuing.`);
11222
13618
  }
11223
13619
  return;
11224
13620
  }
@@ -11247,6 +13643,7 @@ var init_dist = __esm({
11247
13643
  init_opencloud_client();
11248
13644
  init_install_plugin_helpers();
11249
13645
  init_roblox_cookie_client();
13646
+ init_studio_skills();
11250
13647
  }
11251
13648
  });
11252
13649
 
@@ -11256,13 +13653,13 @@ __export(install_plugin_exports, {
11256
13653
  installBundledPlugin: () => installBundledPlugin,
11257
13654
  installPlugin: () => installPlugin
11258
13655
  });
11259
- import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync6, unlinkSync as unlinkSync2 } from "fs";
11260
- import { dirname as dirname3, join as join5 } from "path";
13656
+ import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync8, unlinkSync as unlinkSync2 } from "fs";
13657
+ import { dirname as dirname5, join as join7 } from "path";
11261
13658
  import { fileURLToPath } from "url";
11262
13659
  import { get } from "https";
11263
13660
  function httpsGet(url) {
11264
- return new Promise((resolve4, reject) => {
11265
- const req = get(url, { headers: { "User-Agent": "robloxstudio-mcp-inspector" } }, resolve4);
13661
+ return new Promise((resolve5, reject) => {
13662
+ const req = get(url, { headers: { "User-Agent": "robloxstudio-mcp-inspector" } }, resolve5);
11266
13663
  req.on("error", reject);
11267
13664
  req.setTimeout(TIMEOUT_MS, () => {
11268
13665
  req.destroy(new Error(`Request timed out after ${TIMEOUT_MS}ms`));
@@ -11280,21 +13677,21 @@ async function download(url, dest, redirects = 0) {
11280
13677
  if (res.statusCode !== 200) {
11281
13678
  throw new Error(`Download failed: HTTP ${res.statusCode}`);
11282
13679
  }
11283
- return new Promise((resolve4, reject) => {
13680
+ return new Promise((resolve5, reject) => {
11284
13681
  const file = createWriteStream(dest);
11285
- const cleanup = (err) => {
13682
+ const cleanup = (err2) => {
11286
13683
  file.close(() => {
11287
13684
  try {
11288
13685
  unlinkSync2(dest);
11289
13686
  } catch {
11290
13687
  }
11291
- reject(err);
13688
+ reject(err2);
11292
13689
  });
11293
13690
  };
11294
13691
  res.pipe(file);
11295
13692
  file.on("finish", () => {
11296
13693
  file.close();
11297
- resolve4();
13694
+ resolve5();
11298
13695
  });
11299
13696
  file.on("error", cleanup);
11300
13697
  res.on("error", cleanup);
@@ -11317,8 +13714,8 @@ function prepareInstall({
11317
13714
  warn
11318
13715
  }) {
11319
13716
  const pluginsFolder = getPluginsFolder();
11320
- if (!existsSync5(pluginsFolder)) {
11321
- mkdirSync4(pluginsFolder, { recursive: true });
13717
+ if (!existsSync7(pluginsFolder)) {
13718
+ mkdirSync5(pluginsFolder, { recursive: true });
11322
13719
  }
11323
13720
  handleVariantConflict({
11324
13721
  pluginsFolder,
@@ -11330,23 +13727,23 @@ function prepareInstall({
11330
13727
  return pluginsFolder;
11331
13728
  }
11332
13729
  function bundledAssetPath() {
11333
- const currentDir = dirname3(fileURLToPath(import.meta.url));
13730
+ const currentDir = dirname5(fileURLToPath(import.meta.url));
11334
13731
  const candidates = [
11335
- join5(currentDir, "..", "studio-plugin", ASSET_NAME),
11336
- join5(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
13732
+ join7(currentDir, "..", "studio-plugin", ASSET_NAME),
13733
+ join7(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
11337
13734
  ];
11338
- return candidates.find((candidate) => existsSync5(candidate)) ?? null;
13735
+ return candidates.find((candidate) => existsSync7(candidate)) ?? null;
11339
13736
  }
11340
13737
  function packageVersion() {
11341
- const currentDir = dirname3(fileURLToPath(import.meta.url));
11342
- const pkg = JSON.parse(readFileSync6(join5(currentDir, "..", "package.json"), "utf8"));
13738
+ const currentDir = dirname5(fileURLToPath(import.meta.url));
13739
+ const pkg = JSON.parse(readFileSync8(join7(currentDir, "..", "package.json"), "utf8"));
11343
13740
  if (!pkg.version) {
11344
13741
  throw new Error("Package version not found");
11345
13742
  }
11346
13743
  return pkg.version;
11347
13744
  }
11348
13745
  function bundledPluginVersion(source) {
11349
- const match = readFileSync6(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
13746
+ const match = readFileSync8(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
11350
13747
  return match ? match[1] : null;
11351
13748
  }
11352
13749
  function assertBundledPluginVersion(source) {
@@ -11359,9 +13756,9 @@ function assertBundledPluginVersion(source) {
11359
13756
  }
11360
13757
  }
11361
13758
  function filesMatch(a, b) {
11362
- if (!existsSync5(b)) return false;
11363
- const aBytes = readFileSync6(a);
11364
- const bBytes = readFileSync6(b);
13759
+ if (!existsSync7(b)) return false;
13760
+ const aBytes = readFileSync8(a);
13761
+ const bBytes = readFileSync8(b);
11365
13762
  return aBytes.length === bBytes.length && aBytes.equals(bBytes);
11366
13763
  }
11367
13764
  async function installBundledPlugin(options = {}) {
@@ -11374,7 +13771,7 @@ async function installBundledPlugin(options = {}) {
11374
13771
  }
11375
13772
  assertBundledPluginVersion(source);
11376
13773
  const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
11377
- const dest = join5(pluginsFolder, ASSET_NAME);
13774
+ const dest = join7(pluginsFolder, ASSET_NAME);
11378
13775
  if (filesMatch(source, dest)) return;
11379
13776
  copyFileSync2(source, dest);
11380
13777
  log(`Installed ${ASSET_NAME} to ${dest}`);
@@ -11387,7 +13784,7 @@ async function installPlugin(options = {}) {
11387
13784
  const bundled = bundledAssetPath();
11388
13785
  if (bundled) {
11389
13786
  assertBundledPluginVersion(bundled);
11390
- const dest2 = join5(pluginsFolder, ASSET_NAME);
13787
+ const dest2 = join7(pluginsFolder, ASSET_NAME);
11391
13788
  if (filesMatch(bundled, dest2)) {
11392
13789
  log(`${ASSET_NAME} already installed.`);
11393
13790
  return;
@@ -11402,7 +13799,7 @@ async function installPlugin(options = {}) {
11402
13799
  if (!asset) {
11403
13800
  throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
11404
13801
  }
11405
- const dest = join5(pluginsFolder, ASSET_NAME);
13802
+ const dest = join7(pluginsFolder, ASSET_NAME);
11406
13803
  log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
11407
13804
  await download(asset.browser_download_url, dest);
11408
13805
  log(`Installed to ${dest}`);
@@ -11425,8 +13822,8 @@ init_dist();
11425
13822
  import { createRequire } from "module";
11426
13823
  if (process.argv.includes("--install-plugin")) {
11427
13824
  const { installPlugin: installPlugin2 } = await Promise.resolve().then(() => (init_install_plugin(), install_plugin_exports));
11428
- await installPlugin2().catch((err) => {
11429
- console.error(err instanceof Error ? err.message : String(err));
13825
+ await installPlugin2().catch((err2) => {
13826
+ console.error(err2 instanceof Error ? err2.message : String(err2));
11430
13827
  process.exitCode = 1;
11431
13828
  });
11432
13829
  } else {
@@ -11435,9 +13832,9 @@ if (process.argv.includes("--install-plugin")) {
11435
13832
  await installBundledPlugin2({
11436
13833
  log: (message) => console.error(`[install-plugin] ${message}`),
11437
13834
  warn: (message) => console.error(message)
11438
- }).catch((err) => {
13835
+ }).catch((err2) => {
11439
13836
  console.error(
11440
- `[install-plugin] Auto-install skipped: ${err instanceof Error ? err.message : String(err)}`
13837
+ `[install-plugin] Auto-install skipped: ${err2 instanceof Error ? err2.message : String(err2)}`
11441
13838
  );
11442
13839
  });
11443
13840
  }