@chrrxs/robloxstudio-mcp-inspector 2.21.0 → 2.22.1

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
@@ -32,7 +32,7 @@ function publishedInstanceId(placeId) {
32
32
  return void 0;
33
33
  return `place:${Math.trunc(placeId)}`;
34
34
  }
35
- var RoutingFailure, STALE_INSTANCE_MS, INSTANCE_ALIAS_TTL_MS, BridgeService;
35
+ var RoutingFailure, STALE_INSTANCE_MS, DUPLICATE_TAKEOVER_MS, INSTANCE_ALIAS_TTL_MS, BridgeService;
36
36
  var init_bridge_service = __esm({
37
37
  "../core/dist/bridge-service.js"() {
38
38
  "use strict";
@@ -45,13 +45,33 @@ var init_bridge_service = __esm({
45
45
  }
46
46
  };
47
47
  STALE_INSTANCE_MS = 3e4;
48
+ DUPLICATE_TAKEOVER_MS = 3e3;
48
49
  INSTANCE_ALIAS_TTL_MS = 5 * 60 * 1e3;
49
50
  BridgeService = class {
50
51
  pendingRequests = /* @__PURE__ */ new Map();
51
52
  // Keyed by pluginSessionId (the per-plugin GUID).
52
53
  instances = /* @__PURE__ */ new Map();
53
54
  instanceAliases = /* @__PURE__ */ new Map();
55
+ instanceRegisteredListeners = /* @__PURE__ */ new Set();
54
56
  requestTimeout = 3e4;
57
+ onInstanceRegistered(listener) {
58
+ this.instanceRegisteredListeners.add(listener);
59
+ for (const instance of this.getPublicInstances()) {
60
+ try {
61
+ listener(instance);
62
+ } catch {
63
+ }
64
+ }
65
+ return () => this.instanceRegisteredListeners.delete(listener);
66
+ }
67
+ notifyInstanceRegistered(instance) {
68
+ for (const listener of this.instanceRegisteredListeners) {
69
+ try {
70
+ listener(instance);
71
+ } catch {
72
+ }
73
+ }
74
+ }
55
75
  canonicalInstanceId(instanceId, placeId) {
56
76
  return publishedInstanceId(placeId) ?? instanceId;
57
77
  }
@@ -148,16 +168,20 @@ var init_bridge_service = __esm({
148
168
  }
149
169
  const existing = Array.from(this.instances.values()).find((i) => i.instanceId === instanceId && i.role === assignedRole && i.pluginSessionId !== pluginSessionId);
150
170
  if (existing) {
151
- return {
152
- ok: false,
153
- error: {
154
- code: "duplicate_instance_role",
155
- message: `Another plugin is already registered as (${instanceId}, ${assignedRole}).`,
156
- existing: toPublic(existing)
157
- }
158
- };
171
+ if (Date.now() - existing.lastActivity > DUPLICATE_TAKEOVER_MS) {
172
+ this.unregisterInstance(existing.pluginSessionId);
173
+ } else {
174
+ return {
175
+ ok: false,
176
+ error: {
177
+ code: "duplicate_instance_role",
178
+ message: `Another plugin is already registered as (${instanceId}, ${assignedRole}).`,
179
+ existing: toPublic(existing)
180
+ }
181
+ };
182
+ }
159
183
  }
160
- this.instances.set(pluginSessionId, {
184
+ const registered = {
161
185
  pluginSessionId,
162
186
  instanceId,
163
187
  role: assignedRole,
@@ -171,7 +195,9 @@ var init_bridge_service = __esm({
171
195
  versionMismatch,
172
196
  lastActivity: Date.now(),
173
197
  connectedAt: prior?.connectedAt ?? Date.now()
174
- });
198
+ };
199
+ this.instances.set(pluginSessionId, registered);
200
+ this.notifyInstanceRegistered(toPublic(registered));
175
201
  return { ok: true, assignedRole, instanceId };
176
202
  }
177
203
  unregisterInstance(pluginSessionId) {
@@ -369,7 +395,7 @@ var init_bridge_service = __esm({
369
395
  async sendRequest(endpoint, data, targetInstanceId, targetRole, timeoutMs = this.requestTimeout) {
370
396
  const requestId = uuidv4();
371
397
  const effectiveTimeoutMs = Math.max(1, timeoutMs);
372
- return new Promise((resolve4, reject) => {
398
+ return new Promise((resolve5, reject) => {
373
399
  const timeoutId = setTimeout(() => {
374
400
  if (this.pendingRequests.has(requestId)) {
375
401
  this.pendingRequests.delete(requestId);
@@ -384,7 +410,7 @@ var init_bridge_service = __esm({
384
410
  targetRole,
385
411
  timestamp: Date.now(),
386
412
  inFlight: false,
387
- resolve: resolve4,
413
+ resolve: resolve5,
388
414
  reject,
389
415
  timeoutId,
390
416
  timeoutMs: effectiveTimeoutMs
@@ -480,6 +506,93 @@ function cacheSet(key, entry) {
480
506
  function docUrl(category, name) {
481
507
  return `${DOCS_BASE_URL}/${category}/${encodeURIComponent(name)}.md`;
482
508
  }
509
+ function normalizeDocName(value) {
510
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
511
+ }
512
+ function queryNameVariants(value) {
513
+ const parts = value.split(/[./:#\s]+/).filter(Boolean);
514
+ const variants = /* @__PURE__ */ new Set([normalizeDocName(value)]);
515
+ for (const part of parts) {
516
+ const normalized = normalizeDocName(part);
517
+ if (normalized && normalized !== "enum")
518
+ variants.add(normalized);
519
+ }
520
+ return Array.from(variants).filter(Boolean);
521
+ }
522
+ function editDistance(left, right) {
523
+ const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
524
+ for (let i = 1; i <= left.length; i++) {
525
+ let diagonal = previous[0];
526
+ previous[0] = i;
527
+ for (let j = 1; j <= right.length; j++) {
528
+ const above = previous[j];
529
+ previous[j] = Math.min(previous[j] + 1, previous[j - 1] + 1, diagonal + (left[i - 1] === right[j - 1] ? 0 : 1));
530
+ diagonal = above;
531
+ }
532
+ }
533
+ return previous[right.length];
534
+ }
535
+ async function fetchDocCatalog() {
536
+ if (catalogCache && Date.now() - catalogCache.fetchedAt <= CACHE_TTL_MS) {
537
+ return catalogCache.pages;
538
+ }
539
+ const controller = new AbortController();
540
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
541
+ let response;
542
+ try {
543
+ response = await fetch(DOCS_INDEX_URL, {
544
+ signal: controller.signal,
545
+ headers: { Accept: "text/html" }
546
+ });
547
+ } finally {
548
+ clearTimeout(timer);
549
+ }
550
+ if (!response.ok) {
551
+ throw new Error(`Failed to fetch Roblox docs index: HTTP ${response.status}`);
552
+ }
553
+ const html = await response.text();
554
+ const pages = /* @__PURE__ */ new Map();
555
+ const routePattern = /reference\/engine\/(classes|enums|datatypes|libraries|globals)\/([A-Za-z0-9_]+)/g;
556
+ for (const match of html.matchAll(routePattern)) {
557
+ const category = match[1];
558
+ const name = match[2];
559
+ const key = `${category}/${name}`;
560
+ pages.set(key, { category, name, url: docUrl(category, name) });
561
+ }
562
+ const result = Array.from(pages.values());
563
+ if (result.length === 0) {
564
+ throw new Error("Roblox docs index did not contain any engine reference pages");
565
+ }
566
+ catalogCache = { fetchedAt: Date.now(), pages: result };
567
+ return result;
568
+ }
569
+ async function recommendRobloxDocs(category, name, limit = 5) {
570
+ const variants = queryNameVariants(name);
571
+ const catalog = await fetchDocCatalog();
572
+ return catalog.map((page) => {
573
+ const candidate = normalizeDocName(page.name);
574
+ const similarity = Math.max(...variants.map((variant) => {
575
+ if (variant === candidate)
576
+ return 2;
577
+ const distance = editDistance(variant, candidate);
578
+ const ratio = 1 - distance / Math.max(variant.length, candidate.length, 1);
579
+ const prefixBonus = variant.startsWith(candidate) || candidate.startsWith(variant) ? 0.15 : 0;
580
+ const containsBonus = variant.includes(candidate) || candidate.includes(variant) ? 0.05 : 0;
581
+ return ratio + prefixBonus + containsBonus;
582
+ }));
583
+ return { page, score: similarity + (page.category === category ? 0.08 : 0) };
584
+ }).sort((a, b) => b.score - a.score || a.page.name.localeCompare(b.page.name)).slice(0, Math.max(1, limit)).map(({ page }) => page);
585
+ }
586
+ function recommendationsMarkdown(requestedCategory, requestedName, recommendations) {
587
+ const lines = recommendations.map((page) => `- [${page.category}/${page.name}](${page.url}) \u2014 retry with \`name="${page.name}", doc_type="${page.category}"\``);
588
+ return [
589
+ `# No exact Roblox documentation page found`,
590
+ "",
591
+ `The lookup \`${requestedCategory}/${requestedName}\` did not resolve. Recommended pages:`,
592
+ "",
593
+ ...lines
594
+ ].join("\n");
595
+ }
483
596
  async function fetchRobloxDoc(category, name) {
484
597
  const key = `${category}/${name}`;
485
598
  const cached = cacheGet(key);
@@ -544,7 +657,24 @@ function extractSection(markdown, section) {
544
657
  return lines.slice(start, end).join("\n").trimEnd();
545
658
  }
546
659
  async function getRobloxDoc(category, name, section) {
547
- const markdown = await fetchRobloxDoc(category, name);
660
+ let markdown;
661
+ try {
662
+ markdown = await fetchRobloxDoc(category, name);
663
+ } catch (error) {
664
+ if (!(error instanceof DocNotFoundError))
665
+ throw error;
666
+ try {
667
+ const recommendations = await recommendRobloxDocs(category, name);
668
+ return {
669
+ content: recommendationsMarkdown(category, name, recommendations),
670
+ truncated: false,
671
+ sections: [],
672
+ recommendations
673
+ };
674
+ } catch {
675
+ throw error;
676
+ }
677
+ }
548
678
  const sections = listSections(markdown);
549
679
  if (section) {
550
680
  const extracted = extractSection(markdown, section);
@@ -563,12 +693,13 @@ async function getRobloxDoc(category, name, section) {
563
693
  }
564
694
  return { content: markdown, truncated: false, sections };
565
695
  }
566
- var DOC_CATEGORIES, DOCS_BASE_URL, FETCH_TIMEOUT_MS, CACHE_TTL_MS, NEGATIVE_CACHE_TTL_MS, MAX_CACHE_ENTRIES, MAX_DOC_CHARS, cache, DocNotFoundError;
696
+ var DOC_CATEGORIES, DOCS_BASE_URL, DOCS_INDEX_URL, FETCH_TIMEOUT_MS, CACHE_TTL_MS, NEGATIVE_CACHE_TTL_MS, MAX_CACHE_ENTRIES, MAX_DOC_CHARS, cache, catalogCache, DocNotFoundError;
567
697
  var init_roblox_docs = __esm({
568
698
  "../core/dist/roblox-docs.js"() {
569
699
  "use strict";
570
700
  DOC_CATEGORIES = ["classes", "enums", "datatypes", "libraries", "globals"];
571
701
  DOCS_BASE_URL = "https://create.roblox.com/docs/reference/engine";
702
+ DOCS_INDEX_URL = DOCS_BASE_URL;
572
703
  FETCH_TIMEOUT_MS = 15e3;
573
704
  CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
574
705
  NEGATIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
@@ -686,8 +817,8 @@ function resolveAuthToken() {
686
817
  } catch {
687
818
  }
688
819
  return { token: fresh, source: "file", filePath };
689
- } catch (err) {
690
- console.error(`[auth] Could not read/create ${filePath} (${err instanceof Error ? err.message : err}). Using an in-memory token for this process; set ROBLOX_STUDIO_AUTH_TOKEN to share one across sessions.`);
820
+ } catch (err2) {
821
+ 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.`);
691
822
  return { token: randomBytes(32).toString("hex"), source: "file" };
692
823
  }
693
824
  }
@@ -811,7 +942,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
811
942
  next();
812
943
  });
813
944
  const authToken = security?.authToken;
814
- const authRequired = (path5) => path5 === "/mcp" || path5.startsWith("/mcp/") || path5 === "/proxy" || path5 === "/instances" || path5 === "/unregister-instance-id";
945
+ const authRequired = (path6) => path6 === "/mcp" || path6.startsWith("/mcp/") || path6 === "/proxy" || path6 === "/instances" || path6 === "/unregister-instance-id";
815
946
  app.use((req, res, next) => {
816
947
  if (!authToken || !authRequired(req.path)) {
817
948
  next();
@@ -891,11 +1022,11 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
891
1022
  pluginVariant: typeof pluginVariant === "string" ? pluginVariant : "unknown",
892
1023
  serverVersion: serverConfig?.version ?? ""
893
1024
  });
894
- } catch (err) {
1025
+ } catch (err2) {
895
1026
  res.status(500).json({
896
1027
  success: false,
897
1028
  error: "ready_registration_exception",
898
- message: err instanceof Error ? err.message : String(err),
1029
+ message: err2 instanceof Error ? err2.message : String(err2),
899
1030
  request: requestContext
900
1031
  });
901
1032
  return;
@@ -1046,8 +1177,8 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
1046
1177
  try {
1047
1178
  const response = await bridge.sendRequest(endpoint, data, targetInstanceId, targetRole);
1048
1179
  res.json({ response });
1049
- } catch (err) {
1050
- res.status(500).json({ error: err.message || "Proxy request failed" });
1180
+ } catch (err2) {
1181
+ res.status(500).json({ error: err2.message || "Proxy request failed" });
1051
1182
  }
1052
1183
  });
1053
1184
  if (serverConfig) {
@@ -1159,19 +1290,19 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
1159
1290
  return app;
1160
1291
  }
1161
1292
  function listenWithRetry(app, host, startPort, maxAttempts = 5) {
1162
- return new Promise(async (resolve4, reject) => {
1293
+ return new Promise(async (resolve5, reject) => {
1163
1294
  for (let i = 0; i < maxAttempts; i++) {
1164
1295
  const port = startPort + i;
1165
1296
  try {
1166
1297
  const server = await bindPort(app, host, port);
1167
- resolve4({ server, port });
1298
+ resolve5({ server, port });
1168
1299
  return;
1169
- } catch (err) {
1170
- if (err.code === "EADDRINUSE") {
1300
+ } catch (err2) {
1301
+ if (err2.code === "EADDRINUSE") {
1171
1302
  console.error(`Port ${port} in use, trying next...`);
1172
1303
  continue;
1173
1304
  }
1174
- reject(err);
1305
+ reject(err2);
1175
1306
  return;
1176
1307
  }
1177
1308
  }
@@ -1179,16 +1310,16 @@ function listenWithRetry(app, host, startPort, maxAttempts = 5) {
1179
1310
  });
1180
1311
  }
1181
1312
  function bindPort(app, host, port) {
1182
- return new Promise((resolve4, reject) => {
1313
+ return new Promise((resolve5, reject) => {
1183
1314
  const server = http.createServer(app);
1184
- const onError = (err) => {
1315
+ const onError = (err2) => {
1185
1316
  server.removeListener("error", onError);
1186
- reject(err);
1317
+ reject(err2);
1187
1318
  };
1188
1319
  server.once("error", onError);
1189
1320
  server.listen(port, host, () => {
1190
1321
  server.removeListener("error", onError);
1191
- resolve4(server);
1322
+ resolve5(server);
1192
1323
  });
1193
1324
  });
1194
1325
  }
@@ -1200,6 +1331,7 @@ var init_http_server = __esm({
1200
1331
  init_mcp_compat();
1201
1332
  init_auth();
1202
1333
  TOOL_HANDLERS = {
1334
+ get_roblox_skills: (tools, body) => tools.getRobloxSkills(body.action, body.name),
1203
1335
  get_roblox_docs: (tools, body) => tools.getRobloxDocs(body.name, body.doc_type, body.section),
1204
1336
  get_file_tree: (tools, body) => tools.getFileTree(body.path, body.instance_id),
1205
1337
  search_files: (tools, body) => tools.searchFiles(body.query, body.searchType, body.instance_id),
@@ -1382,8 +1514,8 @@ function runRestrictedScript(code, globals, options) {
1382
1514
  let ast;
1383
1515
  try {
1384
1516
  ast = acorn.parse(code, { ecmaVersion: 2022, sourceType: "script" });
1385
- } catch (err) {
1386
- throw new Error(`Syntax error: ${err.message}`);
1517
+ } catch (err2) {
1518
+ throw new Error(`Syntax error: ${err2.message}`);
1387
1519
  }
1388
1520
  const globalScope = new Scope(void 0, true);
1389
1521
  for (const [name, value] of Object.entries(globals)) {
@@ -1694,15 +1826,15 @@ function runRestrictedScript(code, globals, options) {
1694
1826
  case "TryStatement": {
1695
1827
  try {
1696
1828
  execute(node.block, scope);
1697
- } catch (err) {
1698
- if (err instanceof BreakSignal || err instanceof ContinueSignal || err instanceof ReturnSignal)
1699
- throw err;
1700
- if (err instanceof ScriptTimeoutError)
1701
- throw err;
1829
+ } catch (err2) {
1830
+ if (err2 instanceof BreakSignal || err2 instanceof ContinueSignal || err2 instanceof ReturnSignal)
1831
+ throw err2;
1832
+ if (err2 instanceof ScriptTimeoutError)
1833
+ throw err2;
1702
1834
  if (node.handler) {
1703
1835
  const catchScope = new Scope(scope);
1704
1836
  if (node.handler.param) {
1705
- const message = err instanceof Error ? { message: err.message } : err;
1837
+ const message = err2 instanceof Error ? { message: err2.message } : err2;
1706
1838
  bindPattern(node.handler.param, message, catchScope, "let");
1707
1839
  }
1708
1840
  execute(node.handler.body, catchScope);
@@ -2580,11 +2712,11 @@ function runBuildExecutor(code, palette, seed, options) {
2580
2712
  };
2581
2713
  try {
2582
2714
  runRestrictedScript(code, sandbox, { timeoutMs: timeout });
2583
- } catch (err) {
2584
- if (err instanceof ScriptTimeoutError) {
2715
+ } catch (err2) {
2716
+ if (err2 instanceof ScriptTimeoutError) {
2585
2717
  throw new Error(`Build code execution timed out after ${timeout}ms`);
2586
2718
  }
2587
- throw new Error(`Build code execution error: ${err?.message ?? String(err)}`);
2719
+ throw new Error(`Build code execution error: ${err2?.message ?? String(err2)}`);
2588
2720
  }
2589
2721
  if (parts.length === 0) {
2590
2722
  throw new Error("Build code produced no parts. Make sure to call part(), wall(), floor(), etc.");
@@ -2843,7 +2975,7 @@ var init_opencloud_client = __esm({
2843
2975
  if (result.error) {
2844
2976
  throw new Error(`Asset upload failed: ${result.error.message}`);
2845
2977
  }
2846
- await new Promise((resolve4) => setTimeout(resolve4, intervalMs));
2978
+ await new Promise((resolve5) => setTimeout(resolve5, intervalMs));
2847
2979
  }
2848
2980
  throw new Error(`Asset upload timed out after ${maxAttempts * intervalMs / 1e3}s. Operation ID: ${operationId}`);
2849
2981
  }
@@ -2964,7 +3096,7 @@ function isRecord(value) {
2964
3096
  const record = value;
2965
3097
  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";
2966
3098
  }
2967
- var REGISTRY_VERSION, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, EVENT_RETENTION_DAYS, ManagedInstanceRegistry;
3099
+ var REGISTRY_VERSION, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, EVENT_RETENTION_DAYS, TERMINAL_RECORD_RETENTION_MS, ManagedInstanceRegistry;
2968
3100
  var init_managed_instance_registry = __esm({
2969
3101
  "../core/dist/managed-instance-registry.js"() {
2970
3102
  "use strict";
@@ -2973,6 +3105,7 @@ var init_managed_instance_registry = __esm({
2973
3105
  LOCK_RETRY_MS = 25;
2974
3106
  LOCK_TIMEOUT_MS = 5e3;
2975
3107
  EVENT_RETENTION_DAYS = 2;
3108
+ TERMINAL_RECORD_RETENTION_MS = 24 * 60 * 60 * 1e3;
2976
3109
  ManagedInstanceRegistry = class {
2977
3110
  dir;
2978
3111
  constructor(dir = defaultManagedInstanceRegistryDir()) {
@@ -3000,12 +3133,21 @@ var init_managed_instance_registry = __esm({
3000
3133
  findAnyByInstanceId(instanceId) {
3001
3134
  return this.withLock(() => this.readRecordsUnlocked().find((record) => record.instanceId === instanceId));
3002
3135
  }
3136
+ findAnyByRecordId(recordId, options) {
3137
+ return this.withLock(() => {
3138
+ this.sweepUnlocked(options);
3139
+ return this.readRecordsUnlocked().find((record) => record.recordId === recordId);
3140
+ });
3141
+ }
3003
3142
  listOpen(options) {
3004
3143
  return this.withLock(() => {
3005
3144
  this.sweepUnlocked(options);
3006
3145
  return this.readOpenRecordsUnlocked();
3007
3146
  });
3008
3147
  }
3148
+ listOpenUnchecked() {
3149
+ return this.withLock(() => this.readOpenRecordsUnlocked());
3150
+ }
3009
3151
  markClosed(recordId, closedAt = Date.now()) {
3010
3152
  this.withLock(() => {
3011
3153
  const record = this.readRecordUnlocked(recordId);
@@ -3177,41 +3319,52 @@ var init_managed_instance_registry = __esm({
3177
3319
  }, now);
3178
3320
  continue;
3179
3321
  }
3180
- if (parsed.closedAt !== void 0) {
3181
- fs.rmSync(file, { force: true });
3182
- this.appendEventUnlocked({
3183
- event: "registry_pruned_closed_record",
3184
- recordId: parsed.recordId,
3185
- instanceId: parsed.instanceId,
3186
- source: parsed.source,
3187
- reason: "closed_at_present",
3188
- action: "deleted_record"
3189
- }, now);
3322
+ const terminalAt = parsed.closedAt ?? parsed.exitedAt;
3323
+ if (terminalAt !== void 0) {
3324
+ if (now - terminalAt > TERMINAL_RECORD_RETENTION_MS) {
3325
+ fs.rmSync(file, { force: true });
3326
+ this.appendEventUnlocked({
3327
+ event: "registry_pruned_terminal_record",
3328
+ recordId: parsed.recordId,
3329
+ instanceId: parsed.instanceId,
3330
+ source: parsed.source,
3331
+ reason: "terminal_retention_expired",
3332
+ action: "deleted_record"
3333
+ }, now);
3334
+ }
3190
3335
  continue;
3191
3336
  }
3192
3337
  if (parsed.bootId !== options.currentBootId) {
3193
3338
  this.cleanupRecord(options, parsed);
3194
- fs.rmSync(file, { force: true });
3339
+ parsed.state = parsed.state === "failed" ? "failed" : "exited";
3340
+ parsed.exitedAt = now;
3341
+ parsed.closedAt = now;
3342
+ parsed.failureReason ??= "Studio process belongs to a previous host boot.";
3343
+ this.writeRecordUnlocked(parsed);
3195
3344
  this.appendEventUnlocked({
3196
- event: "registry_pruned_previous_boot",
3345
+ event: "registry_marked_previous_boot_exited",
3197
3346
  recordId: parsed.recordId,
3198
3347
  instanceId: parsed.instanceId,
3199
3348
  source: parsed.source,
3200
3349
  reason: "boot_id_changed",
3201
- action: "deleted_record_and_cleaned_baseplate"
3350
+ action: "marked_exited_and_cleaned_baseplate"
3202
3351
  }, now);
3203
3352
  continue;
3204
3353
  }
3205
3354
  if (options.isProcessRunning && (parsed.nativeProcessId || parsed.spawnPid) && !options.isProcessRunning(parsed)) {
3206
3355
  this.cleanupRecord(options, parsed);
3207
- fs.rmSync(file, { force: true });
3356
+ parsed.state = parsed.state === "failed" ? "failed" : "exited";
3357
+ parsed.exitedAt = now;
3358
+ parsed.closedAt = now;
3359
+ parsed.failureReason ??= parsed.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.";
3360
+ this.writeRecordUnlocked(parsed);
3208
3361
  this.appendEventUnlocked({
3209
- event: "registry_pruned_stale_process",
3362
+ event: "registry_marked_process_exited",
3210
3363
  recordId: parsed.recordId,
3211
3364
  instanceId: parsed.instanceId,
3212
3365
  source: parsed.source,
3213
3366
  reason: "pid_not_running",
3214
- action: "deleted_record_and_cleaned_baseplate"
3367
+ action: "marked_exited_and_cleaned_baseplate"
3215
3368
  }, now);
3216
3369
  }
3217
3370
  }
@@ -3270,6 +3423,63 @@ function toStudioLaunchArg(arg) {
3270
3423
  return arg;
3271
3424
  return run("wslpath", ["-w", arg]);
3272
3425
  }
3426
+ function powershellStringLiteral(value) {
3427
+ return `'${value.replace(/'/g, "''")}'`;
3428
+ }
3429
+ function quoteWindowsCommandLineArg(value) {
3430
+ if (value.length > 0 && !/[\s"]/u.test(value))
3431
+ return value;
3432
+ let quoted = '"';
3433
+ let backslashes = 0;
3434
+ for (const char of value) {
3435
+ if (char === "\\") {
3436
+ backslashes += 1;
3437
+ continue;
3438
+ }
3439
+ if (char === '"') {
3440
+ quoted += "\\".repeat(backslashes * 2 + 1) + '"';
3441
+ backslashes = 0;
3442
+ continue;
3443
+ }
3444
+ quoted += "\\".repeat(backslashes) + char;
3445
+ backslashes = 0;
3446
+ }
3447
+ return `${quoted}${"\\".repeat(backslashes * 2)}"`;
3448
+ }
3449
+ function buildWindowsStudioStartScript(exe, args) {
3450
+ const windowsExe = toStudioLaunchArg(exe);
3451
+ const commandLine = args.map(quoteWindowsCommandLineArg).join(" ");
3452
+ return [
3453
+ "$psi = New-Object System.Diagnostics.ProcessStartInfo",
3454
+ `$psi.FileName = ${powershellStringLiteral(windowsExe)}`,
3455
+ `$psi.Arguments = ${powershellStringLiteral(commandLine)}`,
3456
+ // With UseShellExecute=false, Studio inherits the synchronous
3457
+ // powershell.exe invocation's stdout/stderr pipe handles under WSL. Those
3458
+ // handles keep execFileSync waiting until Studio exits even though
3459
+ // PowerShell already printed the PID. Shell execution prevents that
3460
+ // inheritance while Process.Start still returns the native Studio PID.
3461
+ "$psi.UseShellExecute = $true",
3462
+ "$studio = [System.Diagnostics.Process]::Start($psi)",
3463
+ 'if ($null -eq $studio) { throw "Roblox Studio process did not start." }'
3464
+ ].join("; ");
3465
+ }
3466
+ function spawnWindowsStudioFromWsl(exe, args) {
3467
+ const script = buildWindowsStudioStartScript(exe, args);
3468
+ const output = powershell(`${script}; [PSCustomObject]@{ pid = $studio.Id; started = $studio.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() } | ConvertTo-Json -Compress`);
3469
+ const parsed = JSON.parse(output);
3470
+ const nativePid = Number(parsed.pid);
3471
+ const nativeStartedAt = typeof parsed.started === "string" && /^\d+$/u.test(parsed.started) ? parsed.started : void 0;
3472
+ if (!Number.isSafeInteger(nativePid) || nativePid <= 0) {
3473
+ throw new Error(`Could not determine the Windows Studio process id from: ${nativePid}`);
3474
+ }
3475
+ return {
3476
+ pid: nativePid,
3477
+ nativePid,
3478
+ nativeStartedAt,
3479
+ unref: () => {
3480
+ }
3481
+ };
3482
+ }
3273
3483
  function resolveEntrypointDir() {
3274
3484
  const entrypoint = process.argv[1];
3275
3485
  if (!entrypoint)
@@ -3301,8 +3511,8 @@ function isProcessAlive(pid) {
3301
3511
  try {
3302
3512
  process.kill(pid, 0);
3303
3513
  return true;
3304
- } catch (err) {
3305
- return err.code === "EPERM";
3514
+ } catch (err2) {
3515
+ return err2.code === "EPERM";
3306
3516
  }
3307
3517
  }
3308
3518
  function sweepStaleBaseplateFiles() {
@@ -3395,7 +3605,7 @@ function listStudioProcesses() {
3395
3605
  return [];
3396
3606
  let out = "";
3397
3607
  try {
3398
- out = powershell("Get-Process RobloxStudioBeta -ErrorAction SilentlyContinue | Select-Object Id,Name,Path,MainWindowTitle | ConvertTo-Json -Compress");
3608
+ 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");
3399
3609
  } catch {
3400
3610
  return [];
3401
3611
  }
@@ -3459,7 +3669,7 @@ function buildStudioLaunchArgs(options) {
3459
3669
  }
3460
3670
  }
3461
3671
  function delay(ms) {
3462
- return new Promise((resolve4) => setTimeout(resolve4, ms));
3672
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
3463
3673
  }
3464
3674
  function basenameAny(filePath) {
3465
3675
  return path2.basename(filePath.replace(/\\/g, "/"));
@@ -3477,6 +3687,8 @@ var init_studio_instance_manager = __esm({
3477
3687
  StudioInstanceManager = class {
3478
3688
  managedByInstanceId = /* @__PURE__ */ new Map();
3479
3689
  pending = /* @__PURE__ */ new Set();
3690
+ monitors = /* @__PURE__ */ new Map();
3691
+ connectionTimers = /* @__PURE__ */ new Map();
3480
3692
  registry;
3481
3693
  processAdapter;
3482
3694
  constructor(options = {}) {
@@ -3485,6 +3697,9 @@ var init_studio_instance_manager = __esm({
3485
3697
  }
3486
3698
  list() {
3487
3699
  this.sweepRegistry();
3700
+ for (const record of [...this.managedByInstanceId.values(), ...this.pending]) {
3701
+ this.refresh(record);
3702
+ }
3488
3703
  const records = [...this.managedByInstanceId.values(), ...this.pending];
3489
3704
  for (const registryRecord of this.registry.listOpen(this.registrySweepOptions())) {
3490
3705
  const record = this.fromRegistryRecord(registryRecord);
@@ -3493,27 +3708,82 @@ var init_studio_instance_manager = __esm({
3493
3708
  }
3494
3709
  records.push(record);
3495
3710
  }
3496
- return records.filter((instance, index, all) => all.indexOf(instance) === index);
3711
+ return records.filter((record) => record.closedAt === void 0).filter((instance, index, all) => all.indexOf(instance) === index);
3497
3712
  }
3498
3713
  get(instanceId) {
3499
3714
  this.sweepRegistry();
3500
3715
  const memoryRecord = this.managedByInstanceId.get(instanceId);
3501
3716
  if (memoryRecord)
3502
- return memoryRecord;
3503
- const registryRecord = this.registry.findOpenByInstanceId(instanceId, this.registrySweepOptions());
3504
- return registryRecord ? this.fromRegistryRecord(registryRecord) : void 0;
3717
+ return this.refresh(memoryRecord);
3718
+ const registryRecord = this.registry.findAnyByInstanceId(instanceId);
3719
+ return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord)) : void 0;
3720
+ }
3721
+ getByLaunchId(launchId) {
3722
+ this.sweepRegistry();
3723
+ const memoryRecord = [...this.managedByInstanceId.values(), ...this.pending].find((record) => record.recordId === launchId);
3724
+ if (memoryRecord)
3725
+ return this.refresh(memoryRecord);
3726
+ const registryRecord = this.registry.findAnyByRecordId(launchId, this.registrySweepOptions());
3727
+ return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord)) : void 0;
3728
+ }
3729
+ pendingLaunches() {
3730
+ const now = Date.now();
3731
+ const records = [...this.pending];
3732
+ for (const registryRecord of this.registry.listOpenUnchecked()) {
3733
+ if (registryRecord.bootId !== this.getCurrentBootId())
3734
+ continue;
3735
+ if (records.some((record) => record.recordId === registryRecord.recordId))
3736
+ continue;
3737
+ records.push(this.fromRegistryRecord(registryRecord));
3738
+ }
3739
+ return records.filter((record) => record.instanceId === void 0).filter((record) => record.state === "launching").filter((record) => record.connectionDeadlineAt === void 0 || record.connectionDeadlineAt > now);
3505
3740
  }
3506
3741
  attachInstanceId(record, instanceId) {
3742
+ this.refresh(record);
3743
+ if (record.closedAt !== void 0 || record.state === "failed" || record.state === "exited")
3744
+ return;
3745
+ if (record.instanceId && record.instanceId !== instanceId)
3746
+ return;
3507
3747
  record.instanceId = instanceId;
3748
+ record.state = "connected";
3749
+ record.connectedAt = record.connectedAt ?? Date.now();
3750
+ this.clearConnectionTimer(record);
3508
3751
  this.pending.delete(record);
3509
3752
  this.managedByInstanceId.set(instanceId, record);
3510
- if (record.recordId) {
3511
- this.registry.attachInstanceId(record.recordId, instanceId);
3753
+ this.persist(record);
3754
+ }
3755
+ markFailed(record, reason) {
3756
+ this.refresh(record);
3757
+ if (record.closedAt !== void 0 || record.state !== "launching")
3758
+ return record;
3759
+ record.state = "failed";
3760
+ record.failedAt = Date.now();
3761
+ record.failureReason = reason;
3762
+ this.clearConnectionTimer(record);
3763
+ this.persist(record);
3764
+ return record;
3765
+ }
3766
+ refresh(record) {
3767
+ if (record.closedAt !== void 0)
3768
+ return record;
3769
+ const processId = record.nativeProcessId ?? record.spawnPid;
3770
+ const studioProcess = processId ? this.findProcessById(processId) : void 0;
3771
+ if (processId && (!studioProcess || !this.verifyProcessForRecord(record, studioProcess))) {
3772
+ 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.");
3512
3773
  }
3774
+ if (record.state === "launching" && record.connectionDeadlineAt !== void 0 && Date.now() >= record.connectionDeadlineAt) {
3775
+ record.state = "failed";
3776
+ record.failedAt = Date.now();
3777
+ record.failureReason = "Studio launched, but the MCP plugin did not connect before timeout.";
3778
+ this.clearConnectionTimer(record);
3779
+ this.persist(record);
3780
+ }
3781
+ return record;
3513
3782
  }
3514
3783
  async launch(options) {
3515
3784
  this.sweepRegistry();
3516
3785
  const preparedOptions = prepareStudioLaunchOptions(options);
3786
+ const bootId = this.getCurrentBootId();
3517
3787
  const before = new Set(this.listStudioProcesses().map((proc2) => proc2.Id));
3518
3788
  const exe = this.processAdapter.resolveStudioExe?.() ?? resolveStudioExe();
3519
3789
  const args = buildStudioLaunchArgs(preparedOptions).map(toStudioLaunchArg);
@@ -3522,11 +3792,35 @@ var init_studio_instance_manager = __esm({
3522
3792
  detached: true,
3523
3793
  stdio: "ignore"
3524
3794
  };
3525
- const proc = this.processAdapter.spawnStudio ? this.processAdapter.spawnStudio(exe, args, spawnOptions) : spawn(exe, args, spawnOptions);
3526
- proc.unref();
3795
+ let proc;
3796
+ try {
3797
+ if (this.processAdapter.spawnStudio) {
3798
+ proc = this.processAdapter.spawnStudio(exe, args, spawnOptions);
3799
+ } else if (isWsl()) {
3800
+ proc = spawnWindowsStudioFromWsl(exe, args);
3801
+ } else {
3802
+ const child = spawn(exe, args, spawnOptions);
3803
+ proc = {
3804
+ pid: child.pid,
3805
+ nativePid: child.pid,
3806
+ unref: () => child.unref(),
3807
+ onExit: (listener) => {
3808
+ child.once("exit", listener);
3809
+ },
3810
+ onError: (listener) => {
3811
+ child.once("error", listener);
3812
+ }
3813
+ };
3814
+ }
3815
+ } catch (error) {
3816
+ cleanupManagedBaseplateFiles({ source: preparedOptions.source, localPlaceFile: preparedOptions.localPlaceFile });
3817
+ throw error;
3818
+ }
3527
3819
  const record = {
3528
3820
  recordId: randomUUID(),
3529
3821
  source: options.source,
3822
+ nativeProcessId: proc.nativePid,
3823
+ nativeProcessStartedAt: proc.nativeStartedAt,
3530
3824
  spawnPid: proc.pid,
3531
3825
  exe,
3532
3826
  args,
@@ -3535,29 +3829,74 @@ var init_studio_instance_manager = __esm({
3535
3829
  placeVersion: preparedOptions.placeVersion,
3536
3830
  localPlaceFile: preparedOptions.localPlaceFile,
3537
3831
  launchedAt: Date.now(),
3832
+ connectionDeadlineAt: Date.now() + (options.connectionTimeoutMs ?? 12e4),
3833
+ state: "launching",
3538
3834
  ownerPid: process.pid,
3539
- bootId: this.getCurrentBootId(),
3835
+ bootId,
3540
3836
  deleteLocalPlaceFileOnClose: options.source === "baseplate"
3541
3837
  };
3542
3838
  this.pending.add(record);
3543
- this.registry.upsert(this.toRegistryRecord(record));
3839
+ try {
3840
+ this.persist(record);
3841
+ } catch (error) {
3842
+ this.pending.delete(record);
3843
+ const processId = record.nativeProcessId ?? record.spawnPid;
3844
+ let stopError;
3845
+ if (processId) {
3846
+ try {
3847
+ this.closeProcess(processId);
3848
+ } catch (caught) {
3849
+ stopError = caught;
3850
+ }
3851
+ }
3852
+ cleanupManagedBaseplateFiles(record);
3853
+ const detail = error instanceof Error ? error.message : String(error);
3854
+ const cleanupDetail = stopError ? ` The newly launched process could not be stopped: ${stopError instanceof Error ? stopError.message : String(stopError)}` : "";
3855
+ throw new Error(`Studio launched, but its managed-instance record could not be persisted: ${detail}.${cleanupDetail}`);
3856
+ }
3857
+ proc.unref();
3858
+ proc.onExit?.((code, signal) => {
3859
+ 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.");
3860
+ });
3861
+ proc.onError?.((error) => {
3862
+ this.markFailed(record, `Studio process failed to start: ${error.message}`);
3863
+ });
3544
3864
  const deadline = Date.now() + 5e3;
3545
3865
  while (Date.now() < deadline && record.nativeProcessId === void 0) {
3546
3866
  const created = this.listStudioProcesses().find((candidate) => !before.has(candidate.Id));
3547
3867
  if (created) {
3548
3868
  record.nativeProcessId = created.Id;
3549
- this.registry.upsert(this.toRegistryRecord(record));
3869
+ record.nativeProcessStartedAt = created.StartTimeUtcFileTime;
3870
+ this.persist(record);
3550
3871
  break;
3551
3872
  }
3552
3873
  await delay(250);
3553
3874
  }
3554
3875
  if (record.nativeProcessId === void 0 && process.platform !== "win32" && !isWsl()) {
3555
3876
  record.nativeProcessId = proc.pid;
3556
- this.registry.upsert(this.toRegistryRecord(record));
3877
+ this.persist(record);
3557
3878
  }
3879
+ if (record.nativeProcessId !== void 0 && record.nativeProcessStartedAt === void 0) {
3880
+ const nativeProcess = this.findProcessById(record.nativeProcessId);
3881
+ if (nativeProcess?.StartTimeUtcFileTime !== void 0) {
3882
+ record.nativeProcessStartedAt = nativeProcess.StartTimeUtcFileTime;
3883
+ this.persist(record);
3884
+ }
3885
+ }
3886
+ this.startMonitor(record);
3558
3887
  return record;
3559
3888
  }
3889
+ closeByLaunchId(launchId) {
3890
+ const record = this.getByLaunchId(launchId);
3891
+ if (!record)
3892
+ return { status: "not_found", launchId };
3893
+ if (record.closedAt !== void 0) {
3894
+ return { status: "already_closed", launchId, instanceId: record.instanceId };
3895
+ }
3896
+ return this.close(record);
3897
+ }
3560
3898
  closeByInstanceId(instanceId) {
3899
+ this.sweepRegistry();
3561
3900
  const memoryRecord = this.managedByInstanceId.get(instanceId);
3562
3901
  if (memoryRecord)
3563
3902
  return this.close(memoryRecord);
@@ -3568,33 +3907,24 @@ var init_studio_instance_manager = __esm({
3568
3907
  }
3569
3908
  if (registryRecord.closedAt !== void 0) {
3570
3909
  this.cleanupManagedRecord(registryRecord);
3571
- this.registry.delete(registryRecord.recordId);
3572
3910
  this.registry.logEvent({
3573
3911
  event: "registry_close_already_stopped",
3574
3912
  recordId: registryRecord.recordId,
3575
3913
  instanceId: registryRecord.instanceId,
3576
3914
  source: registryRecord.source,
3577
3915
  reason: "closed_at_present",
3578
- action: "deleted_record_and_cleaned_baseplate"
3579
- });
3580
- return { status: "already_closed", instanceId };
3581
- }
3582
- if (registryRecord.bootId !== this.getCurrentBootId()) {
3583
- this.cleanupManagedRecord(registryRecord);
3584
- this.registry.delete(registryRecord.recordId);
3585
- this.registry.logEvent({
3586
- event: "registry_pruned_previous_boot",
3587
- recordId: registryRecord.recordId,
3588
- instanceId: registryRecord.instanceId,
3589
- source: registryRecord.source,
3590
- reason: "boot_id_changed",
3591
- action: "deleted_record_and_cleaned_baseplate"
3916
+ action: "retained_terminal_record_and_cleaned_baseplate"
3592
3917
  });
3593
- return { status: "already_closed", instanceId };
3918
+ return { status: "already_closed", launchId: registryRecord.recordId, instanceId };
3594
3919
  }
3595
3920
  return this.close(this.fromRegistryRecord(registryRecord));
3596
3921
  }
3597
3922
  close(record) {
3923
+ this.stopMonitor(record);
3924
+ this.refresh(record);
3925
+ if (record.closedAt !== void 0) {
3926
+ return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
3927
+ }
3598
3928
  const processId = record.nativeProcessId ?? record.spawnPid;
3599
3929
  if (!processId) {
3600
3930
  throw new Error(`Cannot close ${record.instanceId ?? "Studio launch"} because its process id was not detected.`);
@@ -3602,8 +3932,7 @@ var init_studio_instance_manager = __esm({
3602
3932
  const studioProcess = this.findProcessById(processId);
3603
3933
  if (!studioProcess) {
3604
3934
  this.cleanupManagedRecord(record);
3605
- this.markClosedInMemory(record);
3606
- this.markClosedInRegistry(record);
3935
+ this.markProcessExited(record, void 0, record.failureReason);
3607
3936
  this.registry.logEvent({
3608
3937
  event: "registry_close_already_stopped",
3609
3938
  recordId: record.recordId,
@@ -3612,7 +3941,7 @@ var init_studio_instance_manager = __esm({
3612
3941
  reason: "pid_not_running",
3613
3942
  action: "marked_closed_and_cleaned_baseplate"
3614
3943
  });
3615
- return { status: "already_closed", instanceId: record.instanceId };
3944
+ return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
3616
3945
  }
3617
3946
  if (!this.verifyProcessForRecord(record, studioProcess)) {
3618
3947
  this.registry.logEvent({
@@ -3638,15 +3967,18 @@ var init_studio_instance_manager = __esm({
3638
3967
  action: "marked_closed_and_cleaned_baseplate"
3639
3968
  });
3640
3969
  this.cleanupManagedRecord(record);
3641
- this.markClosedInMemory(record);
3642
- this.markClosedInRegistry(record);
3643
- return { status: "already_closed", instanceId: record.instanceId };
3644
- }
3645
- record.closedAt = Date.now();
3970
+ this.markProcessExited(record, void 0, record.failureReason);
3971
+ return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
3972
+ }
3973
+ const closedAt = Date.now();
3974
+ record.closedAt = closedAt;
3975
+ record.exitedAt = record.exitedAt ?? closedAt;
3976
+ if (record.state !== "failed")
3977
+ record.state = "exited";
3646
3978
  this.cleanupManagedRecord(record);
3647
3979
  this.markClosedInMemory(record);
3648
- this.markClosedInRegistry(record);
3649
- return { status: "closed", instanceId: record.instanceId };
3980
+ this.persist(record);
3981
+ return { status: "closed", launchId: record.recordId, instanceId: record.instanceId };
3650
3982
  }
3651
3983
  closeConnectedInstance(instance) {
3652
3984
  const process2 = this.findProcessForConnectedInstance(instance);
@@ -3721,6 +4053,9 @@ var init_studio_instance_manager = __esm({
3721
4053
  const processName = `${studioProcess.Name ?? ""} ${studioProcess.Path ?? ""}`.toLowerCase();
3722
4054
  if (!processName.includes("robloxstudio"))
3723
4055
  return false;
4056
+ if (record.nativeProcessStartedAt !== void 0 && studioProcess.StartTimeUtcFileTime !== record.nativeProcessStartedAt) {
4057
+ return false;
4058
+ }
3724
4059
  const processId = record.nativeProcessId ?? record.spawnPid;
3725
4060
  if (record.spawnPid && record.spawnPid === processId && studioProcess.Id === processId)
3726
4061
  return true;
@@ -3746,10 +4081,64 @@ var init_studio_instance_manager = __esm({
3746
4081
  if (record.instanceId)
3747
4082
  this.managedByInstanceId.delete(record.instanceId);
3748
4083
  this.pending.delete(record);
4084
+ this.stopMonitor(record);
4085
+ }
4086
+ markProcessExited(record, exitCode, reason) {
4087
+ if (record.closedAt !== void 0)
4088
+ return record;
4089
+ const exitedAt = Date.now();
4090
+ record.exitedAt = exitedAt;
4091
+ record.closedAt = exitedAt;
4092
+ if (record.state !== "failed")
4093
+ record.state = "exited";
4094
+ if (exitCode !== void 0)
4095
+ record.exitCode = exitCode;
4096
+ if (reason)
4097
+ record.failureReason = reason;
4098
+ this.cleanupManagedRecord(record);
4099
+ this.markClosedInMemory(record);
4100
+ this.persist(record);
4101
+ return record;
3749
4102
  }
3750
- markClosedInRegistry(record) {
3751
- if (record.recordId)
3752
- this.registry.markClosed(record.recordId, record.closedAt ?? Date.now());
4103
+ startMonitor(record) {
4104
+ if (!record.recordId || record.closedAt !== void 0 || this.monitors.has(record.recordId))
4105
+ return;
4106
+ if (record.state === "launching" && record.connectionDeadlineAt !== void 0) {
4107
+ const timeout = setTimeout(() => {
4108
+ this.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
4109
+ }, Math.max(0, record.connectionDeadlineAt - Date.now()));
4110
+ if (typeof timeout === "object" && "unref" in timeout)
4111
+ timeout.unref();
4112
+ this.connectionTimers.set(record.recordId, timeout);
4113
+ }
4114
+ const timer = setInterval(() => {
4115
+ this.refresh(record);
4116
+ if (record.closedAt !== void 0)
4117
+ this.stopMonitor(record);
4118
+ }, 5e3);
4119
+ if (typeof timer === "object" && "unref" in timer)
4120
+ timer.unref();
4121
+ this.monitors.set(record.recordId, timer);
4122
+ }
4123
+ stopMonitor(record) {
4124
+ if (!record.recordId)
4125
+ return;
4126
+ const timer = this.monitors.get(record.recordId);
4127
+ if (timer)
4128
+ clearInterval(timer);
4129
+ this.monitors.delete(record.recordId);
4130
+ this.clearConnectionTimer(record);
4131
+ }
4132
+ clearConnectionTimer(record) {
4133
+ if (!record.recordId)
4134
+ return;
4135
+ const timer = this.connectionTimers.get(record.recordId);
4136
+ if (timer)
4137
+ clearTimeout(timer);
4138
+ this.connectionTimers.delete(record.recordId);
4139
+ }
4140
+ persist(record) {
4141
+ this.registry.upsert(this.toRegistryRecord(record));
3753
4142
  }
3754
4143
  toRegistryRecord(record) {
3755
4144
  if (!record.recordId)
@@ -3762,6 +4151,7 @@ var init_studio_instance_manager = __esm({
3762
4151
  instanceId: record.instanceId,
3763
4152
  source: record.source,
3764
4153
  nativeProcessId: record.nativeProcessId,
4154
+ nativeProcessStartedAt: record.nativeProcessStartedAt,
3765
4155
  spawnPid: record.spawnPid,
3766
4156
  exe: record.exe,
3767
4157
  args: record.args,
@@ -3771,18 +4161,26 @@ var init_studio_instance_manager = __esm({
3771
4161
  localPlaceFile: record.localPlaceFile,
3772
4162
  deleteLocalPlaceFileOnClose: record.deleteLocalPlaceFileOnClose,
3773
4163
  launchedAt: record.launchedAt,
3774
- attachedAt: record.instanceId ? Date.now() : void 0,
4164
+ attachedAt: record.connectedAt,
4165
+ connectionDeadlineAt: record.connectionDeadlineAt,
4166
+ state: record.state,
4167
+ failedAt: record.failedAt,
4168
+ exitedAt: record.exitedAt,
4169
+ exitCode: record.exitCode,
4170
+ failureReason: record.failureReason,
3775
4171
  closedAt: record.closedAt,
3776
4172
  ownerPid: record.ownerPid,
3777
4173
  bootId: record.bootId
3778
4174
  };
3779
4175
  }
3780
4176
  fromRegistryRecord(record) {
4177
+ const state = record.state ?? (record.closedAt !== void 0 ? "exited" : record.instanceId ? "connected" : "launching");
3781
4178
  return {
3782
4179
  recordId: record.recordId,
3783
4180
  source: record.source,
3784
4181
  instanceId: record.instanceId,
3785
4182
  nativeProcessId: record.nativeProcessId,
4183
+ nativeProcessStartedAt: record.nativeProcessStartedAt,
3786
4184
  spawnPid: record.spawnPid,
3787
4185
  exe: record.exe,
3788
4186
  args: record.args,
@@ -3791,6 +4189,13 @@ var init_studio_instance_manager = __esm({
3791
4189
  placeVersion: record.placeVersion,
3792
4190
  localPlaceFile: record.localPlaceFile,
3793
4191
  launchedAt: record.launchedAt,
4192
+ connectionDeadlineAt: record.connectionDeadlineAt ?? (state === "launching" ? record.launchedAt + 12e4 : void 0),
4193
+ state,
4194
+ connectedAt: record.attachedAt,
4195
+ failedAt: record.failedAt,
4196
+ exitedAt: record.exitedAt,
4197
+ exitCode: record.exitCode,
4198
+ failureReason: record.failureReason,
3794
4199
  closedAt: record.closedAt,
3795
4200
  ownerPid: record.ownerPid,
3796
4201
  bootId: record.bootId,
@@ -3976,6 +4381,982 @@ var init_image_decode = __esm({
3976
4381
  }
3977
4382
  });
3978
4383
 
4384
+ // ../../node_modules/fzstd/esm/index.mjs
4385
+ function decompress(dat, buf) {
4386
+ var bufs = [], nb = +!buf;
4387
+ var bt = 0, ol = 0;
4388
+ for (; dat.length; ) {
4389
+ var st = rzfh(dat, nb || buf);
4390
+ if (typeof st == "object") {
4391
+ if (nb) {
4392
+ buf = null;
4393
+ if (st.w.length == st.u) {
4394
+ bufs.push(buf = st.w);
4395
+ ol += st.u;
4396
+ }
4397
+ } else {
4398
+ bufs.push(buf);
4399
+ st.e = 0;
4400
+ }
4401
+ for (; !st.l; ) {
4402
+ var blk = rzb(dat, st, buf);
4403
+ if (!blk)
4404
+ err(5);
4405
+ if (buf)
4406
+ st.e = st.y;
4407
+ else {
4408
+ bufs.push(blk);
4409
+ ol += blk.length;
4410
+ cpw(st.w, 0, blk.length);
4411
+ st.w.set(blk, st.w.length - blk.length);
4412
+ }
4413
+ }
4414
+ bt = st.b + st.c * 4;
4415
+ } else
4416
+ bt = st;
4417
+ dat = dat.subarray(bt);
4418
+ }
4419
+ return cct(bufs, ol);
4420
+ }
4421
+ 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;
4422
+ var init_esm = __esm({
4423
+ "../../node_modules/fzstd/esm/index.mjs"() {
4424
+ "use strict";
4425
+ ab = ArrayBuffer;
4426
+ u8 = Uint8Array;
4427
+ u16 = Uint16Array;
4428
+ i16 = Int16Array;
4429
+ i32 = Int32Array;
4430
+ slc = function(v, s, e) {
4431
+ if (u8.prototype.slice)
4432
+ return u8.prototype.slice.call(v, s, e);
4433
+ if (s == null || s < 0)
4434
+ s = 0;
4435
+ if (e == null || e > v.length)
4436
+ e = v.length;
4437
+ var n = new u8(e - s);
4438
+ n.set(v.subarray(s, e));
4439
+ return n;
4440
+ };
4441
+ fill = function(v, n, s, e) {
4442
+ if (u8.prototype.fill)
4443
+ return u8.prototype.fill.call(v, n, s, e);
4444
+ if (s == null || s < 0)
4445
+ s = 0;
4446
+ if (e == null || e > v.length)
4447
+ e = v.length;
4448
+ for (; s < e; ++s)
4449
+ v[s] = n;
4450
+ return v;
4451
+ };
4452
+ cpw = function(v, t, s, e) {
4453
+ if (u8.prototype.copyWithin)
4454
+ return u8.prototype.copyWithin.call(v, t, s, e);
4455
+ if (s == null || s < 0)
4456
+ s = 0;
4457
+ if (e == null || e > v.length)
4458
+ e = v.length;
4459
+ while (s < e) {
4460
+ v[t++] = v[s++];
4461
+ }
4462
+ };
4463
+ ec = [
4464
+ "invalid zstd data",
4465
+ "window size too large (>2046MB)",
4466
+ "invalid block type",
4467
+ "FSE accuracy too high",
4468
+ "match distance too far back",
4469
+ "unexpected EOF"
4470
+ ];
4471
+ err = function(ind, msg, nt) {
4472
+ var e = new Error(msg || ec[ind]);
4473
+ e.code = ind;
4474
+ if (Error.captureStackTrace)
4475
+ Error.captureStackTrace(e, err);
4476
+ if (!nt)
4477
+ throw e;
4478
+ return e;
4479
+ };
4480
+ rb = function(d, b, n) {
4481
+ var i = 0, o = 0;
4482
+ for (; i < n; ++i)
4483
+ o |= d[b++] << (i << 3);
4484
+ return o;
4485
+ };
4486
+ b4 = function(d, b) {
4487
+ return (d[b] | d[b + 1] << 8 | d[b + 2] << 16 | d[b + 3] << 24) >>> 0;
4488
+ };
4489
+ rzfh = function(dat, w) {
4490
+ var n3 = dat[0] | dat[1] << 8 | dat[2] << 16;
4491
+ if (n3 == 3126568 && dat[3] == 253) {
4492
+ var flg = dat[4];
4493
+ var ss = flg >> 5 & 1, cc = flg >> 2 & 1, df = flg & 3, fcf = flg >> 6;
4494
+ if (flg & 8)
4495
+ err(0);
4496
+ var bt = 6 - ss;
4497
+ var db = df == 3 ? 4 : df;
4498
+ var di = rb(dat, bt, db);
4499
+ bt += db;
4500
+ var fsb = fcf ? 1 << fcf : ss;
4501
+ var fss = rb(dat, bt, fsb) + (fcf == 1 && 256);
4502
+ var ws = fss;
4503
+ if (!ss) {
4504
+ var wb = 1 << 10 + (dat[5] >> 3);
4505
+ ws = wb + (wb >> 3) * (dat[5] & 7);
4506
+ }
4507
+ if (ws > 2145386496)
4508
+ err(1);
4509
+ var buf = new u8((w == 1 ? fss || ws : w ? 0 : ws) + 12);
4510
+ buf[0] = 1, buf[4] = 4, buf[8] = 8;
4511
+ return {
4512
+ b: bt + fsb,
4513
+ y: 0,
4514
+ l: 0,
4515
+ d: di,
4516
+ w: w && w != 1 ? w : buf.subarray(12),
4517
+ e: ws,
4518
+ o: new i32(buf.buffer, 0, 3),
4519
+ u: fss,
4520
+ c: cc,
4521
+ m: Math.min(131072, ws)
4522
+ };
4523
+ } else if ((n3 >> 4 | dat[3] << 20) == 25481893) {
4524
+ return b4(dat, 4) + 8;
4525
+ }
4526
+ err(0);
4527
+ };
4528
+ msb = function(val) {
4529
+ var bits = 0;
4530
+ for (; 1 << bits <= val; ++bits)
4531
+ ;
4532
+ return bits - 1;
4533
+ };
4534
+ rfse = function(dat, bt, mal) {
4535
+ var tpos = (bt << 3) + 4;
4536
+ var al = (dat[bt] & 15) + 5;
4537
+ if (al > mal)
4538
+ err(3);
4539
+ var sz = 1 << al;
4540
+ var probs = sz, sym = -1, re = -1, i = -1, ht = sz;
4541
+ var buf = new ab(512 + (sz << 2));
4542
+ var freq = new i16(buf, 0, 256);
4543
+ var dstate = new u16(buf, 0, 256);
4544
+ var nstate = new u16(buf, 512, sz);
4545
+ var bb1 = 512 + (sz << 1);
4546
+ var syms = new u8(buf, bb1, sz);
4547
+ var nbits = new u8(buf, bb1 + sz);
4548
+ while (sym < 255 && probs > 0) {
4549
+ var bits = msb(probs + 1);
4550
+ var cbt = tpos >> 3;
4551
+ var msk = (1 << bits + 1) - 1;
4552
+ var val = (dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16) >> (tpos & 7) & msk;
4553
+ var msk1fb = (1 << bits) - 1;
4554
+ var msv = msk - probs - 1;
4555
+ var sval = val & msk1fb;
4556
+ if (sval < msv)
4557
+ tpos += bits, val = sval;
4558
+ else {
4559
+ tpos += bits + 1;
4560
+ if (val > msk1fb)
4561
+ val -= msv;
4562
+ }
4563
+ freq[++sym] = --val;
4564
+ if (val == -1) {
4565
+ probs += val;
4566
+ syms[--ht] = sym;
4567
+ } else
4568
+ probs -= val;
4569
+ if (!val) {
4570
+ do {
4571
+ var rbt = tpos >> 3;
4572
+ re = (dat[rbt] | dat[rbt + 1] << 8) >> (tpos & 7) & 3;
4573
+ tpos += 2;
4574
+ sym += re;
4575
+ } while (re == 3);
4576
+ }
4577
+ }
4578
+ if (sym > 255 || probs)
4579
+ err(0);
4580
+ var sympos = 0;
4581
+ var sstep = (sz >> 1) + (sz >> 3) + 3;
4582
+ var smask = sz - 1;
4583
+ for (var s = 0; s <= sym; ++s) {
4584
+ var sf = freq[s];
4585
+ if (sf < 1) {
4586
+ dstate[s] = -sf;
4587
+ continue;
4588
+ }
4589
+ for (i = 0; i < sf; ++i) {
4590
+ syms[sympos] = s;
4591
+ do {
4592
+ sympos = sympos + sstep & smask;
4593
+ } while (sympos >= ht);
4594
+ }
4595
+ }
4596
+ if (sympos)
4597
+ err(0);
4598
+ for (i = 0; i < sz; ++i) {
4599
+ var ns = dstate[syms[i]]++;
4600
+ var nb = nbits[i] = al - msb(ns);
4601
+ nstate[i] = (ns << nb) - sz;
4602
+ }
4603
+ return [tpos + 7 >> 3, {
4604
+ b: al,
4605
+ s: syms,
4606
+ n: nbits,
4607
+ t: nstate
4608
+ }];
4609
+ };
4610
+ rhu = function(dat, bt) {
4611
+ var i = 0, wc = -1;
4612
+ var buf = new u8(292), hb = dat[bt];
4613
+ var hw = buf.subarray(0, 256);
4614
+ var rc = buf.subarray(256, 268);
4615
+ var ri = new u16(buf.buffer, 268);
4616
+ if (hb < 128) {
4617
+ var _a = rfse(dat, bt + 1, 6), ebt = _a[0], fdt = _a[1];
4618
+ bt += hb;
4619
+ var epos = ebt << 3;
4620
+ var lb = dat[bt];
4621
+ if (!lb)
4622
+ err(0);
4623
+ var st1 = 0, st2 = 0, btr1 = fdt.b, btr2 = btr1;
4624
+ var fpos = (++bt << 3) - 8 + msb(lb);
4625
+ for (; ; ) {
4626
+ fpos -= btr1;
4627
+ if (fpos < epos)
4628
+ break;
4629
+ var cbt = fpos >> 3;
4630
+ st1 += (dat[cbt] | dat[cbt + 1] << 8) >> (fpos & 7) & (1 << btr1) - 1;
4631
+ hw[++wc] = fdt.s[st1];
4632
+ fpos -= btr2;
4633
+ if (fpos < epos)
4634
+ break;
4635
+ cbt = fpos >> 3;
4636
+ st2 += (dat[cbt] | dat[cbt + 1] << 8) >> (fpos & 7) & (1 << btr2) - 1;
4637
+ hw[++wc] = fdt.s[st2];
4638
+ btr1 = fdt.n[st1];
4639
+ st1 = fdt.t[st1];
4640
+ btr2 = fdt.n[st2];
4641
+ st2 = fdt.t[st2];
4642
+ }
4643
+ if (++wc > 255)
4644
+ err(0);
4645
+ } else {
4646
+ wc = hb - 127;
4647
+ for (; i < wc; i += 2) {
4648
+ var byte = dat[++bt];
4649
+ hw[i] = byte >> 4;
4650
+ hw[i + 1] = byte & 15;
4651
+ }
4652
+ ++bt;
4653
+ }
4654
+ var wes = 0;
4655
+ for (i = 0; i < wc; ++i) {
4656
+ var wt = hw[i];
4657
+ if (wt > 11)
4658
+ err(0);
4659
+ wes += wt && 1 << wt - 1;
4660
+ }
4661
+ var mb = msb(wes) + 1;
4662
+ var ts = 1 << mb;
4663
+ var rem = ts - wes;
4664
+ if (rem & rem - 1)
4665
+ err(0);
4666
+ hw[wc++] = msb(rem) + 1;
4667
+ for (i = 0; i < wc; ++i) {
4668
+ var wt = hw[i];
4669
+ ++rc[hw[i] = wt && mb + 1 - wt];
4670
+ }
4671
+ var hbuf = new u8(ts << 1);
4672
+ var syms = hbuf.subarray(0, ts), nb = hbuf.subarray(ts);
4673
+ ri[mb] = 0;
4674
+ for (i = mb; i > 0; --i) {
4675
+ var pv = ri[i];
4676
+ fill(nb, i, pv, ri[i - 1] = pv + rc[i] * (1 << mb - i));
4677
+ }
4678
+ if (ri[0] != ts)
4679
+ err(0);
4680
+ for (i = 0; i < wc; ++i) {
4681
+ var bits = hw[i];
4682
+ if (bits) {
4683
+ var code = ri[bits];
4684
+ fill(syms, i, code, ri[bits] = code + (1 << mb - bits));
4685
+ }
4686
+ }
4687
+ return [bt, {
4688
+ n: nb,
4689
+ b: mb,
4690
+ s: syms
4691
+ }];
4692
+ };
4693
+ dllt = rfse(/* @__PURE__ */ new u8([
4694
+ 81,
4695
+ 16,
4696
+ 99,
4697
+ 140,
4698
+ 49,
4699
+ 198,
4700
+ 24,
4701
+ 99,
4702
+ 12,
4703
+ 33,
4704
+ 196,
4705
+ 24,
4706
+ 99,
4707
+ 102,
4708
+ 102,
4709
+ 134,
4710
+ 70,
4711
+ 146,
4712
+ 4
4713
+ ]), 0, 6)[1];
4714
+ dmlt = rfse(/* @__PURE__ */ new u8([
4715
+ 33,
4716
+ 20,
4717
+ 196,
4718
+ 24,
4719
+ 99,
4720
+ 140,
4721
+ 33,
4722
+ 132,
4723
+ 16,
4724
+ 66,
4725
+ 8,
4726
+ 33,
4727
+ 132,
4728
+ 16,
4729
+ 66,
4730
+ 8,
4731
+ 33,
4732
+ 68,
4733
+ 68,
4734
+ 68,
4735
+ 68,
4736
+ 68,
4737
+ 68,
4738
+ 68,
4739
+ 68,
4740
+ 36,
4741
+ 9
4742
+ ]), 0, 6)[1];
4743
+ doct = rfse(/* @__PURE__ */ new u8([
4744
+ 32,
4745
+ 132,
4746
+ 16,
4747
+ 66,
4748
+ 102,
4749
+ 70,
4750
+ 68,
4751
+ 68,
4752
+ 68,
4753
+ 68,
4754
+ 36,
4755
+ 73,
4756
+ 2
4757
+ ]), 0, 5)[1];
4758
+ b2bl = function(b, s) {
4759
+ var len = b.length, bl = new i32(len);
4760
+ for (var i = 0; i < len; ++i) {
4761
+ bl[i] = s;
4762
+ s += 1 << b[i];
4763
+ }
4764
+ return bl;
4765
+ };
4766
+ llb = /* @__PURE__ */ new u8((/* @__PURE__ */ new i32([
4767
+ 0,
4768
+ 0,
4769
+ 0,
4770
+ 0,
4771
+ 16843009,
4772
+ 50528770,
4773
+ 134678020,
4774
+ 202050057,
4775
+ 269422093
4776
+ ])).buffer, 0, 36);
4777
+ llbl = /* @__PURE__ */ b2bl(llb, 0);
4778
+ mlb = /* @__PURE__ */ new u8((/* @__PURE__ */ new i32([
4779
+ 0,
4780
+ 0,
4781
+ 0,
4782
+ 0,
4783
+ 0,
4784
+ 0,
4785
+ 0,
4786
+ 0,
4787
+ 16843009,
4788
+ 50528770,
4789
+ 117769220,
4790
+ 185207048,
4791
+ 252579084,
4792
+ 16
4793
+ ])).buffer, 0, 53);
4794
+ mlbl = /* @__PURE__ */ b2bl(mlb, 3);
4795
+ dhu = function(dat, out, hu) {
4796
+ var len = dat.length, ss = out.length, lb = dat[len - 1], msk = (1 << hu.b) - 1, eb = -hu.b;
4797
+ if (!lb)
4798
+ err(0);
4799
+ var st = 0, btr = hu.b, pos = (len << 3) - 8 + msb(lb) - btr, i = -1;
4800
+ for (; pos > eb && i < ss; ) {
4801
+ var cbt = pos >> 3;
4802
+ var val = (dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16) >> (pos & 7);
4803
+ st = (st << btr | val) & msk;
4804
+ out[++i] = hu.s[st];
4805
+ pos -= btr = hu.n[st];
4806
+ }
4807
+ if (pos != eb || i + 1 != ss)
4808
+ err(0);
4809
+ };
4810
+ dhu4 = function(dat, out, hu) {
4811
+ var bt = 6;
4812
+ var ss = out.length, sz1 = ss + 3 >> 2, sz2 = sz1 << 1, sz3 = sz1 + sz2;
4813
+ dhu(dat.subarray(bt, bt += dat[0] | dat[1] << 8), out.subarray(0, sz1), hu);
4814
+ dhu(dat.subarray(bt, bt += dat[2] | dat[3] << 8), out.subarray(sz1, sz2), hu);
4815
+ dhu(dat.subarray(bt, bt += dat[4] | dat[5] << 8), out.subarray(sz2, sz3), hu);
4816
+ dhu(dat.subarray(bt), out.subarray(sz3), hu);
4817
+ };
4818
+ rzb = function(dat, st, out) {
4819
+ var _a;
4820
+ var bt = st.b;
4821
+ var b0 = dat[bt], btype = b0 >> 1 & 3;
4822
+ st.l = b0 & 1;
4823
+ var sz = b0 >> 3 | dat[bt + 1] << 5 | dat[bt + 2] << 13;
4824
+ var ebt = (bt += 3) + sz;
4825
+ if (btype == 1) {
4826
+ if (bt >= dat.length)
4827
+ return;
4828
+ st.b = bt + 1;
4829
+ if (out) {
4830
+ fill(out, dat[bt], st.y, st.y += sz);
4831
+ return out;
4832
+ }
4833
+ return fill(new u8(sz), dat[bt]);
4834
+ }
4835
+ if (ebt > dat.length)
4836
+ return;
4837
+ if (btype == 0) {
4838
+ st.b = ebt;
4839
+ if (out) {
4840
+ out.set(dat.subarray(bt, ebt), st.y);
4841
+ st.y += sz;
4842
+ return out;
4843
+ }
4844
+ return slc(dat, bt, ebt);
4845
+ }
4846
+ if (btype == 2) {
4847
+ var b3 = dat[bt], lbt = b3 & 3, sf = b3 >> 2 & 3;
4848
+ var lss = b3 >> 4, lcs = 0, s4 = 0;
4849
+ if (lbt < 2) {
4850
+ if (sf & 1)
4851
+ lss |= dat[++bt] << 4 | (sf & 2 && dat[++bt] << 12);
4852
+ else
4853
+ lss = b3 >> 3;
4854
+ } else {
4855
+ s4 = sf;
4856
+ if (sf < 2)
4857
+ lss |= (dat[++bt] & 63) << 4, lcs = dat[bt] >> 6 | dat[++bt] << 2;
4858
+ else if (sf == 2)
4859
+ lss |= dat[++bt] << 4 | (dat[++bt] & 3) << 12, lcs = dat[bt] >> 2 | dat[++bt] << 6;
4860
+ else
4861
+ lss |= dat[++bt] << 4 | (dat[++bt] & 63) << 12, lcs = dat[bt] >> 6 | dat[++bt] << 2 | dat[++bt] << 10;
4862
+ }
4863
+ ++bt;
4864
+ var buf = out ? out.subarray(st.y, st.y + st.m) : new u8(st.m);
4865
+ var spl = buf.length - lss;
4866
+ if (lbt == 0)
4867
+ buf.set(dat.subarray(bt, bt += lss), spl);
4868
+ else if (lbt == 1)
4869
+ fill(buf, dat[bt++], spl);
4870
+ else {
4871
+ var hu = st.h;
4872
+ if (lbt == 2) {
4873
+ var hud = rhu(dat, bt);
4874
+ lcs += bt - (bt = hud[0]);
4875
+ st.h = hu = hud[1];
4876
+ } else if (!hu)
4877
+ err(0);
4878
+ (s4 ? dhu4 : dhu)(dat.subarray(bt, bt += lcs), buf.subarray(spl), hu);
4879
+ }
4880
+ var ns = dat[bt++];
4881
+ if (ns) {
4882
+ if (ns == 255)
4883
+ ns = (dat[bt++] | dat[bt++] << 8) + 32512;
4884
+ else if (ns > 127)
4885
+ ns = ns - 128 << 8 | dat[bt++];
4886
+ var scm = dat[bt++];
4887
+ if (scm & 3)
4888
+ err(0);
4889
+ var dts = [dmlt, doct, dllt];
4890
+ for (var i = 2; i > -1; --i) {
4891
+ var md = scm >> (i << 1) + 2 & 3;
4892
+ if (md == 1) {
4893
+ var rbuf = new u8([0, 0, dat[bt++]]);
4894
+ dts[i] = {
4895
+ s: rbuf.subarray(2, 3),
4896
+ n: rbuf.subarray(0, 1),
4897
+ t: new u16(rbuf.buffer, 0, 1),
4898
+ b: 0
4899
+ };
4900
+ } else if (md == 2) {
4901
+ _a = rfse(dat, bt, 9 - (i & 1)), bt = _a[0], dts[i] = _a[1];
4902
+ } else if (md == 3) {
4903
+ if (!st.t)
4904
+ err(0);
4905
+ dts[i] = st.t[i];
4906
+ }
4907
+ }
4908
+ var _b = st.t = dts, mlt = _b[0], oct = _b[1], llt = _b[2];
4909
+ var lb = dat[ebt - 1];
4910
+ if (!lb)
4911
+ err(0);
4912
+ var spos = (ebt << 3) - 8 + msb(lb) - llt.b, cbt = spos >> 3, oubt = 0;
4913
+ var lst = (dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << llt.b) - 1;
4914
+ cbt = (spos -= oct.b) >> 3;
4915
+ var ost = (dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << oct.b) - 1;
4916
+ cbt = (spos -= mlt.b) >> 3;
4917
+ var mst = (dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << mlt.b) - 1;
4918
+ for (++ns; --ns; ) {
4919
+ var llc = llt.s[lst];
4920
+ var lbtr = llt.n[lst];
4921
+ var mlc = mlt.s[mst];
4922
+ var mbtr = mlt.n[mst];
4923
+ var ofc = oct.s[ost];
4924
+ var obtr = oct.n[ost];
4925
+ cbt = (spos -= ofc) >> 3;
4926
+ var ofp = 1 << ofc;
4927
+ var off = ofp + ((dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16 | dat[cbt + 3] << 24) >>> (spos & 7) & ofp - 1);
4928
+ cbt = (spos -= mlb[mlc]) >> 3;
4929
+ var ml = mlbl[mlc] + ((dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16) >> (spos & 7) & (1 << mlb[mlc]) - 1);
4930
+ cbt = (spos -= llb[llc]) >> 3;
4931
+ var ll = llbl[llc] + ((dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16) >> (spos & 7) & (1 << llb[llc]) - 1);
4932
+ cbt = (spos -= lbtr) >> 3;
4933
+ lst = llt.t[lst] + ((dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << lbtr) - 1);
4934
+ cbt = (spos -= mbtr) >> 3;
4935
+ mst = mlt.t[mst] + ((dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << mbtr) - 1);
4936
+ cbt = (spos -= obtr) >> 3;
4937
+ ost = oct.t[ost] + ((dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << obtr) - 1);
4938
+ if (off > 3) {
4939
+ st.o[2] = st.o[1];
4940
+ st.o[1] = st.o[0];
4941
+ st.o[0] = off -= 3;
4942
+ } else {
4943
+ var idx = off - (ll != 0);
4944
+ if (idx) {
4945
+ off = idx == 3 ? st.o[0] - 1 : st.o[idx];
4946
+ if (idx > 1)
4947
+ st.o[2] = st.o[1];
4948
+ st.o[1] = st.o[0];
4949
+ st.o[0] = off;
4950
+ } else
4951
+ off = st.o[0];
4952
+ }
4953
+ for (var i = 0; i < ll; ++i) {
4954
+ buf[oubt + i] = buf[spl + i];
4955
+ }
4956
+ oubt += ll, spl += ll;
4957
+ var stin = oubt - off;
4958
+ if (stin < 0) {
4959
+ var len = -stin;
4960
+ var bs = st.e + stin;
4961
+ if (len > ml)
4962
+ len = ml;
4963
+ for (var i = 0; i < len; ++i) {
4964
+ buf[oubt + i] = st.w[bs + i];
4965
+ }
4966
+ oubt += len, ml -= len, stin = 0;
4967
+ }
4968
+ for (var i = 0; i < ml; ++i) {
4969
+ buf[oubt + i] = buf[stin + i];
4970
+ }
4971
+ oubt += ml;
4972
+ }
4973
+ if (oubt != spl) {
4974
+ while (spl < buf.length) {
4975
+ buf[oubt++] = buf[spl++];
4976
+ }
4977
+ } else
4978
+ oubt = buf.length;
4979
+ if (out)
4980
+ st.y += oubt;
4981
+ else
4982
+ buf = slc(buf, 0, oubt);
4983
+ } else if (out) {
4984
+ st.y += lss;
4985
+ if (spl) {
4986
+ for (var i = 0; i < lss; ++i) {
4987
+ buf[i] = buf[spl + i];
4988
+ }
4989
+ }
4990
+ } else if (spl)
4991
+ buf = slc(buf, spl);
4992
+ st.b = ebt;
4993
+ return buf;
4994
+ }
4995
+ err(2);
4996
+ };
4997
+ cct = function(bufs, ol) {
4998
+ if (bufs.length == 1)
4999
+ return bufs[0];
5000
+ var buf = new u8(ol);
5001
+ for (var i = 0, b = 0; i < bufs.length; ++i) {
5002
+ var chk = bufs[i];
5003
+ buf.set(chk, b);
5004
+ b += chk.length;
5005
+ }
5006
+ return buf;
5007
+ };
5008
+ }
5009
+ });
5010
+
5011
+ // ../core/dist/studio-skills.js
5012
+ import { createHash as createHash2 } from "crypto";
5013
+ import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
5014
+ import * as path4 from "path";
5015
+ function decompressLz4Block(input, outputLength) {
5016
+ const output = Buffer.allocUnsafe(outputLength);
5017
+ let inputOffset = 0;
5018
+ let outputOffset = 0;
5019
+ const readExtendedLength = (initial) => {
5020
+ let length = initial;
5021
+ if (initial !== 15)
5022
+ return length;
5023
+ let next = 255;
5024
+ while (next === 255) {
5025
+ if (inputOffset >= input.length) {
5026
+ throw new Error("Invalid LZ4 chunk: truncated extended length");
5027
+ }
5028
+ next = input[inputOffset++];
5029
+ length += next;
5030
+ }
5031
+ return length;
5032
+ };
5033
+ while (inputOffset < input.length) {
5034
+ const token = input[inputOffset++];
5035
+ const literalLength = readExtendedLength(token >>> 4);
5036
+ if (inputOffset + literalLength > input.length || outputOffset + literalLength > output.length) {
5037
+ throw new Error("Invalid LZ4 chunk: literal exceeds chunk boundary");
5038
+ }
5039
+ input.copy(output, outputOffset, inputOffset, inputOffset + literalLength);
5040
+ inputOffset += literalLength;
5041
+ outputOffset += literalLength;
5042
+ if (inputOffset === input.length)
5043
+ break;
5044
+ if (inputOffset + 2 > input.length) {
5045
+ throw new Error("Invalid LZ4 chunk: missing match offset");
5046
+ }
5047
+ const matchOffset = input.readUInt16LE(inputOffset);
5048
+ inputOffset += 2;
5049
+ if (matchOffset === 0 || matchOffset > outputOffset) {
5050
+ throw new Error("Invalid LZ4 chunk: invalid match offset");
5051
+ }
5052
+ const matchLength = readExtendedLength(token & 15) + 4;
5053
+ if (outputOffset + matchLength > output.length) {
5054
+ throw new Error("Invalid LZ4 chunk: match exceeds output boundary");
5055
+ }
5056
+ for (let index = 0; index < matchLength; index += 1) {
5057
+ output[outputOffset] = output[outputOffset - matchOffset];
5058
+ outputOffset += 1;
5059
+ }
5060
+ }
5061
+ if (outputOffset !== output.length) {
5062
+ throw new Error(`Invalid LZ4 chunk: produced ${outputOffset} bytes, expected ${output.length}`);
5063
+ }
5064
+ return output;
5065
+ }
5066
+ function decompressChunk(compressed, outputLength) {
5067
+ if (compressed.subarray(0, ZSTD_MAGIC.length).equals(ZSTD_MAGIC)) {
5068
+ const decompressed = Buffer.from(decompress(compressed));
5069
+ if (decompressed.length !== outputLength) {
5070
+ throw new Error(`Invalid Zstandard chunk: produced ${decompressed.length} bytes, expected ${outputLength}`);
5071
+ }
5072
+ return decompressed;
5073
+ }
5074
+ return decompressLz4Block(compressed, outputLength);
5075
+ }
5076
+ function readChunk(reader) {
5077
+ if (reader.remaining < RBXM_CHUNK_HEADER_BYTES) {
5078
+ throw new Error("Invalid Assistant.rbxm: truncated chunk header");
5079
+ }
5080
+ const type = reader.readBytes(4).toString("latin1");
5081
+ const compressedLength = reader.readUint32();
5082
+ const uncompressedLength = reader.readUint32();
5083
+ reader.skip(4);
5084
+ if (type === "END\0")
5085
+ return { type };
5086
+ if (compressedLength === 0) {
5087
+ return { type, content: reader.readBytes(uncompressedLength) };
5088
+ }
5089
+ return {
5090
+ type,
5091
+ content: decompressChunk(reader.readBytes(compressedLength), uncompressedLength)
5092
+ };
5093
+ }
5094
+ function parseFrontmatter(markdown) {
5095
+ const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
5096
+ if (!match)
5097
+ return void 0;
5098
+ const fields = /* @__PURE__ */ new Map();
5099
+ for (const line of match[1].split(/\r?\n/)) {
5100
+ const separator = line.indexOf(":");
5101
+ if (separator <= 0)
5102
+ continue;
5103
+ const key = line.slice(0, separator).trim().toLowerCase();
5104
+ let value = line.slice(separator + 1).trim();
5105
+ if (value.startsWith('"') && value.endsWith('"')) {
5106
+ try {
5107
+ value = JSON.parse(value);
5108
+ } catch {
5109
+ }
5110
+ } else if (value.startsWith("'") && value.endsWith("'")) {
5111
+ value = value.slice(1, -1).replace(/''/g, "'");
5112
+ }
5113
+ fields.set(key, value);
5114
+ }
5115
+ const name = fields.get("name")?.trim();
5116
+ if (!name)
5117
+ return void 0;
5118
+ return { name, description: fields.get("description")?.trim() ?? "" };
5119
+ }
5120
+ function canonicalBuiltInSkillName(sourceName) {
5121
+ return sourceName.toLowerCase().startsWith("rbx-") ? sourceName : `rbx-${sourceName}`;
5122
+ }
5123
+ function parseBuiltInStudioSkills(buffer) {
5124
+ if (buffer.length < RBXM_HEADER_BYTES || !buffer.subarray(0, RBXM_MAGIC.length).equals(RBXM_MAGIC)) {
5125
+ throw new Error("Invalid Assistant.rbxm: Roblox binary model header not found");
5126
+ }
5127
+ const reader = new ByteReader(buffer, "Assistant.rbxm");
5128
+ reader.skip(RBXM_MAGIC.length);
5129
+ reader.readUint16();
5130
+ reader.readInt32();
5131
+ reader.readInt32();
5132
+ reader.skip(8);
5133
+ const stringValueClasses = /* @__PURE__ */ new Map();
5134
+ let foundEnd = false;
5135
+ while (reader.remaining > 0) {
5136
+ const chunk = readChunk(reader);
5137
+ if (chunk.type === "END\0") {
5138
+ foundEnd = true;
5139
+ break;
5140
+ }
5141
+ if (!chunk.content)
5142
+ continue;
5143
+ const chunkReader = new ByteReader(chunk.content, `${chunk.type} chunk`);
5144
+ if (chunk.type === "INST") {
5145
+ const classId2 = chunkReader.readUint32();
5146
+ const className = chunkReader.readString();
5147
+ chunkReader.readUint8();
5148
+ const count = chunkReader.readUint32();
5149
+ chunkReader.skip(count * 4);
5150
+ if (className === "StringValue")
5151
+ stringValueClasses.set(classId2, { count });
5152
+ continue;
5153
+ }
5154
+ if (chunk.type !== "PROP")
5155
+ continue;
5156
+ const classId = chunkReader.readUint32();
5157
+ const propertyName = chunkReader.readString();
5158
+ const propertyType = chunkReader.readUint8();
5159
+ const classInfo = stringValueClasses.get(classId);
5160
+ if (!classInfo || propertyType !== STRING_PROPERTY_TYPE)
5161
+ continue;
5162
+ if (propertyName !== "Name" && propertyName !== "Value")
5163
+ continue;
5164
+ const values = [];
5165
+ for (let index = 0; index < classInfo.count; index += 1) {
5166
+ values.push(chunkReader.readString());
5167
+ }
5168
+ if (propertyName === "Name")
5169
+ classInfo.names = values;
5170
+ else
5171
+ classInfo.values = values;
5172
+ }
5173
+ if (!foundEnd)
5174
+ throw new Error("Invalid Assistant.rbxm: END chunk not found");
5175
+ const documents = [];
5176
+ for (const classInfo of stringValueClasses.values()) {
5177
+ if (!classInfo.names || !classInfo.values)
5178
+ continue;
5179
+ for (let index = 0; index < classInfo.count; index += 1) {
5180
+ const document = classInfo.names[index];
5181
+ if (document !== "SKILL" && document !== "SKILL-combined")
5182
+ continue;
5183
+ const content = classInfo.values[index];
5184
+ const frontmatter = parseFrontmatter(content);
5185
+ if (!frontmatter)
5186
+ continue;
5187
+ documents.push({
5188
+ document,
5189
+ sourceName: frontmatter.name,
5190
+ description: frontmatter.description,
5191
+ content
5192
+ });
5193
+ }
5194
+ }
5195
+ const grouped = /* @__PURE__ */ new Map();
5196
+ for (const document of documents) {
5197
+ const key = document.sourceName.toLowerCase();
5198
+ const group = grouped.get(key) ?? {};
5199
+ if (document.document === "SKILL-combined")
5200
+ group.combined = document;
5201
+ else
5202
+ group.base = document;
5203
+ grouped.set(key, group);
5204
+ }
5205
+ return [...grouped.values()].map((group) => {
5206
+ const selected = group.combined ?? group.base;
5207
+ return {
5208
+ name: canonicalBuiltInSkillName(selected.sourceName),
5209
+ sourceName: selected.sourceName,
5210
+ description: selected.description,
5211
+ document: selected.document,
5212
+ hasCombinedDocument: group.combined !== void 0,
5213
+ content: selected.content,
5214
+ contentLength: Buffer.byteLength(selected.content, "utf8"),
5215
+ contentSha256: createHash2("sha256").update(selected.content).digest("hex")
5216
+ };
5217
+ }).sort((left, right) => left.name.localeCompare(right.name));
5218
+ }
5219
+ function discoverNamedFile(root, fileName, depth = 0) {
5220
+ if (depth > MAX_DISCOVERY_DEPTH || !existsSync4(root))
5221
+ return [];
5222
+ const matches = [];
5223
+ let entries;
5224
+ try {
5225
+ entries = readdirSync3(root, { withFileTypes: true });
5226
+ } catch {
5227
+ return matches;
5228
+ }
5229
+ for (const entry of entries) {
5230
+ const entryPath = path4.join(root, entry.name);
5231
+ if (entry.isFile() && entry.name.toLowerCase() === fileName.toLowerCase()) {
5232
+ matches.push(entryPath);
5233
+ } else if (entry.isDirectory()) {
5234
+ matches.push(...discoverNamedFile(entryPath, fileName, depth + 1));
5235
+ }
5236
+ }
5237
+ return matches;
5238
+ }
5239
+ function resolveAssistantBundlePath(studioExe = resolveStudioExe()) {
5240
+ const override = process.env.ROBLOX_STUDIO_ASSISTANT_BUNDLE;
5241
+ if (override) {
5242
+ if (!existsSync4(override)) {
5243
+ throw new Error(`ROBLOX_STUDIO_ASSISTANT_BUNDLE does not exist: ${override}`);
5244
+ }
5245
+ return override;
5246
+ }
5247
+ const exeDirectory = path4.dirname(studioExe);
5248
+ const roots = [
5249
+ path4.join(exeDirectory, "BuiltInStandalonePlugins"),
5250
+ path4.resolve(exeDirectory, "..", "Resources", "BuiltInStandalonePlugins"),
5251
+ path4.resolve(exeDirectory, "..", "..", "OTAPlugins")
5252
+ ];
5253
+ const direct = path4.join(exeDirectory, "BuiltInStandalonePlugins", "Optimized_Embedded_Signature", "Assistant.rbxm");
5254
+ const candidates = /* @__PURE__ */ new Set();
5255
+ if (existsSync4(direct))
5256
+ candidates.add(direct);
5257
+ for (const root of roots) {
5258
+ for (const candidate of discoverNamedFile(root, "Assistant.rbxm"))
5259
+ candidates.add(candidate);
5260
+ }
5261
+ const newest = [...candidates].map((candidate) => ({ candidate, modifiedAt: statSync3(candidate).mtimeMs })).sort((left, right) => right.modifiedAt - left.modifiedAt)[0]?.candidate;
5262
+ if (!newest) {
5263
+ throw new Error(`Studio Assistant bundle not found for ${studioExe}. Set ROBLOX_STUDIO_ASSISTANT_BUNDLE to the installed Assistant.rbxm path.`);
5264
+ }
5265
+ return newest;
5266
+ }
5267
+ function loadBuiltInStudioSkills(bundlePath = resolveAssistantBundlePath()) {
5268
+ const stats = statSync3(bundlePath);
5269
+ if (cachedBundle?.path === bundlePath && cachedBundle.modifiedAt === stats.mtimeMs && cachedBundle.size === stats.size) {
5270
+ return cachedBundle.value;
5271
+ }
5272
+ const buffer = readFileSync5(bundlePath);
5273
+ const skills = parseBuiltInStudioSkills(buffer);
5274
+ if (skills.length === 0) {
5275
+ throw new Error(`No built-in skill documents found in ${bundlePath}`);
5276
+ }
5277
+ const studioVersion = bundlePath.split(path4.sep).find((segment) => /^version-[a-z0-9]+$/i.test(segment));
5278
+ const value = {
5279
+ bundlePath,
5280
+ bundleModifiedAt: stats.mtime.toISOString(),
5281
+ bundleSha256: createHash2("sha256").update(buffer).digest("hex"),
5282
+ studioVersion,
5283
+ skills
5284
+ };
5285
+ cachedBundle = { path: bundlePath, modifiedAt: stats.mtimeMs, size: stats.size, value };
5286
+ return value;
5287
+ }
5288
+ function findBuiltInStudioSkill(bundle, requestedName) {
5289
+ const wanted = requestedName.trim().toLowerCase();
5290
+ return bundle.skills.find((skill) => skill.name.toLowerCase() === wanted || skill.sourceName.toLowerCase() === wanted);
5291
+ }
5292
+ var RBXM_MAGIC, RBXM_HEADER_BYTES, RBXM_CHUNK_HEADER_BYTES, ZSTD_MAGIC, STRING_PROPERTY_TYPE, MAX_DISCOVERY_DEPTH, ByteReader, cachedBundle;
5293
+ var init_studio_skills = __esm({
5294
+ "../core/dist/studio-skills.js"() {
5295
+ "use strict";
5296
+ init_esm();
5297
+ init_studio_instance_manager();
5298
+ RBXM_MAGIC = Buffer.from("<roblox!\x89\xFF\r\n\n", "latin1");
5299
+ RBXM_HEADER_BYTES = 32;
5300
+ RBXM_CHUNK_HEADER_BYTES = 16;
5301
+ ZSTD_MAGIC = Buffer.from([40, 181, 47, 253]);
5302
+ STRING_PROPERTY_TYPE = 1;
5303
+ MAX_DISCOVERY_DEPTH = 6;
5304
+ ByteReader = class {
5305
+ bytes;
5306
+ label;
5307
+ offset = 0;
5308
+ constructor(bytes, label) {
5309
+ this.bytes = bytes;
5310
+ this.label = label;
5311
+ }
5312
+ get remaining() {
5313
+ return this.bytes.length - this.offset;
5314
+ }
5315
+ requireBytes(length) {
5316
+ if (!Number.isSafeInteger(length) || length < 0 || this.remaining < length) {
5317
+ throw new Error(`Invalid ${this.label}: requested ${length} bytes with ${this.remaining} remaining`);
5318
+ }
5319
+ }
5320
+ readUint8() {
5321
+ this.requireBytes(1);
5322
+ return this.bytes[this.offset++];
5323
+ }
5324
+ readUint16() {
5325
+ this.requireBytes(2);
5326
+ const value = this.bytes.readUInt16LE(this.offset);
5327
+ this.offset += 2;
5328
+ return value;
5329
+ }
5330
+ readUint32() {
5331
+ this.requireBytes(4);
5332
+ const value = this.bytes.readUInt32LE(this.offset);
5333
+ this.offset += 4;
5334
+ return value;
5335
+ }
5336
+ readInt32() {
5337
+ this.requireBytes(4);
5338
+ const value = this.bytes.readInt32LE(this.offset);
5339
+ this.offset += 4;
5340
+ return value;
5341
+ }
5342
+ readBytes(length) {
5343
+ this.requireBytes(length);
5344
+ const value = this.bytes.subarray(this.offset, this.offset + length);
5345
+ this.offset += length;
5346
+ return value;
5347
+ }
5348
+ skip(length) {
5349
+ this.requireBytes(length);
5350
+ this.offset += length;
5351
+ }
5352
+ readString() {
5353
+ const length = this.readUint32();
5354
+ return this.readBytes(length).toString("utf8");
5355
+ }
5356
+ };
5357
+ }
5358
+ });
5359
+
3979
5360
  // ../core/dist/jpeg-encoder.js
3980
5361
  function rgbaToJpeg(rgba, width, height, quality = 80) {
3981
5362
  if (width <= 0 || height <= 0)
@@ -4995,7 +6376,7 @@ var init_png_encoder = __esm({
4995
6376
  // ../core/dist/tools/index.js
4996
6377
  import * as fs3 from "fs";
4997
6378
  import * as os3 from "os";
4998
- import * as path4 from "path";
6379
+ import * as path5 from "path";
4999
6380
  function multiplayerStopDisabledBody() {
5000
6381
  return {
5001
6382
  success: false,
@@ -5020,7 +6401,7 @@ function encodeImageFromRgbaResponse(response, format, quality) {
5020
6401
  };
5021
6402
  }
5022
6403
  function sleep(ms) {
5023
- return new Promise((resolve4) => setTimeout(resolve4, ms));
6404
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
5024
6405
  }
5025
6406
  function errorMessage(error) {
5026
6407
  return error instanceof Error ? error.message : String(error);
@@ -5082,7 +6463,7 @@ function loadMicroProfilerBaseline(source, sourcePath) {
5082
6463
  if (typeof sourcePath !== "string" || sourcePath === "") {
5083
6464
  throw new Error("baseline_path must be a non-empty string when provided");
5084
6465
  }
5085
- const resolved = path4.resolve(sourcePath);
6466
+ const resolved = path5.resolve(sourcePath);
5086
6467
  const parsed = JSON.parse(fs3.readFileSync(resolved, "utf8"));
5087
6468
  const record = asRecord(parsed);
5088
6469
  if (!record)
@@ -5543,6 +6924,7 @@ var init_tools = __esm({
5543
6924
  init_studio_instance_manager();
5544
6925
  init_image_decode();
5545
6926
  init_roblox_docs();
6927
+ init_studio_skills();
5546
6928
  init_jpeg_encoder();
5547
6929
  init_png_encoder();
5548
6930
  MAX_INLINE_IMAGE_BYTES = 6e6;
@@ -5610,10 +6992,52 @@ var init_tools = __esm({
5610
6992
  this.openCloudClient = new OpenCloudClient();
5611
6993
  this.cookieClient = new RobloxCookieClient();
5612
6994
  this.instanceManager = new StudioInstanceManager();
6995
+ this.bridge.onInstanceRegistered((instance) => this._associateManagedEditConnection(instance));
5613
6996
  }
5614
6997
  _textResult(body) {
5615
6998
  return { content: [{ type: "text", text: JSON.stringify(body) }] };
5616
6999
  }
7000
+ async getRobloxSkills(action, name) {
7001
+ if (action !== "list" && action !== "get") {
7002
+ throw new Error('get_roblox_skills action must be "list" or "get"');
7003
+ }
7004
+ const bundle = loadBuiltInStudioSkills();
7005
+ const bundleMetadata = {
7006
+ source: "installed-studio-assistant",
7007
+ studioVersion: bundle.studioVersion,
7008
+ bundlePath: bundle.bundlePath,
7009
+ bundleModifiedAt: bundle.bundleModifiedAt,
7010
+ bundleSha256: bundle.bundleSha256
7011
+ };
7012
+ if (action === "list") {
7013
+ return this._textResult({
7014
+ action,
7015
+ ...bundleMetadata,
7016
+ count: bundle.skills.length,
7017
+ skills: bundle.skills.map((skill2) => ({
7018
+ name: skill2.name,
7019
+ sourceName: skill2.sourceName,
7020
+ description: skill2.description,
7021
+ document: skill2.document,
7022
+ hasCombinedDocument: skill2.hasCombinedDocument,
7023
+ contentLength: skill2.contentLength,
7024
+ contentSha256: skill2.contentSha256
7025
+ }))
7026
+ });
7027
+ }
7028
+ if (!name || typeof name !== "string" || !name.trim()) {
7029
+ throw new Error('get_roblox_skills action="get" requires a skill name from action="list"');
7030
+ }
7031
+ const skill = findBuiltInStudioSkill(bundle, name);
7032
+ if (!skill) {
7033
+ throw new Error(`Built-in Studio skill "${name}" was not found. Available skills: ` + bundle.skills.map((candidate) => candidate.name).join(", "));
7034
+ }
7035
+ return this._textResult({
7036
+ action,
7037
+ ...bundleMetadata,
7038
+ skill
7039
+ });
7040
+ }
5617
7041
  async getRobloxDocs(name, docType, section) {
5618
7042
  if (!name || typeof name !== "string") {
5619
7043
  throw new Error('get_roblox_docs requires a name (e.g. "ProximityPrompt")');
@@ -5966,8 +7390,8 @@ var init_tools = __esm({
5966
7390
  timedOut: true
5967
7391
  };
5968
7392
  }
5969
- async getFileTree(path5 = "", instance_id) {
5970
- const response = await this._callSingle("/api/file-tree", { path: path5 }, void 0, instance_id);
7393
+ async getFileTree(path6 = "", instance_id) {
7394
+ const response = await this._callSingle("/api/file-tree", { path: path6 }, void 0, instance_id);
5971
7395
  return {
5972
7396
  content: [
5973
7397
  {
@@ -6084,9 +7508,9 @@ var init_tools = __esm({
6084
7508
  ]
6085
7509
  };
6086
7510
  }
6087
- async getProjectStructure(path5, maxDepth, scriptsOnly, instance_id) {
7511
+ async getProjectStructure(path6, maxDepth, scriptsOnly, instance_id) {
6088
7512
  const response = await this._callSingle("/api/project-structure", {
6089
- path: path5,
7513
+ path: path6,
6090
7514
  maxDepth,
6091
7515
  scriptsOnly
6092
7516
  }, void 0, instance_id);
@@ -7010,8 +8434,8 @@ ${code}`
7010
8434
  const rawJson = mutable.raw_json;
7011
8435
  if (typeof rawJson === "string") {
7012
8436
  if (typeof outputPath === "string" && outputPath !== "") {
7013
- const resolvedOutputPath = path4.resolve(outputPath);
7014
- fs3.mkdirSync(path4.dirname(resolvedOutputPath), { recursive: true });
8437
+ const resolvedOutputPath = path5.resolve(outputPath);
8438
+ fs3.mkdirSync(path5.dirname(resolvedOutputPath), { recursive: true });
7015
8439
  fs3.writeFileSync(resolvedOutputPath, rawJson, "utf8");
7016
8440
  mutable.output_path = resolvedOutputPath;
7017
8441
  }
@@ -7073,8 +8497,8 @@ ${code}`
7073
8497
  const rawSnapshotBase64 = mutable.raw_snapshot_base64;
7074
8498
  if (typeof rawSnapshotBase64 === "string") {
7075
8499
  if (typeof outputPath === "string" && outputPath !== "") {
7076
- const resolvedOutputPath = path4.resolve(outputPath);
7077
- fs3.mkdirSync(path4.dirname(resolvedOutputPath), { recursive: true });
8500
+ const resolvedOutputPath = path5.resolve(outputPath);
8501
+ fs3.mkdirSync(path5.dirname(resolvedOutputPath), { recursive: true });
7078
8502
  fs3.writeFileSync(resolvedOutputPath, Buffer.from(rawSnapshotBase64, "base64"));
7079
8503
  mutable.output_path = resolvedOutputPath;
7080
8504
  }
@@ -7089,8 +8513,8 @@ ${code}`
7089
8513
  });
7090
8514
  }
7091
8515
  if (typeof summaryOutputPath === "string" && summaryOutputPath !== "") {
7092
- const resolvedSummaryPath = path4.resolve(summaryOutputPath);
7093
- fs3.mkdirSync(path4.dirname(resolvedSummaryPath), { recursive: true });
8516
+ const resolvedSummaryPath = path5.resolve(summaryOutputPath);
8517
+ fs3.mkdirSync(path5.dirname(resolvedSummaryPath), { recursive: true });
7094
8518
  fs3.writeFileSync(resolvedSummaryPath, JSON.stringify(mutable, null, 2), "utf8");
7095
8519
  mutable.summary_output_path = resolvedSummaryPath;
7096
8520
  }
@@ -7158,11 +8582,18 @@ ${code}`
7158
8582
  return record.placeId !== void 0 && instance.placeId === record.placeId;
7159
8583
  }
7160
8584
  if ((record.source === "baseplate" || record.source === "local_file") && record.localPlaceFile) {
7161
- const expectedName = path4.basename(record.localPlaceFile);
8585
+ const expectedName = path5.basename(record.localPlaceFile);
7162
8586
  return instance.placeName === expectedName || instance.dataModelName === expectedName;
7163
8587
  }
7164
8588
  return true;
7165
8589
  }
8590
+ _associateManagedEditConnection(instance) {
8591
+ if (instance.role !== "edit")
8592
+ return;
8593
+ 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];
8594
+ if (candidate)
8595
+ this.instanceManager.attachInstanceId(candidate, instance.instanceId);
8596
+ }
7166
8597
  async _deriveUniverseId(placeId) {
7167
8598
  const response = await fetch(`https://apis.roblox.com/universes/v1/places/${placeId}/universe`);
7168
8599
  if (!response.ok) {
@@ -7178,6 +8609,10 @@ ${code}`
7178
8609
  async _waitForManagedEditConnection(record, beforeKeys, timeoutMs) {
7179
8610
  const deadline = Date.now() + timeoutMs;
7180
8611
  while (Date.now() < deadline) {
8612
+ this.instanceManager.refresh(record);
8613
+ if (record.state === "failed" || record.state === "exited" || record.closedAt !== void 0) {
8614
+ return void 0;
8615
+ }
7181
8616
  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);
7182
8617
  if (candidates[0])
7183
8618
  return candidates[0];
@@ -7186,12 +8621,25 @@ ${code}`
7186
8621
  return void 0;
7187
8622
  }
7188
8623
  _managedStatus(record) {
8624
+ this.instanceManager.refresh(record);
7189
8625
  const connected = record.instanceId ? this.bridge.getPublicInstances().filter((instance) => instance.instanceId === record.instanceId) : [];
7190
8626
  return {
8627
+ launch_id: record.recordId,
7191
8628
  instance_id: record.instanceId,
8629
+ managed: true,
8630
+ state: record.state,
8631
+ pid: record.nativeProcessId ?? record.spawnPid,
8632
+ process_running: record.closedAt === void 0 && record.exitedAt === void 0,
7192
8633
  source: record.source,
8634
+ local_place_file: record.localPlaceFile,
7193
8635
  place_id: record.placeId,
7194
8636
  place_version: record.placeVersion,
8637
+ launched_at: new Date(record.launchedAt).toISOString(),
8638
+ connected_at: record.connectedAt ? new Date(record.connectedAt).toISOString() : void 0,
8639
+ failed_at: record.failedAt ? new Date(record.failedAt).toISOString() : void 0,
8640
+ exited_at: record.exitedAt ? new Date(record.exitedAt).toISOString() : void 0,
8641
+ exit_code: record.exitCode,
8642
+ failure_reason: record.failureReason,
7195
8643
  connected: connected.length > 0,
7196
8644
  roles: connected.map((instance) => instance.role).sort()
7197
8645
  };
@@ -7203,6 +8651,10 @@ ${code}`
7203
8651
  async manageInstance(request) {
7204
8652
  const action = request.action;
7205
8653
  const instance_id = typeof request.instance_id === "string" ? request.instance_id : void 0;
8654
+ const launch_id = typeof request.launch_id === "string" ? request.launch_id : void 0;
8655
+ if (instance_id && launch_id) {
8656
+ throw new Error("manage_instance accepts only one of instance_id or launch_id.");
8657
+ }
7206
8658
  if (action !== "launch" && action !== "close" && action !== "status" && action !== "list_place_versions") {
7207
8659
  throw new Error("manage_instance requires action=launch|close|status|list_place_versions");
7208
8660
  }
@@ -7230,18 +8682,26 @@ ${code}`
7230
8682
  return this._textResult(body);
7231
8683
  }
7232
8684
  if (action === "status") {
8685
+ if (launch_id) {
8686
+ const record2 = this.instanceManager.getByLaunchId(launch_id);
8687
+ if (!record2)
8688
+ return this._textResult({ error: "Launch is not managed.", launch_id });
8689
+ return this._textResult(this._managedStatus(record2));
8690
+ }
7233
8691
  if (instance_id) {
7234
8692
  const record2 = this.instanceManager.get(instance_id);
7235
8693
  const connected2 = this.bridge.getPublicInstances().filter((instance) => instance.instanceId === instance_id);
7236
8694
  if (!record2 && connected2.length === 0) {
7237
8695
  return this._textResult({ error: "Instance is not connected or managed.", instance_id });
7238
8696
  }
8697
+ if (record2)
8698
+ return this._textResult(this._managedStatus(record2));
7239
8699
  return this._textResult({
7240
8700
  instance_id,
7241
- managed: !!record2,
7242
- source: record2?.source,
7243
- place_id: record2?.placeId ?? connected2[0]?.placeId,
7244
- place_version: record2?.placeVersion,
8701
+ managed: false,
8702
+ state: "connected",
8703
+ place_id: connected2[0]?.placeId,
8704
+ connected: true,
7245
8705
  roles: connected2.map((instance) => instance.role).sort()
7246
8706
  });
7247
8707
  }
@@ -7257,14 +8717,32 @@ ${code}`
7257
8717
  }
7258
8718
  if (action === "close") {
7259
8719
  let record2;
8720
+ if (launch_id) {
8721
+ record2 = this.instanceManager.getByLaunchId(launch_id);
8722
+ if (!record2)
8723
+ return this._textResult({ error: "Launch is not managed.", launch_id });
8724
+ const connectedInstanceId = record2.instanceId;
8725
+ const closeResult2 = record2.closedAt === void 0 ? this.instanceManager.close(record2) : { status: "already_closed" };
8726
+ if (connectedInstanceId) {
8727
+ await this.bridge.unregisterInstanceIdEverywhere(connectedInstanceId);
8728
+ await sleep(500);
8729
+ await this.bridge.unregisterInstanceIdEverywhere(connectedInstanceId);
8730
+ }
8731
+ return this._textResult({
8732
+ ...this._managedStatus(record2),
8733
+ message: closeResult2.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
8734
+ });
8735
+ }
7260
8736
  if (instance_id) {
8737
+ const recordBeforeClose = this.instanceManager.get(instance_id);
7261
8738
  const managedClose = this.instanceManager.closeByInstanceId(instance_id);
7262
8739
  if (managedClose.status !== "not_found") {
7263
8740
  await this.bridge.unregisterInstanceIdEverywhere(instance_id);
7264
8741
  await sleep(500);
7265
8742
  await this.bridge.unregisterInstanceIdEverywhere(instance_id);
8743
+ const closedRecord = recordBeforeClose ?? (managedClose.launchId ? this.instanceManager.getByLaunchId(managedClose.launchId) : void 0);
7266
8744
  return this._textResult({
7267
- instance_id,
8745
+ ...closedRecord ? this._managedStatus(closedRecord) : { instance_id },
7268
8746
  message: managedClose.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
7269
8747
  });
7270
8748
  }
@@ -7311,7 +8789,7 @@ ${code}`
7311
8789
  await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
7312
8790
  }
7313
8791
  return this._textResult({
7314
- instance_id: record2.instanceId,
8792
+ ...this._managedStatus(record2),
7315
8793
  message: closeResult.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
7316
8794
  });
7317
8795
  }
@@ -7338,24 +8816,34 @@ ${code}`
7338
8816
  localPlaceFile,
7339
8817
  placeId,
7340
8818
  universeId,
7341
- placeVersion
8819
+ placeVersion,
8820
+ connectionTimeoutMs: timeoutMs
7342
8821
  });
7343
8822
  if (!waitForConnection) {
7344
- return this._textResult({ message: "Studio launch requested." });
8823
+ return this._textResult({
8824
+ ...this._managedStatus(record),
8825
+ message: "Studio launch requested."
8826
+ });
7345
8827
  }
7346
8828
  const connected = await this._waitForManagedEditConnection(record, beforeKeys, timeoutMs);
7347
8829
  if (!connected) {
7348
- try {
7349
- this.instanceManager.close(record);
7350
- } catch {
8830
+ if (record.state === "launching") {
8831
+ this.instanceManager.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
8832
+ }
8833
+ if (record.closedAt === void 0) {
8834
+ try {
8835
+ this.instanceManager.close(record);
8836
+ } catch {
8837
+ }
7351
8838
  }
7352
8839
  return this._textResult({
7353
- error: "Studio launched, but the MCP plugin did not connect before timeout."
8840
+ ...this._managedStatus(record),
8841
+ error: record.failureReason ?? "Studio launched, but the MCP plugin did not connect before timeout."
7354
8842
  });
7355
8843
  }
7356
8844
  this.instanceManager.attachInstanceId(record, connected.instanceId);
7357
8845
  return this._textResult({
7358
- instance_id: connected.instanceId,
8846
+ ...this._managedStatus(record),
7359
8847
  message: launchSource === "place_revision" ? `Studio opened place revision ${placeVersion}.` : "Studio opened."
7360
8848
  });
7361
8849
  }
@@ -7538,16 +9026,16 @@ ${code}`
7538
9026
  try {
7539
9027
  editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit");
7540
9028
  body.edit = editState;
7541
- } catch (err) {
7542
- body.edit = { error: err instanceof Error ? err.message : String(err) };
9029
+ } catch (err2) {
9030
+ body.edit = { error: err2 instanceof Error ? err2.message : String(err2) };
7543
9031
  }
7544
9032
  }
7545
9033
  if (server) {
7546
9034
  try {
7547
9035
  serverState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "server");
7548
9036
  body.server = serverState;
7549
- } catch (err) {
7550
- body.server = { error: err instanceof Error ? err.message : String(err) };
9037
+ } catch (err2) {
9038
+ body.server = { error: err2 instanceof Error ? err2.message : String(err2) };
7551
9039
  }
7552
9040
  }
7553
9041
  const session = editState?.session;
@@ -7873,14 +9361,14 @@ ${code}`
7873
9361
  };
7874
9362
  }
7875
9363
  static findProjectRoot(startDir) {
7876
- let dir = path4.resolve(startDir);
9364
+ let dir = path5.resolve(startDir);
7877
9365
  let previous = "";
7878
9366
  while (dir !== previous) {
7879
- if (fs3.existsSync(path4.join(dir, ".git")) || fs3.existsSync(path4.join(dir, "package.json"))) {
9367
+ if (fs3.existsSync(path5.join(dir, ".git")) || fs3.existsSync(path5.join(dir, "package.json"))) {
7880
9368
  return dir;
7881
9369
  }
7882
9370
  previous = dir;
7883
- dir = path4.dirname(dir);
9371
+ dir = path5.dirname(dir);
7884
9372
  }
7885
9373
  return null;
7886
9374
  }
@@ -7894,7 +9382,7 @@ ${code}`
7894
9382
  }
7895
9383
  }
7896
9384
  static ensureWritableDirectory(candidate, label) {
7897
- const resolved = path4.resolve(candidate);
9385
+ const resolved = path5.resolve(candidate);
7898
9386
  try {
7899
9387
  fs3.mkdirSync(resolved, { recursive: true });
7900
9388
  } catch (error) {
@@ -7915,11 +9403,11 @@ ${code}`
7915
9403
  if (_RobloxStudioTools._cachedLibraryPath)
7916
9404
  return _RobloxStudioTools._cachedLibraryPath;
7917
9405
  const overridePath = process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
7918
- const cwd = path4.resolve(process.cwd());
9406
+ const cwd = path5.resolve(process.cwd());
7919
9407
  const projectRoot = _RobloxStudioTools.findProjectRoot(cwd);
7920
- const homeLibraryPath = path4.join(os3.homedir(), ".robloxstudio-mcp", "build-library");
7921
- const projectLibraryPath = projectRoot ? path4.join(projectRoot, "build-library") : null;
7922
- const cwdLibraryPath = path4.join(cwd, "build-library");
9408
+ const homeLibraryPath = path5.join(os3.homedir(), ".robloxstudio-mcp", "build-library");
9409
+ const projectLibraryPath = projectRoot ? path5.join(projectRoot, "build-library") : null;
9410
+ const cwdLibraryPath = path5.join(cwd, "build-library");
7923
9411
  let result;
7924
9412
  if (overridePath) {
7925
9413
  result = _RobloxStudioTools.ensureWritableDirectory(overridePath, "override");
@@ -7933,12 +9421,12 @@ ${code}`
7933
9421
  }
7934
9422
  })());
7935
9423
  if (existing) {
7936
- result = path4.resolve(existing);
9424
+ result = path5.resolve(existing);
7937
9425
  } else if (projectLibraryPath) {
7938
9426
  try {
7939
9427
  result = _RobloxStudioTools.ensureWritableDirectory(projectLibraryPath, "project-root");
7940
- } catch (err) {
7941
- console.error(`Warning: could not create build-library at project root (${projectLibraryPath}): ${err.message}. Falling back to home directory.`);
9428
+ } catch (err2) {
9429
+ console.error(`Warning: could not create build-library at project root (${projectLibraryPath}): ${err2.message}. Falling back to home directory.`);
7942
9430
  result = _RobloxStudioTools.ensureWritableDirectory(homeLibraryPath, "home");
7943
9431
  }
7944
9432
  } else {
@@ -7960,8 +9448,8 @@ ${code}`
7960
9448
  if (response && response.success && response.buildData) {
7961
9449
  const buildData = response.buildData;
7962
9450
  const buildId = buildData.id || `${style}/exported`;
7963
- const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${buildId}.json`);
7964
- const dirPath = path4.dirname(filePath);
9451
+ const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${buildId}.json`);
9452
+ const dirPath = path5.dirname(filePath);
7965
9453
  if (!fs3.existsSync(dirPath)) {
7966
9454
  fs3.mkdirSync(dirPath, { recursive: true });
7967
9455
  }
@@ -8062,8 +9550,8 @@ ${code}`
8062
9550
  const normalizedParts = this.normalizeBuildParts(parts, new Set(Object.keys(normalizedPalette)));
8063
9551
  const computedBounds = bounds || this.computeBounds(normalizedParts);
8064
9552
  const buildData = { id, style, bounds: computedBounds, palette: normalizedPalette, parts: normalizedParts };
8065
- const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
8066
- const dirPath = path4.dirname(filePath);
9553
+ const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
9554
+ const dirPath = path5.dirname(filePath);
8067
9555
  if (!fs3.existsSync(dirPath)) {
8068
9556
  fs3.mkdirSync(dirPath, { recursive: true });
8069
9557
  }
@@ -8121,8 +9609,8 @@ ${code}`
8121
9609
  };
8122
9610
  if (seed !== void 0)
8123
9611
  buildData.generatorSeed = seed;
8124
- const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
8125
- const dirPath = path4.dirname(filePath);
9612
+ const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
9613
+ const dirPath = path5.dirname(filePath);
8126
9614
  if (!fs3.existsSync(dirPath)) {
8127
9615
  fs3.mkdirSync(dirPath, { recursive: true });
8128
9616
  }
@@ -8150,13 +9638,13 @@ ${code}`
8150
9638
  }
8151
9639
  let resolved;
8152
9640
  if (typeof buildData === "string") {
8153
- const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${buildData}.json`);
9641
+ const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${buildData}.json`);
8154
9642
  if (!fs3.existsSync(filePath)) {
8155
9643
  throw new Error(`Build not found in library: ${buildData}`);
8156
9644
  }
8157
9645
  resolved = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
8158
9646
  } else if (buildData.id && !buildData.parts) {
8159
- const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${buildData.id}.json`);
9647
+ const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${buildData.id}.json`);
8160
9648
  if (!fs3.existsSync(filePath)) {
8161
9649
  throw new Error(`Build not found in library: ${buildData.id}`);
8162
9650
  }
@@ -8183,13 +9671,13 @@ ${code}`
8183
9671
  const styles = style ? [style] : ["medieval", "modern", "nature", "scifi", "misc"];
8184
9672
  const builds = [];
8185
9673
  for (const s of styles) {
8186
- const dirPath = path4.join(libraryPath, s);
9674
+ const dirPath = path5.join(libraryPath, s);
8187
9675
  if (!fs3.existsSync(dirPath))
8188
9676
  continue;
8189
9677
  const files = fs3.readdirSync(dirPath).filter((f) => f.endsWith(".json"));
8190
9678
  for (const file of files) {
8191
9679
  try {
8192
- const content = fs3.readFileSync(path4.join(dirPath, file), "utf-8");
9680
+ const content = fs3.readFileSync(path5.join(dirPath, file), "utf-8");
8193
9681
  const data = JSON.parse(content);
8194
9682
  builds.push({
8195
9683
  id: data.id || `${s}/${file.replace(".json", "")}`,
@@ -8228,7 +9716,7 @@ ${code}`
8228
9716
  if (!id) {
8229
9717
  throw new Error("Build ID is required for get_build");
8230
9718
  }
8231
- const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
9719
+ const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
8232
9720
  if (!fs3.existsSync(filePath)) {
8233
9721
  throw new Error(`Build not found in library: ${id}`);
8234
9722
  }
@@ -8315,7 +9803,7 @@ ${code}`
8315
9803
  if (!buildId) {
8316
9804
  throw new Error(`Invalid ${validatedKeyPath}: model key "${modelKey}" is not defined in sceneData.models`);
8317
9805
  }
8318
- const filePath = path4.join(libraryPath, `${buildId}.json`);
9806
+ const filePath = path5.join(libraryPath, `${buildId}.json`);
8319
9807
  if (!fs3.existsSync(filePath)) {
8320
9808
  throw new Error(`Build not found in library: ${buildId}`);
8321
9809
  }
@@ -8663,7 +10151,7 @@ ${code}`
8663
10151
  throw new Error(`File not found: ${filePath}`);
8664
10152
  }
8665
10153
  const fileContent = fs3.readFileSync(filePath);
8666
- const fileName = path4.basename(filePath);
10154
+ const fileName = path5.basename(filePath);
8667
10155
  if (assetType === "Decal" && this.cookieClient.hasCookie()) {
8668
10156
  const result2 = await this.cookieClient.uploadDecal(fileContent, displayName, description || "");
8669
10157
  return {
@@ -8888,12 +10376,12 @@ ${code}`
8888
10376
  return { content: [{ type: "text", text: JSON.stringify({ error: "plugin returned no base64 payload" }) }] };
8889
10377
  }
8890
10378
  const bytes = Buffer.from(response.base64, "base64");
8891
- const resolved = path4.resolve(outputPath);
10379
+ const resolved = path5.resolve(outputPath);
8892
10380
  try {
8893
- fs3.mkdirSync(path4.dirname(resolved), { recursive: true });
10381
+ fs3.mkdirSync(path5.dirname(resolved), { recursive: true });
8894
10382
  fs3.writeFileSync(resolved, bytes);
8895
- } catch (err) {
8896
- return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err.message}` }) }] };
10383
+ } catch (err2) {
10384
+ return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err2.message}` }) }] };
8897
10385
  }
8898
10386
  return {
8899
10387
  content: [{
@@ -8924,11 +10412,11 @@ ${code}`
8924
10412
  let bytes;
8925
10413
  let sourceLabel;
8926
10414
  if (source.path !== void 0) {
8927
- const resolved = path4.resolve(source.path);
10415
+ const resolved = path5.resolve(source.path);
8928
10416
  try {
8929
10417
  bytes = fs3.readFileSync(resolved);
8930
- } catch (err) {
8931
- return { content: [{ type: "text", text: JSON.stringify({ error: `failed to read ${resolved}: ${err.message}` }) }] };
10418
+ } catch (err2) {
10419
+ return { content: [{ type: "text", text: JSON.stringify({ error: `failed to read ${resolved}: ${err2.message}` }) }] };
8932
10420
  }
8933
10421
  sourceLabel = resolved;
8934
10422
  } else if (source.url !== void 0) {
@@ -8957,15 +10445,15 @@ ${code}`
8957
10445
  return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url}: downloaded ${arr.byteLength} bytes exceeds ${MAX_IMPORT_BYTES} byte cap` }) }] };
8958
10446
  }
8959
10447
  bytes = Buffer.from(arr);
8960
- } catch (err) {
8961
- return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url} failed: ${err.message}` }) }] };
10448
+ } catch (err2) {
10449
+ return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url} failed: ${err2.message}` }) }] };
8962
10450
  }
8963
10451
  sourceLabel = source.url;
8964
10452
  } else {
8965
10453
  try {
8966
10454
  bytes = Buffer.from(source.base64, "base64");
8967
- } catch (err) {
8968
- return { content: [{ type: "text", text: JSON.stringify({ error: `base64 decode failed: ${err.message}` }) }] };
10455
+ } catch (err2) {
10456
+ return { content: [{ type: "text", text: JSON.stringify({ error: `base64 decode failed: ${err2.message}` }) }] };
8969
10457
  }
8970
10458
  sourceLabel = `base64(${bytes.length}B)`;
8971
10459
  }
@@ -9098,7 +10586,13 @@ var init_proxy_bridge_service = __esm({
9098
10586
  return;
9099
10587
  const body = await res.json();
9100
10588
  if (Array.isArray(body.instances)) {
10589
+ const previousKeys = new Set(this.cachedInstances.map((instance) => `${instance.pluginSessionId}\0${instance.instanceId}\0${instance.role}\0${instance.connectedAt}`));
9101
10590
  this.cachedInstances = body.instances;
10591
+ for (const instance of body.instances) {
10592
+ const key = `${instance.pluginSessionId}\0${instance.instanceId}\0${instance.role}\0${instance.connectedAt}`;
10593
+ if (!previousKeys.has(key))
10594
+ this.notifyInstanceRegistered(toPublic(instance));
10595
+ }
9102
10596
  }
9103
10597
  } catch {
9104
10598
  }
@@ -9158,12 +10652,12 @@ var init_proxy_bridge_service = __esm({
9158
10652
  throw new Error(result.error);
9159
10653
  }
9160
10654
  return result.response;
9161
- } catch (err) {
10655
+ } catch (err2) {
9162
10656
  clearTimeout(timeoutId);
9163
- if (err.name === "AbortError") {
10657
+ if (err2.name === "AbortError") {
9164
10658
  throw new Error("Proxy request timeout");
9165
10659
  }
9166
- throw err;
10660
+ throw err2;
9167
10661
  }
9168
10662
  }
9169
10663
  cleanupOldRequests() {
@@ -10346,7 +11840,7 @@ var init_definitions = __esm({
10346
11840
  {
10347
11841
  name: "manage_instance",
10348
11842
  category: "write",
10349
- 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.',
11843
+ 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.',
10350
11844
  inputSchema: {
10351
11845
  type: "object",
10352
11846
  properties: {
@@ -10374,11 +11868,11 @@ var init_definitions = __esm({
10374
11868
  },
10375
11869
  wait_for_connection: {
10376
11870
  type: "boolean",
10377
- description: 'For action="launch": wait until the MCP plugin connects and return instance_id (default true).'
11871
+ 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.'
10378
11872
  },
10379
11873
  timeout_ms: {
10380
11874
  type: "number",
10381
- description: 'For action="launch": max milliseconds to wait for plugin connection (default 120000).'
11875
+ description: 'For action="launch": max milliseconds for plugin connection (default 120000). The deadline also applies asynchronously when wait_for_connection=false.'
10382
11876
  },
10383
11877
  max_page_size: {
10384
11878
  type: "number",
@@ -10390,7 +11884,11 @@ var init_definitions = __esm({
10390
11884
  },
10391
11885
  instance_id: {
10392
11886
  type: "string",
10393
- description: 'For action="close" or action="status": Studio instance to inspect or close. close accepts MCP-managed instances and explicitly connected edit instances.'
11887
+ description: 'For action="close" or action="status": connected Studio instance to inspect or close. Mutually exclusive with launch_id.'
11888
+ },
11889
+ launch_id: {
11890
+ type: "string",
11891
+ 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.'
10394
11892
  }
10395
11893
  },
10396
11894
  required: ["action"]
@@ -11968,11 +13466,32 @@ part(0,2,0,2,1,1,"b")`,
11968
13466
  required: ["pattern", "replacement"]
11969
13467
  }
11970
13468
  },
13469
+ // === Installed Studio Skills ===
13470
+ {
13471
+ name: "get_roblox_skills",
13472
+ category: "read",
13473
+ 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.`,
13474
+ inputSchema: {
13475
+ type: "object",
13476
+ properties: {
13477
+ action: {
13478
+ type: "string",
13479
+ enum: ["list", "get"],
13480
+ description: "List installed built-in skills or get one skill document."
13481
+ },
13482
+ name: {
13483
+ type: "string",
13484
+ description: 'Skill name returned by action="list". Required for action="get"; both canonical rbx-* and embedded source names are accepted.'
13485
+ }
13486
+ },
13487
+ required: ["action"]
13488
+ }
13489
+ },
11971
13490
  // === Documentation ===
11972
13491
  {
11973
13492
  name: "get_roblox_docs",
11974
13493
  category: "read",
11975
- description: 'Fetch official Roblox engine API documentation as markdown from create.roblox.com. Call this BEFORE writing or editing code that uses an engine class, enum, datatype, or Luau library you are not fully certain about (e.g. ProximityPrompt, Enum.KeyCode, CFrame, TweenService) \u2014 the page includes the description, properties, methods, events, and code samples. Results are cached, so repeat lookups are cheap. Very large pages are truncated with a section index; pass section (e.g. "Properties", "Methods", "Events") to read one section in full.',
13494
+ description: 'Fetch official Roblox engine API documentation as markdown from create.roblox.com. Call this BEFORE writing or editing code that uses an engine class, enum, datatype, or Luau library you are not fully certain about (e.g. ProximityPrompt, Enum.KeyCode, CFrame, TweenService) \u2014 the page includes the description, properties, methods, events, and code samples. Unresolved names return ranked recommendations from the official engine index, including pages in other doc categories. Results are cached, so repeat lookups are cheap. Very large pages are truncated with a section index; pass section (e.g. "Properties", "Methods", "Events") to read one section in full.',
11976
13495
  inputSchema: {
11977
13496
  type: "object",
11978
13497
  properties: {
@@ -12151,15 +13670,15 @@ part(0,2,0,2,1,1,"b")`,
12151
13670
  });
12152
13671
 
12153
13672
  // ../core/dist/install-plugin-helpers.js
12154
- import { existsSync as existsSync5, readFileSync as readFileSync6, unlinkSync } from "fs";
13673
+ import { existsSync as existsSync6, readFileSync as readFileSync7, unlinkSync } from "fs";
12155
13674
  import { execSync } from "child_process";
12156
- import { join as join5 } from "path";
13675
+ import { join as join6 } from "path";
12157
13676
  import { homedir as homedir5 } from "os";
12158
13677
  function isWSL() {
12159
13678
  if (process.platform !== "linux")
12160
13679
  return false;
12161
13680
  try {
12162
- const v = readFileSync6("/proc/version", "utf8");
13681
+ const v = readFileSync7("/proc/version", "utf8");
12163
13682
  return /microsoft|wsl/i.test(v);
12164
13683
  } catch {
12165
13684
  return false;
@@ -12177,7 +13696,7 @@ function getWindowsUserPluginsDir() {
12177
13696
  }).toString().trim();
12178
13697
  if (!linuxPath)
12179
13698
  return null;
12180
- return join5(linuxPath, "Roblox", "Plugins");
13699
+ return join6(linuxPath, "Roblox", "Plugins");
12181
13700
  } catch {
12182
13701
  return null;
12183
13702
  }
@@ -12186,7 +13705,7 @@ function getPluginsFolder() {
12186
13705
  if (process.env.MCP_PLUGINS_DIR)
12187
13706
  return process.env.MCP_PLUGINS_DIR;
12188
13707
  if (process.platform === "win32") {
12189
- return join5(process.env.LOCALAPPDATA || join5(homedir5(), "AppData", "Local"), "Roblox", "Plugins");
13708
+ return join6(process.env.LOCALAPPDATA || join6(homedir5(), "AppData", "Local"), "Roblox", "Plugins");
12190
13709
  }
12191
13710
  if (isWSL()) {
12192
13711
  const win = getWindowsUserPluginsDir();
@@ -12194,18 +13713,18 @@ function getPluginsFolder() {
12194
13713
  return win;
12195
13714
  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.");
12196
13715
  }
12197
- return join5(homedir5(), "Documents", "Roblox", "Plugins");
13716
+ return join6(homedir5(), "Documents", "Roblox", "Plugins");
12198
13717
  }
12199
13718
  function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
12200
- const otherDest = join5(pluginsFolder, otherAssetName);
12201
- if (!existsSync5(otherDest))
13719
+ const otherDest = join6(pluginsFolder, otherAssetName);
13720
+ if (!existsSync6(otherDest))
12202
13721
  return;
12203
13722
  if (replace) {
12204
13723
  try {
12205
13724
  unlinkSync(otherDest);
12206
13725
  log(`Removed conflicting ${otherAssetName}.`);
12207
- } catch (err) {
12208
- warn(`[install-plugin] Could not remove ${otherDest}: ${err}. Continuing.`);
13726
+ } catch (err2) {
13727
+ warn(`[install-plugin] Could not remove ${otherDest}: ${err2}. Continuing.`);
12209
13728
  }
12210
13729
  return;
12211
13730
  }
@@ -12234,6 +13753,7 @@ var init_dist = __esm({
12234
13753
  init_opencloud_client();
12235
13754
  init_install_plugin_helpers();
12236
13755
  init_roblox_cookie_client();
13756
+ init_studio_skills();
12237
13757
  }
12238
13758
  });
12239
13759
 
@@ -12243,13 +13763,13 @@ __export(install_plugin_exports, {
12243
13763
  installBundledPlugin: () => installBundledPlugin,
12244
13764
  installPlugin: () => installPlugin
12245
13765
  });
12246
- import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
12247
- import { dirname as dirname4, join as join6 } from "path";
13766
+ import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync8, unlinkSync as unlinkSync2 } from "fs";
13767
+ import { dirname as dirname5, join as join7 } from "path";
12248
13768
  import { fileURLToPath } from "url";
12249
13769
  import { get } from "https";
12250
13770
  function httpsGet(url) {
12251
- return new Promise((resolve4, reject) => {
12252
- const req = get(url, { headers: { "User-Agent": "robloxstudio-mcp-inspector" } }, resolve4);
13771
+ return new Promise((resolve5, reject) => {
13772
+ const req = get(url, { headers: { "User-Agent": "robloxstudio-mcp-inspector" } }, resolve5);
12253
13773
  req.on("error", reject);
12254
13774
  req.setTimeout(TIMEOUT_MS, () => {
12255
13775
  req.destroy(new Error(`Request timed out after ${TIMEOUT_MS}ms`));
@@ -12267,21 +13787,21 @@ async function download(url, dest, redirects = 0) {
12267
13787
  if (res.statusCode !== 200) {
12268
13788
  throw new Error(`Download failed: HTTP ${res.statusCode}`);
12269
13789
  }
12270
- return new Promise((resolve4, reject) => {
13790
+ return new Promise((resolve5, reject) => {
12271
13791
  const file = createWriteStream(dest);
12272
- const cleanup = (err) => {
13792
+ const cleanup = (err2) => {
12273
13793
  file.close(() => {
12274
13794
  try {
12275
13795
  unlinkSync2(dest);
12276
13796
  } catch {
12277
13797
  }
12278
- reject(err);
13798
+ reject(err2);
12279
13799
  });
12280
13800
  };
12281
13801
  res.pipe(file);
12282
13802
  file.on("finish", () => {
12283
13803
  file.close();
12284
- resolve4();
13804
+ resolve5();
12285
13805
  });
12286
13806
  file.on("error", cleanup);
12287
13807
  res.on("error", cleanup);
@@ -12304,7 +13824,7 @@ function prepareInstall({
12304
13824
  warn
12305
13825
  }) {
12306
13826
  const pluginsFolder = getPluginsFolder();
12307
- if (!existsSync6(pluginsFolder)) {
13827
+ if (!existsSync7(pluginsFolder)) {
12308
13828
  mkdirSync5(pluginsFolder, { recursive: true });
12309
13829
  }
12310
13830
  handleVariantConflict({
@@ -12317,23 +13837,23 @@ function prepareInstall({
12317
13837
  return pluginsFolder;
12318
13838
  }
12319
13839
  function bundledAssetPath() {
12320
- const currentDir = dirname4(fileURLToPath(import.meta.url));
13840
+ const currentDir = dirname5(fileURLToPath(import.meta.url));
12321
13841
  const candidates = [
12322
- join6(currentDir, "..", "studio-plugin", ASSET_NAME),
12323
- join6(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
13842
+ join7(currentDir, "..", "studio-plugin", ASSET_NAME),
13843
+ join7(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
12324
13844
  ];
12325
- return candidates.find((candidate) => existsSync6(candidate)) ?? null;
13845
+ return candidates.find((candidate) => existsSync7(candidate)) ?? null;
12326
13846
  }
12327
13847
  function packageVersion() {
12328
- const currentDir = dirname4(fileURLToPath(import.meta.url));
12329
- const pkg = JSON.parse(readFileSync7(join6(currentDir, "..", "package.json"), "utf8"));
13848
+ const currentDir = dirname5(fileURLToPath(import.meta.url));
13849
+ const pkg = JSON.parse(readFileSync8(join7(currentDir, "..", "package.json"), "utf8"));
12330
13850
  if (!pkg.version) {
12331
13851
  throw new Error("Package version not found");
12332
13852
  }
12333
13853
  return pkg.version;
12334
13854
  }
12335
13855
  function bundledPluginVersion(source) {
12336
- const match = readFileSync7(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
13856
+ const match = readFileSync8(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
12337
13857
  return match ? match[1] : null;
12338
13858
  }
12339
13859
  function assertBundledPluginVersion(source) {
@@ -12346,9 +13866,9 @@ function assertBundledPluginVersion(source) {
12346
13866
  }
12347
13867
  }
12348
13868
  function filesMatch(a, b) {
12349
- if (!existsSync6(b)) return false;
12350
- const aBytes = readFileSync7(a);
12351
- const bBytes = readFileSync7(b);
13869
+ if (!existsSync7(b)) return false;
13870
+ const aBytes = readFileSync8(a);
13871
+ const bBytes = readFileSync8(b);
12352
13872
  return aBytes.length === bBytes.length && aBytes.equals(bBytes);
12353
13873
  }
12354
13874
  async function installBundledPlugin(options = {}) {
@@ -12361,7 +13881,7 @@ async function installBundledPlugin(options = {}) {
12361
13881
  }
12362
13882
  assertBundledPluginVersion(source);
12363
13883
  const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
12364
- const dest = join6(pluginsFolder, ASSET_NAME);
13884
+ const dest = join7(pluginsFolder, ASSET_NAME);
12365
13885
  if (filesMatch(source, dest)) return;
12366
13886
  copyFileSync2(source, dest);
12367
13887
  log(`Installed ${ASSET_NAME} to ${dest}`);
@@ -12374,7 +13894,7 @@ async function installPlugin(options = {}) {
12374
13894
  const bundled = bundledAssetPath();
12375
13895
  if (bundled) {
12376
13896
  assertBundledPluginVersion(bundled);
12377
- const dest2 = join6(pluginsFolder, ASSET_NAME);
13897
+ const dest2 = join7(pluginsFolder, ASSET_NAME);
12378
13898
  if (filesMatch(bundled, dest2)) {
12379
13899
  log(`${ASSET_NAME} already installed.`);
12380
13900
  return;
@@ -12389,7 +13909,7 @@ async function installPlugin(options = {}) {
12389
13909
  if (!asset) {
12390
13910
  throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
12391
13911
  }
12392
- const dest = join6(pluginsFolder, ASSET_NAME);
13912
+ const dest = join7(pluginsFolder, ASSET_NAME);
12393
13913
  log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
12394
13914
  await download(asset.browser_download_url, dest);
12395
13915
  log(`Installed to ${dest}`);
@@ -12412,8 +13932,8 @@ init_dist();
12412
13932
  import { createRequire } from "module";
12413
13933
  if (process.argv.includes("--install-plugin")) {
12414
13934
  const { installPlugin: installPlugin2 } = await Promise.resolve().then(() => (init_install_plugin(), install_plugin_exports));
12415
- await installPlugin2().catch((err) => {
12416
- console.error(err instanceof Error ? err.message : String(err));
13935
+ await installPlugin2().catch((err2) => {
13936
+ console.error(err2 instanceof Error ? err2.message : String(err2));
12417
13937
  process.exitCode = 1;
12418
13938
  });
12419
13939
  } else {
@@ -12422,9 +13942,9 @@ if (process.argv.includes("--install-plugin")) {
12422
13942
  await installBundledPlugin2({
12423
13943
  log: (message) => console.error(`[install-plugin] ${message}`),
12424
13944
  warn: (message) => console.error(message)
12425
- }).catch((err) => {
13945
+ }).catch((err2) => {
12426
13946
  console.error(
12427
- `[install-plugin] Auto-install skipped: ${err instanceof Error ? err.message : String(err)}`
13947
+ `[install-plugin] Auto-install skipped: ${err2 instanceof Error ? err2.message : String(err2)}`
12428
13948
  );
12429
13949
  });
12430
13950
  }