@chrrxs/robloxstudio-mcp-inspector 2.19.0 → 2.19.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
@@ -451,13 +451,81 @@ var init_bridge_service = __esm({
451
451
  }
452
452
  });
453
453
 
454
+ // ../core/dist/mcp-compat.js
455
+ import { ErrorCode, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, McpError, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
456
+ function registerEmptyResourceShim(server) {
457
+ server.registerCapabilities({ resources: {} });
458
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
459
+ resources: []
460
+ }));
461
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
462
+ resourceTemplates: []
463
+ }));
464
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
465
+ throw new McpError(ErrorCode.InvalidParams, `Resource ${request.params.uri} not found`);
466
+ });
467
+ }
468
+ var init_mcp_compat = __esm({
469
+ "../core/dist/mcp-compat.js"() {
470
+ "use strict";
471
+ }
472
+ });
473
+
454
474
  // ../core/dist/http-server.js
455
475
  import express from "express";
456
476
  import cors from "cors";
457
477
  import http from "http";
458
478
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
459
479
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
460
- import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
480
+ import { CallToolRequestSchema, ErrorCode as ErrorCode2, ListToolsRequestSchema, McpError as McpError2 } from "@modelcontextprotocol/sdk/types.js";
481
+ function parseLineRange(lineRange) {
482
+ const validLine = (line) => line === void 0 || line >= 1;
483
+ if (typeof lineRange === "string") {
484
+ const ranged = lineRange.match(/^\s*(\d+)?\s*[-:]\s*(\d+)?\s*$/);
485
+ if (ranged) {
486
+ const s = ranged[1] !== void 0 ? parseInt(ranged[1], 10) : void 0;
487
+ const e = ranged[2] !== void 0 ? parseInt(ranged[2], 10) : void 0;
488
+ if (!validLine(s) || !validLine(e))
489
+ return void 0;
490
+ if (s !== void 0 && e !== void 0 && s > e)
491
+ return void 0;
492
+ if (s !== void 0 || e !== void 0)
493
+ return { startLine: s, endLine: e };
494
+ }
495
+ const single = lineRange.match(/^\s*(\d+)\s*$/);
496
+ if (single) {
497
+ const n = parseInt(single[1], 10);
498
+ if (n < 1)
499
+ return void 0;
500
+ return { startLine: n, endLine: n };
501
+ }
502
+ }
503
+ return void 0;
504
+ }
505
+ function optionalLineRange(body, toolName) {
506
+ if (body.line_range === void 0)
507
+ return {};
508
+ const parsed = parseLineRange(body.line_range);
509
+ if (!parsed)
510
+ throw new Error(`${toolName} line_range must be a string like "42", "10-20", "10-", or "-20"`);
511
+ return parsed;
512
+ }
513
+ function optionalLineAnchor(body, toolName) {
514
+ const parsed = optionalLineRange(body, toolName);
515
+ if (parsed.startLine === void 0 && parsed.endLine === void 0)
516
+ return void 0;
517
+ if (parsed.startLine === void 0 || parsed.endLine === void 0 || parsed.endLine !== parsed.startLine) {
518
+ throw new Error(`${toolName} line_range must be a single line like "42"`);
519
+ }
520
+ return parsed.startLine;
521
+ }
522
+ function requiredClosedLineRange(body, toolName) {
523
+ const parsed = optionalLineRange(body, toolName);
524
+ if (parsed.startLine === void 0 || parsed.endLine === void 0) {
525
+ throw new Error(`${toolName} requires line_range as "start-end" or a single line like "42"`);
526
+ }
527
+ return { startLine: parsed.startLine, endLine: parsed.endLine };
528
+ }
461
529
  function createHttpServer(tools, bridge, allowedTools, serverConfig) {
462
530
  const app = express();
463
531
  let mcpServerActive = false;
@@ -707,6 +775,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
707
775
  try {
708
776
  trackMCPActivity();
709
777
  const server = new Server({ name: serverConfig.name, version: serverConfig.version }, { capabilities: { tools: {} } });
778
+ registerEmptyResourceShim(server);
710
779
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
711
780
  tools: filteredTools.map((t) => ({
712
781
  name: t.name,
@@ -717,11 +786,11 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
717
786
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
718
787
  const { name, arguments: args } = request.params;
719
788
  if (allowedTools && !allowedTools.has(name)) {
720
- throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
789
+ throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
721
790
  }
722
791
  const handler = TOOL_HANDLERS[name];
723
792
  if (!handler) {
724
- throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
793
+ throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
725
794
  }
726
795
  try {
727
796
  return await handler(tools, args || {});
@@ -739,9 +808,9 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
739
808
  isError: true
740
809
  };
741
810
  }
742
- if (error instanceof McpError)
811
+ if (error instanceof McpError2)
743
812
  throw error;
744
- throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
813
+ throw new McpError2(ErrorCode2.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
745
814
  }
746
815
  });
747
816
  const transport = new StreamableHTTPServerTransport({
@@ -847,6 +916,7 @@ var init_http_server = __esm({
847
916
  "../core/dist/http-server.js"() {
848
917
  "use strict";
849
918
  init_bridge_service();
919
+ init_mcp_compat();
850
920
  TOOL_HANDLERS = {
851
921
  get_file_tree: (tools, body) => tools.getFileTree(body.path, body.instance_id),
852
922
  search_files: (tools, body) => tools.searchFiles(body.query, body.searchType, body.instance_id),
@@ -877,11 +947,17 @@ var init_http_server = __esm({
877
947
  path: body.path,
878
948
  classFilter: body.classFilter
879
949
  }, body.instance_id),
880
- get_script_source: (tools, body) => tools.getScriptSource(body.instancePath, body.startLine, body.endLine, body.instance_id),
950
+ get_script_source: (tools, body) => {
951
+ const { startLine, endLine } = optionalLineRange(body, "get_script_source");
952
+ return tools.getScriptSource(body.instancePath, startLine, endLine, body.instance_id);
953
+ },
881
954
  set_script_source: (tools, body) => tools.setScriptSource(body.instancePath, body.source, body.instance_id),
882
- edit_script_lines: (tools, body) => tools.editScriptLines(body.instancePath, body.old_string, body.new_string, body.startLine, body.instance_id),
955
+ edit_script_lines: (tools, body) => tools.editScriptLines(body.instancePath, body.old_string, body.new_string, optionalLineAnchor(body, "edit_script_lines"), body.instance_id),
883
956
  insert_script_lines: (tools, body) => tools.insertScriptLines(body.instancePath, body.afterLine, body.newContent, body.instance_id),
884
- delete_script_lines: (tools, body) => tools.deleteScriptLines(body.instancePath, body.startLine, body.endLine, body.instance_id),
957
+ delete_script_lines: (tools, body) => {
958
+ const { startLine, endLine } = requiredClosedLineRange(body, "delete_script_lines");
959
+ return tools.deleteScriptLines(body.instancePath, startLine, endLine, body.instance_id);
960
+ },
885
961
  set_attribute: (tools, body) => tools.setAttribute(body.instancePath, body.attributeName, body.attributeValue, body.valueType, body.instance_id),
886
962
  get_attributes: (tools, body) => tools.getAttributes(body.instancePath, body.instance_id),
887
963
  delete_attribute: (tools, body) => tools.deleteAttribute(body.instancePath, body.attributeName, body.instance_id),
@@ -903,8 +979,8 @@ var init_http_server = __esm({
903
979
  solo_playtest: (tools, body) => tools.soloPlaytest(body.action, body.mode, body.timeout, body.instance_id),
904
980
  start_playtest: (tools, body) => tools.startPlaytest(body.mode, body.numPlayers, body.instance_id),
905
981
  stop_playtest: (tools, body) => tools.stopPlaytest(body.instance_id),
906
- multiplayer_playtest: (tools, body) => tools.multiplayerPlaytest(body.action, body.numPlayers, body.target, body.testArgs, body.value, body.timeout, body.instance_id),
907
- multiplayer_test_start: (tools, body) => tools.multiplayerTestStart(body.numPlayers, body.testArgs, body.timeout, body.instance_id),
982
+ multiplayer_playtest: (tools, body) => tools.multiplayerPlaytest(body.action, body.numPlayers, body.target, body.testArgs, body.value, body.timeout, body.instance_id, body.force),
983
+ multiplayer_test_start: (tools, body) => tools.multiplayerTestStart(body.numPlayers, body.testArgs, body.timeout, body.instance_id, body.force),
908
984
  multiplayer_test_state: (tools, body) => tools.multiplayerTestState(body.instance_id),
909
985
  multiplayer_test_add_players: (tools, body) => tools.multiplayerTestAddPlayers(body.numPlayers, body.timeout, body.instance_id),
910
986
  multiplayer_test_leave_client: (tools, body) => tools.multiplayerTestLeaveClient(body.target, body.timeout, body.instance_id),
@@ -1739,11 +1815,300 @@ var init_roblox_cookie_client = __esm({
1739
1815
  }
1740
1816
  });
1741
1817
 
1742
- // ../core/dist/studio-instance-manager.js
1743
- import { execFileSync, spawn } from "child_process";
1744
- import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync } from "fs";
1818
+ // ../core/dist/managed-instance-registry.js
1819
+ import * as fs from "fs";
1745
1820
  import * as os from "os";
1746
1821
  import * as path from "path";
1822
+ function defaultManagedInstanceRegistryDir() {
1823
+ if (process.env.ROBLOXSTUDIO_MCP_MANAGED_INSTANCE_REGISTRY_DIR) {
1824
+ return process.env.ROBLOXSTUDIO_MCP_MANAGED_INSTANCE_REGISTRY_DIR;
1825
+ }
1826
+ if (process.platform === "win32" && process.env.LOCALAPPDATA) {
1827
+ return path.join(process.env.LOCALAPPDATA, "robloxstudio-mcp", "managed-instances", "v1");
1828
+ }
1829
+ if (process.platform === "darwin") {
1830
+ return path.join(os.homedir(), "Library", "Application Support", "robloxstudio-mcp", "managed-instances", "v1");
1831
+ }
1832
+ return path.join(os.homedir(), ".local", "state", "robloxstudio-mcp", "managed-instances", "v1");
1833
+ }
1834
+ function sleepSync(ms) {
1835
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1836
+ }
1837
+ function ymd(timestamp) {
1838
+ return new Date(timestamp).toISOString().slice(0, 10);
1839
+ }
1840
+ function eventLogDate(name) {
1841
+ const match = name.match(/^events-(\d{4}-\d{2}-\d{2})\.jsonl$/);
1842
+ return match?.[1];
1843
+ }
1844
+ function isRecord(value) {
1845
+ if (!value || typeof value !== "object")
1846
+ return false;
1847
+ const record = value;
1848
+ 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";
1849
+ }
1850
+ var REGISTRY_VERSION, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, EVENT_RETENTION_DAYS, ManagedInstanceRegistry;
1851
+ var init_managed_instance_registry = __esm({
1852
+ "../core/dist/managed-instance-registry.js"() {
1853
+ "use strict";
1854
+ REGISTRY_VERSION = 1;
1855
+ LOCK_STALE_MS = 1e4;
1856
+ LOCK_RETRY_MS = 25;
1857
+ LOCK_TIMEOUT_MS = 5e3;
1858
+ EVENT_RETENTION_DAYS = 2;
1859
+ ManagedInstanceRegistry = class {
1860
+ dir;
1861
+ constructor(dir = defaultManagedInstanceRegistryDir()) {
1862
+ this.dir = dir;
1863
+ }
1864
+ upsert(record) {
1865
+ this.withLock(() => this.writeRecordUnlocked(record));
1866
+ }
1867
+ attachInstanceId(recordId, instanceId) {
1868
+ this.withLock(() => {
1869
+ const record = this.readRecordUnlocked(recordId);
1870
+ if (!record)
1871
+ return;
1872
+ record.instanceId = instanceId;
1873
+ record.attachedAt = Date.now();
1874
+ this.writeRecordUnlocked(record);
1875
+ });
1876
+ }
1877
+ findOpenByInstanceId(instanceId, options) {
1878
+ return this.withLock(() => {
1879
+ this.sweepUnlocked(options);
1880
+ return this.readOpenRecordsUnlocked().find((record) => record.instanceId === instanceId);
1881
+ });
1882
+ }
1883
+ findAnyByInstanceId(instanceId) {
1884
+ return this.withLock(() => this.readRecordsUnlocked().find((record) => record.instanceId === instanceId));
1885
+ }
1886
+ listOpen(options) {
1887
+ return this.withLock(() => {
1888
+ this.sweepUnlocked(options);
1889
+ return this.readOpenRecordsUnlocked();
1890
+ });
1891
+ }
1892
+ markClosed(recordId, closedAt = Date.now()) {
1893
+ this.withLock(() => {
1894
+ const record = this.readRecordUnlocked(recordId);
1895
+ if (!record)
1896
+ return;
1897
+ record.closedAt = closedAt;
1898
+ this.writeRecordUnlocked(record);
1899
+ });
1900
+ }
1901
+ delete(recordId) {
1902
+ this.withLock(() => this.deleteRecordUnlocked(recordId));
1903
+ }
1904
+ sweep(options) {
1905
+ this.withLock(() => this.sweepUnlocked(options));
1906
+ }
1907
+ logEvent(event, now = Date.now()) {
1908
+ this.withLock(() => this.appendEventUnlocked(event, now));
1909
+ }
1910
+ withLock(fn) {
1911
+ this.ensureDir();
1912
+ const lockDir = path.join(this.dir, ".lock");
1913
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
1914
+ while (true) {
1915
+ try {
1916
+ fs.mkdirSync(lockDir);
1917
+ break;
1918
+ } catch (error) {
1919
+ const code = error.code;
1920
+ if (code !== "EEXIST")
1921
+ throw error;
1922
+ try {
1923
+ const stat = fs.statSync(lockDir);
1924
+ if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
1925
+ fs.rmSync(lockDir, { recursive: true, force: true });
1926
+ continue;
1927
+ }
1928
+ } catch {
1929
+ continue;
1930
+ }
1931
+ if (Date.now() > deadline) {
1932
+ throw new Error(`Timed out waiting for managed instance registry lock: ${lockDir}`);
1933
+ }
1934
+ sleepSync(LOCK_RETRY_MS);
1935
+ }
1936
+ }
1937
+ try {
1938
+ return fn();
1939
+ } finally {
1940
+ fs.rmSync(lockDir, { recursive: true, force: true });
1941
+ }
1942
+ }
1943
+ ensureDir() {
1944
+ fs.mkdirSync(this.dir, { recursive: true });
1945
+ }
1946
+ recordPath(recordId) {
1947
+ return path.join(this.dir, `${recordId}.json`);
1948
+ }
1949
+ recordFilesUnlocked() {
1950
+ return fs.readdirSync(this.dir).filter((name) => name.endsWith(".json")).map((name) => path.join(this.dir, name));
1951
+ }
1952
+ readRecordUnlocked(recordId) {
1953
+ try {
1954
+ const parsed = JSON.parse(fs.readFileSync(this.recordPath(recordId), "utf8"));
1955
+ return isRecord(parsed) ? parsed : void 0;
1956
+ } catch {
1957
+ return void 0;
1958
+ }
1959
+ }
1960
+ readOpenRecordsUnlocked() {
1961
+ return this.readRecordsUnlocked().filter((record) => record.closedAt === void 0);
1962
+ }
1963
+ readRecordsUnlocked() {
1964
+ const records = [];
1965
+ for (const file of this.recordFilesUnlocked()) {
1966
+ try {
1967
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
1968
+ if (!isRecord(parsed))
1969
+ continue;
1970
+ records.push(parsed);
1971
+ } catch {
1972
+ }
1973
+ }
1974
+ return records;
1975
+ }
1976
+ writeRecordUnlocked(record) {
1977
+ this.ensureDir();
1978
+ const finalPath = this.recordPath(record.recordId);
1979
+ const tmpPath = path.join(this.dir, `${record.recordId}.${process.pid}.${Date.now()}.tmp`);
1980
+ const fd = fs.openSync(tmpPath, "w");
1981
+ try {
1982
+ fs.writeFileSync(fd, `${JSON.stringify(record, null, 2)}
1983
+ `, "utf8");
1984
+ fs.fsyncSync(fd);
1985
+ } finally {
1986
+ fs.closeSync(fd);
1987
+ }
1988
+ fs.renameSync(tmpPath, finalPath);
1989
+ }
1990
+ deleteRecordUnlocked(recordId) {
1991
+ fs.rmSync(this.recordPath(recordId), { force: true });
1992
+ }
1993
+ appendEventUnlocked(event, now) {
1994
+ const file = path.join(this.dir, `events-${ymd(now)}.jsonl`);
1995
+ fs.appendFileSync(file, `${JSON.stringify({
1996
+ ts: new Date(now).toISOString(),
1997
+ ...event
1998
+ })}
1999
+ `, "utf8");
2000
+ }
2001
+ cleanupOldEventLogsUnlocked(now) {
2002
+ const cutoff = ymd(now - EVENT_RETENTION_DAYS * 24 * 60 * 60 * 1e3);
2003
+ for (const name of fs.readdirSync(this.dir)) {
2004
+ const date = eventLogDate(name);
2005
+ if (!date || date >= cutoff)
2006
+ continue;
2007
+ try {
2008
+ fs.rmSync(path.join(this.dir, name), { force: true });
2009
+ } catch {
2010
+ }
2011
+ }
2012
+ }
2013
+ cleanupRecord(options, record) {
2014
+ try {
2015
+ options.cleanupRecord?.(record);
2016
+ } catch {
2017
+ this.appendEventUnlocked({
2018
+ event: "registry_cleanup_failed",
2019
+ recordId: record.recordId,
2020
+ instanceId: record.instanceId,
2021
+ source: record.source,
2022
+ reason: "cleanup_record_error"
2023
+ }, options.now ?? Date.now());
2024
+ }
2025
+ }
2026
+ sweepUnlocked(options) {
2027
+ const now = options.now ?? Date.now();
2028
+ this.cleanupOldEventLogsUnlocked(now);
2029
+ for (const file of this.recordFilesUnlocked()) {
2030
+ let parsed;
2031
+ try {
2032
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
2033
+ } catch {
2034
+ fs.rmSync(file, { force: true });
2035
+ this.appendEventUnlocked({
2036
+ event: "registry_pruned_malformed_record",
2037
+ reason: "parse_error",
2038
+ action: "deleted_record"
2039
+ }, now);
2040
+ continue;
2041
+ }
2042
+ if (!parsed || typeof parsed !== "object") {
2043
+ fs.rmSync(file, { force: true });
2044
+ this.appendEventUnlocked({
2045
+ event: "registry_pruned_malformed_record",
2046
+ reason: "invalid_shape",
2047
+ action: "deleted_record"
2048
+ }, now);
2049
+ continue;
2050
+ }
2051
+ const version = parsed.version;
2052
+ if (typeof version === "number" && version > REGISTRY_VERSION)
2053
+ continue;
2054
+ if (!isRecord(parsed)) {
2055
+ fs.rmSync(file, { force: true });
2056
+ this.appendEventUnlocked({
2057
+ event: "registry_pruned_malformed_record",
2058
+ reason: "invalid_shape",
2059
+ action: "deleted_record"
2060
+ }, now);
2061
+ continue;
2062
+ }
2063
+ if (parsed.closedAt !== void 0) {
2064
+ fs.rmSync(file, { force: true });
2065
+ this.appendEventUnlocked({
2066
+ event: "registry_pruned_closed_record",
2067
+ recordId: parsed.recordId,
2068
+ instanceId: parsed.instanceId,
2069
+ source: parsed.source,
2070
+ reason: "closed_at_present",
2071
+ action: "deleted_record"
2072
+ }, now);
2073
+ continue;
2074
+ }
2075
+ if (parsed.bootId !== options.currentBootId) {
2076
+ this.cleanupRecord(options, parsed);
2077
+ fs.rmSync(file, { force: true });
2078
+ this.appendEventUnlocked({
2079
+ event: "registry_pruned_previous_boot",
2080
+ recordId: parsed.recordId,
2081
+ instanceId: parsed.instanceId,
2082
+ source: parsed.source,
2083
+ reason: "boot_id_changed",
2084
+ action: "deleted_record_and_cleaned_baseplate"
2085
+ }, now);
2086
+ continue;
2087
+ }
2088
+ if (options.isProcessRunning && (parsed.nativeProcessId || parsed.spawnPid) && !options.isProcessRunning(parsed)) {
2089
+ this.cleanupRecord(options, parsed);
2090
+ fs.rmSync(file, { force: true });
2091
+ this.appendEventUnlocked({
2092
+ event: "registry_pruned_stale_process",
2093
+ recordId: parsed.recordId,
2094
+ instanceId: parsed.instanceId,
2095
+ source: parsed.source,
2096
+ reason: "pid_not_running",
2097
+ action: "deleted_record_and_cleaned_baseplate"
2098
+ }, now);
2099
+ }
2100
+ }
2101
+ }
2102
+ };
2103
+ }
2104
+ });
2105
+
2106
+ // ../core/dist/studio-instance-manager.js
2107
+ import { execFileSync, spawn } from "child_process";
2108
+ import { copyFileSync, existsSync, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, rmSync as rmSync2, statSync as statSync2 } from "fs";
2109
+ import { randomUUID } from "crypto";
2110
+ import * as os2 from "os";
2111
+ import * as path2 from "path";
1747
2112
  function run(command, args, options = {}) {
1748
2113
  return execFileSync(command, args, {
1749
2114
  encoding: "utf8",
@@ -1755,7 +2120,7 @@ function isWsl() {
1755
2120
  if (process.platform !== "linux")
1756
2121
  return false;
1757
2122
  try {
1758
- return /microsoft|wsl/i.test(readFileSync("/proc/version", "utf8"));
2123
+ return /microsoft|wsl/i.test(readFileSync2("/proc/version", "utf8"));
1759
2124
  } catch {
1760
2125
  return false;
1761
2126
  }
@@ -1784,7 +2149,7 @@ function toWslPath(windowsPath) {
1784
2149
  return run("wslpath", ["-u", windowsPath]);
1785
2150
  }
1786
2151
  function toStudioLaunchArg(arg) {
1787
- if (!isWsl() || !path.isAbsolute(arg) || !existsSync(arg))
2152
+ if (!isWsl() || !path2.isAbsolute(arg) || !existsSync(arg))
1788
2153
  return arg;
1789
2154
  return run("wslpath", ["-w", arg]);
1790
2155
  }
@@ -1793,21 +2158,21 @@ function resolveEntrypointDir() {
1793
2158
  if (!entrypoint)
1794
2159
  return void 0;
1795
2160
  try {
1796
- return path.dirname(realpathSync(entrypoint));
2161
+ return path2.dirname(realpathSync(entrypoint));
1797
2162
  } catch {
1798
- return path.dirname(path.resolve(entrypoint));
2163
+ return path2.dirname(path2.resolve(entrypoint));
1799
2164
  }
1800
2165
  }
1801
2166
  function resolveBaseplateTemplatePath() {
1802
2167
  const entrypointDir = resolveEntrypointDir();
1803
2168
  const candidates = [
1804
2169
  ...entrypointDir ? [
1805
- path.join(entrypointDir, "assets", BASEPLATE_TEMPLATE_NAME),
1806
- path.join(entrypointDir, "..", "assets", BASEPLATE_TEMPLATE_NAME)
2170
+ path2.join(entrypointDir, "assets", BASEPLATE_TEMPLATE_NAME),
2171
+ path2.join(entrypointDir, "..", "assets", BASEPLATE_TEMPLATE_NAME)
1807
2172
  ] : [],
1808
- path.join(process.cwd(), "packages", "core", "assets", BASEPLATE_TEMPLATE_NAME),
1809
- path.join(process.cwd(), "packages", "robloxstudio-mcp", "dist", "assets", BASEPLATE_TEMPLATE_NAME),
1810
- path.join(process.cwd(), "assets", BASEPLATE_TEMPLATE_NAME)
2173
+ path2.join(process.cwd(), "packages", "core", "assets", BASEPLATE_TEMPLATE_NAME),
2174
+ path2.join(process.cwd(), "packages", "robloxstudio-mcp", "dist", "assets", BASEPLATE_TEMPLATE_NAME),
2175
+ path2.join(process.cwd(), "assets", BASEPLATE_TEMPLATE_NAME)
1811
2176
  ];
1812
2177
  for (const candidate of candidates) {
1813
2178
  if (existsSync(candidate))
@@ -1816,22 +2181,22 @@ function resolveBaseplateTemplatePath() {
1816
2181
  throw new Error(`Baseplate template not found. Expected ${BASEPLATE_TEMPLATE_NAME} in one of: ${candidates.join(", ")}`);
1817
2182
  }
1818
2183
  function createBaseplatePlaceFile() {
1819
- mkdirSync(BASEPLATE_TEMP_DIR, { recursive: true });
1820
- const file = path.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
2184
+ mkdirSync2(BASEPLATE_TEMP_DIR, { recursive: true });
2185
+ const file = path2.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
1821
2186
  copyFileSync(resolveBaseplateTemplatePath(), file);
1822
2187
  return file;
1823
2188
  }
1824
2189
  function isGeneratedBaseplatePlaceFile(file) {
1825
- const resolvedFile = path.resolve(file);
1826
- return path.dirname(resolvedFile) === path.resolve(BASEPLATE_TEMP_DIR) && BASEPLATE_TEMP_NAME.test(path.basename(resolvedFile));
2190
+ const resolvedFile = path2.resolve(file);
2191
+ return path2.dirname(resolvedFile) === path2.resolve(BASEPLATE_TEMP_DIR) && BASEPLATE_TEMP_NAME.test(path2.basename(resolvedFile));
1827
2192
  }
1828
2193
  function cleanupManagedBaseplateFiles(record) {
1829
2194
  if (record.source !== "baseplate" || !record.localPlaceFile)
1830
2195
  return;
1831
2196
  if (!isGeneratedBaseplatePlaceFile(record.localPlaceFile))
1832
2197
  return;
1833
- rmSync(record.localPlaceFile, { force: true });
1834
- rmSync(`${record.localPlaceFile}.lock`, { force: true });
2198
+ rmSync2(record.localPlaceFile, { force: true });
2199
+ rmSync2(`${record.localPlaceFile}.lock`, { force: true });
1835
2200
  }
1836
2201
  function prepareStudioLaunchOptions(options) {
1837
2202
  if (options.source !== "baseplate" || options.localPlaceFile)
@@ -1851,11 +2216,11 @@ function resolveStudioExe() {
1851
2216
  throw new Error("Roblox Studio executable auto-discovery is only supported on Windows, WSL, and macOS. Set ROBLOX_STUDIO_EXE.");
1852
2217
  }
1853
2218
  const localAppData = windowsLocalAppData();
1854
- const root = localAppData ? path.join(toWslPath(localAppData), "Roblox", "Versions") : path.join(os.homedir(), "AppData", "Local", "Roblox", "Versions");
2219
+ const root = localAppData ? path2.join(toWslPath(localAppData), "Roblox", "Versions") : path2.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
1855
2220
  if (!existsSync(root)) {
1856
2221
  throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
1857
2222
  }
1858
- const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
2223
+ const candidates = readdirSync2(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync(candidate)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
1859
2224
  if (candidates.length === 0) {
1860
2225
  throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
1861
2226
  }
@@ -1871,14 +2236,14 @@ function listStudioProcesses() {
1871
2236
  }
1872
2237
  return out2.split("\n").filter(Boolean).map((line) => {
1873
2238
  const [pid, ...rest] = line.trim().split(/\s+/);
1874
- return { Id: Number(pid), Path: rest.join(" "), MainWindowTitle: "" };
2239
+ return { Id: Number(pid), Name: "RobloxStudio", Path: rest.join(" "), MainWindowTitle: "" };
1875
2240
  }).filter((proc) => Number.isFinite(proc.Id));
1876
2241
  }
1877
2242
  if (process.platform !== "win32" && !isWsl())
1878
2243
  return [];
1879
2244
  let out = "";
1880
2245
  try {
1881
- out = powershell("Get-Process RobloxStudioBeta -ErrorAction SilentlyContinue | Select-Object Id,Path,MainWindowTitle | ConvertTo-Json -Compress");
2246
+ out = powershell("Get-Process RobloxStudioBeta -ErrorAction SilentlyContinue | Select-Object Id,Name,Path,MainWindowTitle | ConvertTo-Json -Compress");
1882
2247
  } catch {
1883
2248
  return [];
1884
2249
  }
@@ -1887,6 +2252,27 @@ function listStudioProcesses() {
1887
2252
  const parsed = JSON.parse(out);
1888
2253
  return Array.isArray(parsed) ? parsed : [parsed];
1889
2254
  }
2255
+ function currentBootId() {
2256
+ if (process.platform === "linux") {
2257
+ try {
2258
+ return readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
2259
+ } catch {
2260
+ }
2261
+ }
2262
+ if (process.platform === "win32" || isWsl()) {
2263
+ try {
2264
+ return powershell('(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().ToString("o")');
2265
+ } catch {
2266
+ }
2267
+ }
2268
+ if (process.platform === "darwin") {
2269
+ try {
2270
+ return run("sysctl", ["-n", "kern.boottime"]);
2271
+ } catch {
2272
+ }
2273
+ }
2274
+ return `${process.platform}:${os2.hostname()}:unknown-boot`;
2275
+ }
1890
2276
  function buildStudioLaunchArgs(options) {
1891
2277
  switch (options.source) {
1892
2278
  case "baseplate":
@@ -1899,13 +2285,13 @@ function buildStudioLaunchArgs(options) {
1899
2285
  if (!options.placeId)
1900
2286
  throw new Error('place_id is required when source="published_place".');
1901
2287
  if (!options.universeId)
1902
- throw new Error('universe_id is required when source="published_place".');
2288
+ throw new Error('Derived universe id is required when source="published_place".');
1903
2289
  return ["--task", "EditPlace", "--placeId", String(options.placeId), "--universeId", String(options.universeId)];
1904
2290
  case "place_revision":
1905
2291
  if (!options.placeId)
1906
2292
  throw new Error('place_id is required when source="place_revision".');
1907
2293
  if (!options.universeId)
1908
- throw new Error('universe_id is required when source="place_revision".');
2294
+ throw new Error('Derived universe id is required when source="place_revision".');
1909
2295
  if (!options.placeVersion)
1910
2296
  throw new Error('place_version is required when launching source="place_revision".');
1911
2297
  return [
@@ -1923,39 +2309,69 @@ function buildStudioLaunchArgs(options) {
1923
2309
  function delay(ms) {
1924
2310
  return new Promise((resolve4) => setTimeout(resolve4, ms));
1925
2311
  }
2312
+ function basenameAny(filePath) {
2313
+ return path2.basename(filePath.replace(/\\/g, "/"));
2314
+ }
1926
2315
  var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, StudioInstanceManager;
1927
2316
  var init_studio_instance_manager = __esm({
1928
2317
  "../core/dist/studio-instance-manager.js"() {
1929
2318
  "use strict";
1930
- BASEPLATE_TEMP_DIR = path.join(os.tmpdir(), "robloxstudio-mcp-baseplates");
2319
+ init_managed_instance_registry();
2320
+ BASEPLATE_TEMP_DIR = path2.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
1931
2321
  BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
1932
2322
  BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
1933
2323
  StudioInstanceManager = class {
1934
2324
  managedByInstanceId = /* @__PURE__ */ new Map();
1935
2325
  pending = /* @__PURE__ */ new Set();
2326
+ registry;
2327
+ processAdapter;
2328
+ constructor(options = {}) {
2329
+ this.registry = options.registry ?? new ManagedInstanceRegistry(options.registryDir);
2330
+ this.processAdapter = options.processAdapter ?? {};
2331
+ }
1936
2332
  list() {
1937
- return [...this.managedByInstanceId.values(), ...this.pending].filter((instance, index, all) => all.indexOf(instance) === index);
2333
+ this.sweepRegistry();
2334
+ const records = [...this.managedByInstanceId.values(), ...this.pending];
2335
+ for (const registryRecord of this.registry.listOpen(this.registrySweepOptions())) {
2336
+ const record = this.fromRegistryRecord(registryRecord);
2337
+ if (records.some((existing) => record.recordId && existing.recordId === record.recordId || record.instanceId && existing.instanceId === record.instanceId)) {
2338
+ continue;
2339
+ }
2340
+ records.push(record);
2341
+ }
2342
+ return records.filter((instance, index, all) => all.indexOf(instance) === index);
1938
2343
  }
1939
2344
  get(instanceId) {
1940
- return this.managedByInstanceId.get(instanceId);
2345
+ this.sweepRegistry();
2346
+ const memoryRecord = this.managedByInstanceId.get(instanceId);
2347
+ if (memoryRecord)
2348
+ return memoryRecord;
2349
+ const registryRecord = this.registry.findOpenByInstanceId(instanceId, this.registrySweepOptions());
2350
+ return registryRecord ? this.fromRegistryRecord(registryRecord) : void 0;
1941
2351
  }
1942
2352
  attachInstanceId(record, instanceId) {
1943
2353
  record.instanceId = instanceId;
1944
2354
  this.pending.delete(record);
1945
2355
  this.managedByInstanceId.set(instanceId, record);
2356
+ if (record.recordId) {
2357
+ this.registry.attachInstanceId(record.recordId, instanceId);
2358
+ }
1946
2359
  }
1947
2360
  async launch(options) {
2361
+ this.sweepRegistry();
1948
2362
  const preparedOptions = prepareStudioLaunchOptions(options);
1949
- const before = new Set(listStudioProcesses().map((proc2) => proc2.Id));
1950
- const exe = resolveStudioExe();
2363
+ const before = new Set(this.listStudioProcesses().map((proc2) => proc2.Id));
2364
+ const exe = this.processAdapter.resolveStudioExe?.() ?? resolveStudioExe();
1951
2365
  const args = buildStudioLaunchArgs(preparedOptions).map(toStudioLaunchArg);
1952
- const proc = spawn(exe, args, {
2366
+ const spawnOptions = {
1953
2367
  cwd: isWsl() && existsSync("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
1954
2368
  detached: true,
1955
2369
  stdio: "ignore"
1956
- });
2370
+ };
2371
+ const proc = this.processAdapter.spawnStudio ? this.processAdapter.spawnStudio(exe, args, spawnOptions) : spawn(exe, args, spawnOptions);
1957
2372
  proc.unref();
1958
2373
  const record = {
2374
+ recordId: randomUUID(),
1959
2375
  source: options.source,
1960
2376
  spawnPid: proc.pid,
1961
2377
  exe,
@@ -1964,43 +2380,119 @@ var init_studio_instance_manager = __esm({
1964
2380
  universeId: preparedOptions.universeId,
1965
2381
  placeVersion: preparedOptions.placeVersion,
1966
2382
  localPlaceFile: preparedOptions.localPlaceFile,
1967
- launchedAt: Date.now()
2383
+ launchedAt: Date.now(),
2384
+ ownerPid: process.pid,
2385
+ bootId: this.getCurrentBootId(),
2386
+ deleteLocalPlaceFileOnClose: options.source === "baseplate"
1968
2387
  };
1969
2388
  this.pending.add(record);
2389
+ this.registry.upsert(this.toRegistryRecord(record));
1970
2390
  const deadline = Date.now() + 5e3;
1971
2391
  while (Date.now() < deadline && record.nativeProcessId === void 0) {
1972
- const created = listStudioProcesses().find((candidate) => !before.has(candidate.Id));
2392
+ const created = this.listStudioProcesses().find((candidate) => !before.has(candidate.Id));
1973
2393
  if (created) {
1974
2394
  record.nativeProcessId = created.Id;
2395
+ this.registry.upsert(this.toRegistryRecord(record));
1975
2396
  break;
1976
2397
  }
1977
2398
  await delay(250);
1978
2399
  }
1979
2400
  if (record.nativeProcessId === void 0 && process.platform !== "win32" && !isWsl()) {
1980
2401
  record.nativeProcessId = proc.pid;
2402
+ this.registry.upsert(this.toRegistryRecord(record));
1981
2403
  }
1982
2404
  return record;
1983
2405
  }
2406
+ closeByInstanceId(instanceId) {
2407
+ const memoryRecord = this.managedByInstanceId.get(instanceId);
2408
+ if (memoryRecord)
2409
+ return this.close(memoryRecord);
2410
+ const registryRecord = this.registry.findAnyByInstanceId(instanceId);
2411
+ if (!registryRecord) {
2412
+ this.sweepRegistry();
2413
+ return { status: "not_found", instanceId };
2414
+ }
2415
+ if (registryRecord.closedAt !== void 0) {
2416
+ this.cleanupManagedRecord(registryRecord);
2417
+ this.registry.delete(registryRecord.recordId);
2418
+ this.registry.logEvent({
2419
+ event: "registry_close_already_stopped",
2420
+ recordId: registryRecord.recordId,
2421
+ instanceId: registryRecord.instanceId,
2422
+ source: registryRecord.source,
2423
+ reason: "closed_at_present",
2424
+ action: "deleted_record_and_cleaned_baseplate"
2425
+ });
2426
+ return { status: "already_closed", instanceId };
2427
+ }
2428
+ if (registryRecord.bootId !== this.getCurrentBootId()) {
2429
+ this.cleanupManagedRecord(registryRecord);
2430
+ this.registry.delete(registryRecord.recordId);
2431
+ this.registry.logEvent({
2432
+ event: "registry_pruned_previous_boot",
2433
+ recordId: registryRecord.recordId,
2434
+ instanceId: registryRecord.instanceId,
2435
+ source: registryRecord.source,
2436
+ reason: "boot_id_changed",
2437
+ action: "deleted_record_and_cleaned_baseplate"
2438
+ });
2439
+ return { status: "already_closed", instanceId };
2440
+ }
2441
+ return this.close(this.fromRegistryRecord(registryRecord));
2442
+ }
1984
2443
  close(record) {
1985
2444
  const processId = record.nativeProcessId ?? record.spawnPid;
1986
2445
  if (!processId) {
1987
2446
  throw new Error(`Cannot close ${record.instanceId ?? "Studio launch"} because its process id was not detected.`);
1988
2447
  }
1989
- if (process.platform === "win32" || isWsl()) {
1990
- powershell(`Stop-Process -Id ${Math.trunc(processId)} -Force -ErrorAction Stop`);
1991
- } else {
1992
- try {
1993
- process.kill(processId, "SIGTERM");
1994
- } catch (error) {
1995
- if (error.code !== "ESRCH")
1996
- throw error;
1997
- }
2448
+ const studioProcess = this.findProcessById(processId);
2449
+ if (!studioProcess) {
2450
+ this.cleanupManagedRecord(record);
2451
+ this.markClosedInMemory(record);
2452
+ this.markClosedInRegistry(record);
2453
+ this.registry.logEvent({
2454
+ event: "registry_close_already_stopped",
2455
+ recordId: record.recordId,
2456
+ instanceId: record.instanceId,
2457
+ source: record.source,
2458
+ reason: "pid_not_running",
2459
+ action: "marked_closed_and_cleaned_baseplate"
2460
+ });
2461
+ return { status: "already_closed", instanceId: record.instanceId };
2462
+ }
2463
+ if (!this.verifyProcessForRecord(record, studioProcess)) {
2464
+ this.registry.logEvent({
2465
+ event: "registry_process_verification_failed",
2466
+ recordId: record.recordId,
2467
+ instanceId: record.instanceId,
2468
+ source: record.source,
2469
+ reason: "identity_mismatch"
2470
+ });
2471
+ throw new Error("Managed Studio process identity could not be verified.");
2472
+ }
2473
+ try {
2474
+ this.closeProcess(processId);
2475
+ } catch (error) {
2476
+ if (this.findProcessById(processId))
2477
+ throw error;
2478
+ this.registry.logEvent({
2479
+ event: "registry_close_already_stopped",
2480
+ recordId: record.recordId,
2481
+ instanceId: record.instanceId,
2482
+ source: record.source,
2483
+ reason: "stop_raced_with_exit",
2484
+ action: "marked_closed_and_cleaned_baseplate"
2485
+ });
2486
+ this.cleanupManagedRecord(record);
2487
+ this.markClosedInMemory(record);
2488
+ this.markClosedInRegistry(record);
2489
+ return { status: "already_closed", instanceId: record.instanceId };
1998
2490
  }
1999
2491
  record.closedAt = Date.now();
2000
- if (record.instanceId)
2001
- this.managedByInstanceId.delete(record.instanceId);
2002
- this.pending.delete(record);
2003
- cleanupManagedBaseplateFiles(record);
2492
+ this.cleanupManagedRecord(record);
2493
+ this.markClosedInMemory(record);
2494
+ this.markClosedInRegistry(record);
2495
+ return { status: "closed", instanceId: record.instanceId };
2004
2496
  }
2005
2497
  closeConnectedInstance(instance) {
2006
2498
  const process2 = this.findProcessForConnectedInstance(instance);
@@ -2010,6 +2502,10 @@ var init_studio_instance_manager = __esm({
2010
2502
  this.closeProcess(process2.Id);
2011
2503
  }
2012
2504
  closeProcess(processId) {
2505
+ if (this.processAdapter.stopProcess) {
2506
+ this.processAdapter.stopProcess(processId);
2507
+ return;
2508
+ }
2013
2509
  if (process.platform === "win32" || isWsl()) {
2014
2510
  powershell(`Stop-Process -Id ${Math.trunc(processId)} -Force -ErrorAction Stop`);
2015
2511
  } else {
@@ -2022,7 +2518,7 @@ var init_studio_instance_manager = __esm({
2022
2518
  }
2023
2519
  }
2024
2520
  findProcessForConnectedInstance(instance) {
2025
- const processes = listStudioProcesses();
2521
+ const processes = this.listStudioProcesses();
2026
2522
  if (processes.length === 0)
2027
2523
  return void 0;
2028
2524
  if (processes.length === 1)
@@ -2041,13 +2537,119 @@ var init_studio_instance_manager = __esm({
2041
2537
  }
2042
2538
  return void 0;
2043
2539
  }
2540
+ listStudioProcesses() {
2541
+ return this.processAdapter.listStudioProcesses?.() ?? listStudioProcesses();
2542
+ }
2543
+ getCurrentBootId() {
2544
+ return this.processAdapter.currentBootId?.() ?? currentBootId();
2545
+ }
2546
+ registrySweepOptions() {
2547
+ return {
2548
+ currentBootId: this.getCurrentBootId(),
2549
+ isProcessRunning: (record) => this.isRegistryProcessRunning(record),
2550
+ cleanupRecord: (record) => this.cleanupManagedRecord(record)
2551
+ };
2552
+ }
2553
+ sweepRegistry() {
2554
+ this.registry.sweep(this.registrySweepOptions());
2555
+ }
2556
+ findProcessById(processId) {
2557
+ return this.listStudioProcesses().find((proc) => proc.Id === processId);
2558
+ }
2559
+ isRegistryProcessRunning(record) {
2560
+ const processId = record.nativeProcessId ?? record.spawnPid;
2561
+ if (!processId)
2562
+ return true;
2563
+ const studioProcess = this.findProcessById(processId);
2564
+ return !!studioProcess && this.verifyProcessForRecord(this.fromRegistryRecord(record), studioProcess);
2565
+ }
2566
+ verifyProcessForRecord(record, studioProcess) {
2567
+ const processName = `${studioProcess.Name ?? ""} ${studioProcess.Path ?? ""}`.toLowerCase();
2568
+ if (!processName.includes("robloxstudio"))
2569
+ return false;
2570
+ const processId = record.nativeProcessId ?? record.spawnPid;
2571
+ if (record.spawnPid && record.spawnPid === processId && studioProcess.Id === processId)
2572
+ return true;
2573
+ const processPath = studioProcess.Path ? path2.normalize(studioProcess.Path).toLowerCase() : "";
2574
+ const exePath = record.exe ? path2.normalize(record.exe).toLowerCase() : "";
2575
+ if (processPath && exePath && (processPath === exePath || basenameAny(processPath) === basenameAny(exePath))) {
2576
+ return true;
2577
+ }
2578
+ const commandLine = studioProcess.CommandLine ?? "";
2579
+ if (record.localPlaceFile && commandLine.includes(path2.basename(record.localPlaceFile)))
2580
+ return true;
2581
+ if (record.placeId !== void 0 && commandLine.includes(String(record.placeId)))
2582
+ return true;
2583
+ return false;
2584
+ }
2585
+ cleanupManagedRecord(record) {
2586
+ if (record.source !== "baseplate")
2587
+ return;
2588
+ cleanupManagedBaseplateFiles({ source: "baseplate", localPlaceFile: record.localPlaceFile });
2589
+ }
2590
+ markClosedInMemory(record) {
2591
+ record.closedAt = record.closedAt ?? Date.now();
2592
+ if (record.instanceId)
2593
+ this.managedByInstanceId.delete(record.instanceId);
2594
+ this.pending.delete(record);
2595
+ }
2596
+ markClosedInRegistry(record) {
2597
+ if (record.recordId)
2598
+ this.registry.markClosed(record.recordId, record.closedAt ?? Date.now());
2599
+ }
2600
+ toRegistryRecord(record) {
2601
+ if (!record.recordId)
2602
+ throw new Error("Managed Studio record is missing recordId.");
2603
+ if (!record.bootId)
2604
+ throw new Error("Managed Studio record is missing bootId.");
2605
+ return {
2606
+ version: 1,
2607
+ recordId: record.recordId,
2608
+ instanceId: record.instanceId,
2609
+ source: record.source,
2610
+ nativeProcessId: record.nativeProcessId,
2611
+ spawnPid: record.spawnPid,
2612
+ exe: record.exe,
2613
+ args: record.args,
2614
+ placeId: record.placeId,
2615
+ universeId: record.universeId,
2616
+ placeVersion: record.placeVersion,
2617
+ localPlaceFile: record.localPlaceFile,
2618
+ deleteLocalPlaceFileOnClose: record.deleteLocalPlaceFileOnClose,
2619
+ launchedAt: record.launchedAt,
2620
+ attachedAt: record.instanceId ? Date.now() : void 0,
2621
+ closedAt: record.closedAt,
2622
+ ownerPid: record.ownerPid,
2623
+ bootId: record.bootId
2624
+ };
2625
+ }
2626
+ fromRegistryRecord(record) {
2627
+ return {
2628
+ recordId: record.recordId,
2629
+ source: record.source,
2630
+ instanceId: record.instanceId,
2631
+ nativeProcessId: record.nativeProcessId,
2632
+ spawnPid: record.spawnPid,
2633
+ exe: record.exe,
2634
+ args: record.args,
2635
+ placeId: record.placeId,
2636
+ universeId: record.universeId,
2637
+ placeVersion: record.placeVersion,
2638
+ localPlaceFile: record.localPlaceFile,
2639
+ launchedAt: record.launchedAt,
2640
+ closedAt: record.closedAt,
2641
+ ownerPid: record.ownerPid,
2642
+ bootId: record.bootId,
2643
+ deleteLocalPlaceFileOnClose: record.deleteLocalPlaceFileOnClose
2644
+ };
2645
+ }
2044
2646
  };
2045
2647
  }
2046
2648
  });
2047
2649
 
2048
2650
  // ../core/dist/image-decode.js
2049
- import * as fs from "fs";
2050
- import * as path2 from "path";
2651
+ import * as fs2 from "fs";
2652
+ import * as path3 from "path";
2051
2653
  import { inflateSync } from "zlib";
2052
2654
  function paethPredictor(a, b, c) {
2053
2655
  const p = a + b - c;
@@ -2205,11 +2807,11 @@ function decodePngToRgba(data) {
2205
2807
  return resizeRgbaNearest({ width, height, rgba });
2206
2808
  }
2207
2809
  function decodeImagePathToRgba(imagePath) {
2208
- const resolved = path2.resolve(imagePath);
2209
- if (!fs.existsSync(resolved)) {
2810
+ const resolved = path3.resolve(imagePath);
2811
+ if (!fs2.existsSync(resolved)) {
2210
2812
  throw new Error(`image_path not found: ${imagePath}`);
2211
2813
  }
2212
- return decodePngToRgba(fs.readFileSync(resolved));
2814
+ return decodePngToRgba(fs2.readFileSync(resolved));
2213
2815
  }
2214
2816
  var PNG_SIGNATURE, MAX_EDITABLE_IMAGE_DIMENSION;
2215
2817
  var init_image_decode = __esm({
@@ -3237,9 +3839,19 @@ var init_png_encoder = __esm({
3237
3839
  });
3238
3840
 
3239
3841
  // ../core/dist/tools/index.js
3240
- import * as fs2 from "fs";
3241
- import * as os2 from "os";
3242
- import * as path3 from "path";
3842
+ import * as fs3 from "fs";
3843
+ import * as os3 from "os";
3844
+ import * as path4 from "path";
3845
+ function multiplayerStopDisabledBody() {
3846
+ return {
3847
+ success: false,
3848
+ error: "multiplayer_stop_disabled",
3849
+ message: MULTIPLAYER_STOP_DISABLED_MESSAGE,
3850
+ reason: "StudioTestService:EndTest does not reliably end StudioTestService multiplayer sessions from MCP right now.",
3851
+ manualCleanupRequired: true,
3852
+ recoveryHint: "Close the Roblox Studio multiplayer test windows manually."
3853
+ };
3854
+ }
3243
3855
  function encodeImageFromRgbaResponse(response, format, quality) {
3244
3856
  if (!response.data || response.width === void 0 || response.height === void 0) {
3245
3857
  throw new Error("Render response missing data, width, or height");
@@ -3316,8 +3928,8 @@ function loadMicroProfilerBaseline(source, sourcePath) {
3316
3928
  if (typeof sourcePath !== "string" || sourcePath === "") {
3317
3929
  throw new Error("baseline_path must be a non-empty string when provided");
3318
3930
  }
3319
- const resolved = path3.resolve(sourcePath);
3320
- const parsed = JSON.parse(fs2.readFileSync(resolved, "utf8"));
3931
+ const resolved = path4.resolve(sourcePath);
3932
+ const parsed = JSON.parse(fs3.readFileSync(resolved, "utf8"));
3321
3933
  const record = asRecord(parsed);
3322
3934
  if (!record)
3323
3935
  throw new Error(`baseline_path did not contain a JSON object: ${resolved}`);
@@ -3765,7 +4377,7 @@ end
3765
4377
  error("Unsupported device simulator operation: " .. tostring(opts.operation), 0)
3766
4378
  `.trim();
3767
4379
  }
3768
- var MAX_INLINE_IMAGE_BYTES, MAX_DEVICE_MATRIX_ENTRIES, MAX_NETWORK_PACKET_LOSS_PERCENT, STUDIO_ASSISTANT_SOURCE_IMAGE_LABEL, NETWORK_PROFILE_KEYS, NETWORK_PROFILES, ZERO_NETWORK_PROFILE, SIMULATION_PERSISTENCE_NOTES, RobloxStudioTools;
4380
+ var MAX_INLINE_IMAGE_BYTES, MAX_DEVICE_MATRIX_ENTRIES, MAX_NETWORK_PACKET_LOSS_PERCENT, STUDIO_ASSISTANT_SOURCE_IMAGE_LABEL, MULTIPLAYER_FORCE_REQUIRED_MESSAGE, MULTIPLAYER_STOP_DISABLED_MESSAGE, NETWORK_PROFILE_KEYS, NETWORK_PROFILES, ZERO_NETWORK_PROFILE, SIMULATION_PERSISTENCE_NOTES, RobloxStudioTools;
3769
4381
  var init_tools = __esm({
3770
4382
  "../core/dist/tools/index.js"() {
3771
4383
  "use strict";
@@ -3782,6 +4394,8 @@ var init_tools = __esm({
3782
4394
  MAX_DEVICE_MATRIX_ENTRIES = 6;
3783
4395
  MAX_NETWORK_PACKET_LOSS_PERCENT = 0.5;
3784
4396
  STUDIO_ASSISTANT_SOURCE_IMAGE_LABEL = "Studio Assistant Source Image";
4397
+ MULTIPLAYER_FORCE_REQUIRED_MESSAGE = "StudioTestService multiplayer stop is currently disabled because StudioTestService:EndTest is broken for this flow. Pass force=true only if you understand you must manually close the multiplayer test windows afterward.";
4398
+ MULTIPLAYER_STOP_DISABLED_MESSAGE = "Multiplayer playtest stop/end is disabled because StudioTestService:EndTest is currently broken for this flow. Manually close the Studio multiplayer test windows instead.";
3785
4399
  NETWORK_PROFILE_KEYS = [
3786
4400
  "InboundNetworkMinDelayMs",
3787
4401
  "OutboundNetworkMinDelayMs",
@@ -4186,8 +4800,8 @@ var init_tools = __esm({
4186
4800
  timedOut: true
4187
4801
  };
4188
4802
  }
4189
- async getFileTree(path4 = "", instance_id) {
4190
- const response = await this._callSingle("/api/file-tree", { path: path4 }, void 0, instance_id);
4803
+ async getFileTree(path5 = "", instance_id) {
4804
+ const response = await this._callSingle("/api/file-tree", { path: path5 }, void 0, instance_id);
4191
4805
  return {
4192
4806
  content: [
4193
4807
  {
@@ -4304,9 +4918,9 @@ var init_tools = __esm({
4304
4918
  ]
4305
4919
  };
4306
4920
  }
4307
- async getProjectStructure(path4, maxDepth, scriptsOnly, instance_id) {
4921
+ async getProjectStructure(path5, maxDepth, scriptsOnly, instance_id) {
4308
4922
  const response = await this._callSingle("/api/project-structure", {
4309
- path: path4,
4923
+ path: path5,
4310
4924
  maxDepth,
4311
4925
  scriptsOnly
4312
4926
  }, void 0, instance_id);
@@ -4486,17 +5100,20 @@ var init_tools = __esm({
4486
5100
  const topService = typeof response.topService === "string" && response.topService.length > 0 ? response.topService : pathSegments[0] === "game" ? pathSegments[1] ?? "game" : pathSegments[0];
4487
5101
  const typeNote = scriptTypeInfo[response.className] || response.className;
4488
5102
  const serviceNote = serviceInfo[topService] || topService;
5103
+ const showRange = Boolean(response.isPartial || response.truncated) && response.startLine !== void 0 && response.endLine !== void 0;
4489
5104
  const headerLines = [
4490
5105
  `Path: ${pathStr}`,
4491
5106
  `Type: ${typeNote}`,
4492
5107
  `Location: ${serviceNote}`,
4493
- `Lines: ${response.lineCount} total${response.isPartial ? ` (showing ${response.startLine}-${response.endLine})` : ""}`
5108
+ `Lines: ${response.lineCount} total${showRange ? ` (showing ${response.startLine}-${response.endLine})` : ""}`
4494
5109
  ];
4495
5110
  if (response.enabled === false) {
4496
5111
  headerLines.push(`Status: DISABLED`);
4497
5112
  }
4498
- if (response.truncated) {
4499
- headerLines.push(`Note: Truncated to first 1000 lines, use startLine/endLine to read more`);
5113
+ if (typeof response.note === "string" && response.note.length > 0) {
5114
+ headerLines.push(`Note: ${response.note}`);
5115
+ } else if (response.truncated) {
5116
+ headerLines.push(`Note: Truncated response; use line_range to read more`);
4500
5117
  }
4501
5118
  const header = headerLines.join("\n");
4502
5119
  const code = response.numberedSource || response.source;
@@ -4574,7 +5191,7 @@ ${code}`
4574
5191
  }
4575
5192
  const response = await this._callSingle("/api/grep-scripts", {
4576
5193
  pattern,
4577
- ...options
5194
+ ...options ?? {}
4578
5195
  }, void 0, instance_id);
4579
5196
  return {
4580
5197
  content: [
@@ -5227,9 +5844,9 @@ ${code}`
5227
5844
  const rawJson = mutable.raw_json;
5228
5845
  if (typeof rawJson === "string") {
5229
5846
  if (typeof outputPath === "string" && outputPath !== "") {
5230
- const resolvedOutputPath = path3.resolve(outputPath);
5231
- fs2.mkdirSync(path3.dirname(resolvedOutputPath), { recursive: true });
5232
- fs2.writeFileSync(resolvedOutputPath, rawJson, "utf8");
5847
+ const resolvedOutputPath = path4.resolve(outputPath);
5848
+ fs3.mkdirSync(path4.dirname(resolvedOutputPath), { recursive: true });
5849
+ fs3.writeFileSync(resolvedOutputPath, rawJson, "utf8");
5233
5850
  mutable.output_path = resolvedOutputPath;
5234
5851
  }
5235
5852
  delete mutable.raw_json;
@@ -5290,9 +5907,9 @@ ${code}`
5290
5907
  const rawSnapshotBase64 = mutable.raw_snapshot_base64;
5291
5908
  if (typeof rawSnapshotBase64 === "string") {
5292
5909
  if (typeof outputPath === "string" && outputPath !== "") {
5293
- const resolvedOutputPath = path3.resolve(outputPath);
5294
- fs2.mkdirSync(path3.dirname(resolvedOutputPath), { recursive: true });
5295
- fs2.writeFileSync(resolvedOutputPath, Buffer.from(rawSnapshotBase64, "base64"));
5910
+ const resolvedOutputPath = path4.resolve(outputPath);
5911
+ fs3.mkdirSync(path4.dirname(resolvedOutputPath), { recursive: true });
5912
+ fs3.writeFileSync(resolvedOutputPath, Buffer.from(rawSnapshotBase64, "base64"));
5296
5913
  mutable.output_path = resolvedOutputPath;
5297
5914
  }
5298
5915
  delete mutable.raw_snapshot_base64;
@@ -5306,9 +5923,9 @@ ${code}`
5306
5923
  });
5307
5924
  }
5308
5925
  if (typeof summaryOutputPath === "string" && summaryOutputPath !== "") {
5309
- const resolvedSummaryPath = path3.resolve(summaryOutputPath);
5310
- fs2.mkdirSync(path3.dirname(resolvedSummaryPath), { recursive: true });
5311
- fs2.writeFileSync(resolvedSummaryPath, JSON.stringify(mutable, null, 2), "utf8");
5926
+ const resolvedSummaryPath = path4.resolve(summaryOutputPath);
5927
+ fs3.mkdirSync(path4.dirname(resolvedSummaryPath), { recursive: true });
5928
+ fs3.writeFileSync(resolvedSummaryPath, JSON.stringify(mutable, null, 2), "utf8");
5312
5929
  mutable.summary_output_path = resolvedSummaryPath;
5313
5930
  }
5314
5931
  if (!includeComparisonIndex) {
@@ -5355,6 +5972,14 @@ ${code}`
5355
5972
  return void 0;
5356
5973
  return this._positiveInteger(value, name);
5357
5974
  }
5975
+ _optionalFiniteNumber(value, name) {
5976
+ if (value === void 0 || value === null)
5977
+ return void 0;
5978
+ if (typeof value !== "number" || !Number.isFinite(value)) {
5979
+ throw new Error(`${name} must be a finite number.`);
5980
+ }
5981
+ return value;
5982
+ }
5358
5983
  _publicInstanceKey(instance) {
5359
5984
  return `${instance.instanceId}:${instance.role}:${instance.connectedAt}`;
5360
5985
  }
@@ -5367,7 +5992,7 @@ ${code}`
5367
5992
  return record.placeId !== void 0 && instance.placeId === record.placeId;
5368
5993
  }
5369
5994
  if ((record.source === "baseplate" || record.source === "local_file") && record.localPlaceFile) {
5370
- const expectedName = path3.basename(record.localPlaceFile);
5995
+ const expectedName = path4.basename(record.localPlaceFile);
5371
5996
  return instance.placeName === expectedName || instance.dataModelName === expectedName;
5372
5997
  }
5373
5998
  return true;
@@ -5376,11 +6001,11 @@ ${code}`
5376
6001
  const response = await fetch(`https://apis.roblox.com/universes/v1/places/${placeId}/universe`);
5377
6002
  if (!response.ok) {
5378
6003
  const body = await response.text().catch(() => "");
5379
- throw new Error(`Could not derive universe_id for place ${placeId} (${response.status}): ${body}`);
6004
+ throw new Error(`Could not resolve the universe for place_id ${placeId} (${response.status}): ${body}`);
5380
6005
  }
5381
6006
  const data = await response.json();
5382
6007
  if (typeof data.universeId !== "number" || !Number.isFinite(data.universeId)) {
5383
- throw new Error(`Could not derive universe_id for place ${placeId}.`);
6008
+ throw new Error(`Could not resolve the universe for place_id ${placeId}.`);
5384
6009
  }
5385
6010
  return Math.trunc(data.universeId);
5386
6011
  }
@@ -5422,7 +6047,7 @@ ${code}`
5422
6047
  });
5423
6048
  }
5424
6049
  const placeId2 = this._positiveInteger(request.place_id, "place_id");
5425
- const rawMaxPageSize = this._optionalPositiveInteger(request.max_page_size, "max_page_size") ?? 10;
6050
+ const rawMaxPageSize = Math.trunc(this._optionalFiniteNumber(request.max_page_size, "max_page_size") ?? 10);
5426
6051
  const maxPageSize = Math.max(1, Math.min(50, rawMaxPageSize));
5427
6052
  const pageToken = typeof request.page_token === "string" ? request.page_token : void 0;
5428
6053
  const response = await this.openCloudClient.listAssetVersions(placeId2, maxPageSize, pageToken);
@@ -5467,31 +6092,38 @@ ${code}`
5467
6092
  if (action === "close") {
5468
6093
  let record2;
5469
6094
  if (instance_id) {
5470
- record2 = this.instanceManager.get(instance_id);
5471
- if (!record2) {
5472
- const connected2 = this.bridge.getPublicInstances().filter((instance) => instance.instanceId === instance_id);
5473
- const edit = connected2.find((instance) => instance.role === "edit");
5474
- if (!edit) {
5475
- return this._textResult({
5476
- error: "Instance is not connected or managed.",
5477
- instance_id
5478
- });
5479
- }
5480
- try {
5481
- this.instanceManager.closeConnectedInstance(edit);
5482
- await sleep(500);
5483
- } catch (error) {
5484
- return this._textResult({
5485
- error: error instanceof Error ? error.message : String(error),
5486
- instance_id
5487
- });
5488
- }
6095
+ const managedClose = this.instanceManager.closeByInstanceId(instance_id);
6096
+ if (managedClose.status !== "not_found") {
6097
+ this.bridge.unregisterInstanceId(instance_id);
6098
+ await sleep(500);
5489
6099
  this.bridge.unregisterInstanceId(instance_id);
5490
6100
  return this._textResult({
5491
6101
  instance_id,
5492
- message: "Studio instance closed."
6102
+ message: managedClose.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
6103
+ });
6104
+ }
6105
+ const connected2 = this.bridge.getPublicInstances().filter((instance) => instance.instanceId === instance_id);
6106
+ const edit = connected2.find((instance) => instance.role === "edit");
6107
+ if (!edit) {
6108
+ return this._textResult({
6109
+ error: "Instance is not connected or managed.",
6110
+ instance_id
6111
+ });
6112
+ }
6113
+ try {
6114
+ this.instanceManager.closeConnectedInstance(edit);
6115
+ await sleep(500);
6116
+ } catch (error) {
6117
+ return this._textResult({
6118
+ error: error instanceof Error ? error.message : String(error),
6119
+ instance_id
5493
6120
  });
5494
6121
  }
6122
+ this.bridge.unregisterInstanceId(instance_id);
6123
+ return this._textResult({
6124
+ instance_id,
6125
+ message: "Studio instance closed."
6126
+ });
5495
6127
  } else {
5496
6128
  const active = this.instanceManager.list().filter((entry) => entry.closedAt === void 0);
5497
6129
  if (active.length === 0) {
@@ -5507,14 +6139,14 @@ ${code}`
5507
6139
  }
5508
6140
  if (record2.instanceId)
5509
6141
  this.bridge.unregisterInstanceId(record2.instanceId);
5510
- this.instanceManager.close(record2);
6142
+ const closeResult = this.instanceManager.close(record2);
5511
6143
  if (record2.instanceId) {
5512
6144
  await sleep(500);
5513
6145
  this.bridge.unregisterInstanceId(record2.instanceId);
5514
6146
  }
5515
6147
  return this._textResult({
5516
6148
  instance_id: record2.instanceId,
5517
- message: "Studio instance closed."
6149
+ message: closeResult.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
5518
6150
  });
5519
6151
  }
5520
6152
  const source = request.source;
@@ -5522,8 +6154,8 @@ ${code}`
5522
6154
  throw new Error("manage_instance action=launch requires source=baseplate|local_file|published_place|place_revision");
5523
6155
  }
5524
6156
  const launchSource = source;
5525
- const placeId = launchSource === "published_place" || launchSource === "place_revision" ? this._positiveInteger(request.place_id, "place_id") : this._optionalPositiveInteger(request.place_id, "place_id");
5526
- const placeVersion = launchSource === "place_revision" ? this._positiveInteger(request.place_version, "place_version") : this._optionalPositiveInteger(request.place_version, "place_version");
6157
+ const placeId = launchSource === "published_place" || launchSource === "place_revision" ? this._positiveInteger(request.place_id, "place_id") : void 0;
6158
+ const placeVersion = launchSource === "place_revision" ? this._positiveInteger(request.place_version, "place_version") : void 0;
5527
6159
  const localPlaceFile = typeof request.local_place_file === "string" ? request.local_place_file : void 0;
5528
6160
  if (launchSource === "published_place" && placeId !== void 0 && this._isLatestPublishedPlaceOpen(placeId)) {
5529
6161
  return this._textResult({
@@ -5531,7 +6163,7 @@ ${code}`
5531
6163
  message: `place_id ${placeId} is already connected. Use the existing instance or launch a specific place_revision.`
5532
6164
  });
5533
6165
  }
5534
- const universeId = launchSource === "published_place" || launchSource === "place_revision" ? this._optionalPositiveInteger(request.universe_id, "universe_id") ?? await this._deriveUniverseId(placeId) : this._optionalPositiveInteger(request.universe_id, "universe_id");
6166
+ const universeId = launchSource === "published_place" || launchSource === "place_revision" ? await this._deriveUniverseId(placeId) : void 0;
5535
6167
  const waitForConnection = request.wait_for_connection !== false;
5536
6168
  const timeoutMs = this._optionalPositiveInteger(request.timeout_ms, "timeout_ms") ?? 12e4;
5537
6169
  const beforeKeys = new Set(this.bridge.getPublicInstances().map((instance) => this._publicInstanceKey(instance)));
@@ -5797,26 +6429,40 @@ ${code}`
5797
6429
  }
5798
6430
  return hasServer && clientCount >= 2;
5799
6431
  }
5800
- async _waitForMultiplayerStart(instanceId, clientCount, timeoutSec = 30) {
6432
+ async _waitForMultiplayerStart(instanceId, clientCount, timeoutSec = 30, connectedAfter) {
5801
6433
  const deadline = Date.now() + timeoutSec * 1e3;
6434
+ let lastPhase;
5802
6435
  while (Date.now() < deadline) {
5803
6436
  const exact = await this._waitForExactClientCount(instanceId, clientCount, 0.25, 0);
5804
6437
  if (exact.ok || exact.extraClients) {
6438
+ if (exact.ok && connectedAfter !== void 0) {
6439
+ const instances = this.bridge.getInstances().filter((inst) => inst.instanceId === instanceId);
6440
+ const freshRoles = new Set(instances.filter((inst) => inst.connectedAt >= connectedAfter).map((inst) => inst.role));
6441
+ const freshClientCount = [...freshRoles].filter((role) => /^client-\d+$/.test(role)).length;
6442
+ if (!freshRoles.has("server") || freshClientCount !== clientCount) {
6443
+ await sleep(250);
6444
+ continue;
6445
+ }
6446
+ }
5805
6447
  return { ok: exact.ok, roles: exact.roles, timedOut: false, error: exact.extraClients ? `Expected ${clientCount} client(s), but Studio registered ${exact.clientCount}.` : void 0 };
5806
6448
  }
5807
6449
  try {
5808
- const editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit");
6450
+ const remainingMs = Math.max(1, Math.min(1e3, deadline - Date.now()));
6451
+ const editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit", remainingMs);
5809
6452
  const session = editState?.session;
5810
- if (session?.phase === "failed" || session?.phase === "completed") {
6453
+ if (typeof session?.phase === "string") {
6454
+ lastPhase = session.phase;
6455
+ }
6456
+ if (session?.phase === "failed") {
5811
6457
  return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: false, phase: session.phase, error: session.error };
5812
6458
  }
5813
6459
  } catch {
5814
6460
  }
5815
6461
  await sleep(250);
5816
6462
  }
5817
- return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: true };
6463
+ return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: true, phase: lastPhase };
5818
6464
  }
5819
- async multiplayerPlaytest(action, numPlayers, target, testArgs, value, timeout, instance_id) {
6465
+ async multiplayerPlaytest(action, numPlayers, target, testArgs, value, timeout, instance_id, force) {
5820
6466
  if (action !== "start" && action !== "status" && action !== "add_players" && action !== "leave_client" && action !== "end") {
5821
6467
  throw new Error("multiplayer_playtest requires action=start|status|add_players|leave_client|end");
5822
6468
  }
@@ -5838,93 +6484,130 @@ ${code}`
5838
6484
  });
5839
6485
  }
5840
6486
  if (action === "start") {
5841
- const body2 = this._parseTextResult(await this.multiplayerTestStart(numPlayers, testArgs, timeout, instance_id));
5842
- const state = body2.state && typeof body2.state === "object" ? body2.state : {};
5843
- const success = body2.success === true && body2.ready === true;
5844
- return this._textResult(success ? {
6487
+ if (force !== true) {
6488
+ return this._textResult({
6489
+ success: false,
6490
+ action,
6491
+ error: "multiplayer_force_required",
6492
+ message: MULTIPLAYER_FORCE_REQUIRED_MESSAGE,
6493
+ requiresForce: true,
6494
+ manualCleanupRequired: true
6495
+ });
6496
+ }
6497
+ const body = this._parseTextResult(await this.multiplayerTestStart(numPlayers, testArgs, timeout, instance_id, force));
6498
+ const state = body.state && typeof body.state === "object" ? body.state : {};
6499
+ const launched = body.success === true && body.ready === true;
6500
+ return this._textResult(launched ? {
5845
6501
  success: true,
5846
6502
  action,
5847
- message: "Multiplayer playtest started.",
5848
- roles: Array.isArray(body2.roles) ? body2.roles : void 0,
6503
+ message: "Multiplayer playtest started. Stop/end is disabled; close the multiplayer test windows manually when finished.",
6504
+ ready: true,
6505
+ manualCleanupRequired: true,
6506
+ roles: Array.isArray(body.roles) ? body.roles : void 0,
5849
6507
  clientRoles: Array.isArray(state.clientRoles) ? state.clientRoles : void 0,
5850
6508
  playerCount: typeof state.playerCount === "number" ? state.playerCount : void 0
5851
6509
  } : {
5852
6510
  success: false,
5853
6511
  action,
5854
- error: body2.error ?? body2.wait?.error ?? "start_failed",
5855
- message: body2.success === true ? "Multiplayer playtest did not become ready before timeout." : body2.message ?? "Multiplayer playtest did not start.",
5856
- roles: Array.isArray(body2.roles) ? body2.roles : void 0
6512
+ error: body.error ?? body.wait?.error ?? "multiplayer_start_not_detected",
6513
+ message: body.success === true ? "Multiplayer playtest start was requested, but MCP did not detect the required server/client peers before timeout. You may need to close the test windows manually." : body.message ?? "Multiplayer playtest did not start.",
6514
+ manualCleanupRequired: body.startRequested === true || body.launched === true ? true : void 0,
6515
+ roles: Array.isArray(body.roles) ? body.roles : void 0
5857
6516
  });
5858
6517
  }
5859
6518
  if (action === "add_players") {
5860
- const body2 = this._parseTextResult(await this.multiplayerTestAddPlayers(numPlayers, timeout, instance_id));
5861
- const state = body2.state && typeof body2.state === "object" ? body2.state : {};
5862
- const success = body2.success === true && body2.ready === true;
6519
+ const body = this._parseTextResult(await this.multiplayerTestAddPlayers(numPlayers, timeout, instance_id));
6520
+ const state = body.state && typeof body.state === "object" ? body.state : {};
6521
+ const success = body.success === true && body.ready === true;
5863
6522
  return this._textResult(success ? {
5864
6523
  success: true,
5865
6524
  action,
5866
6525
  message: "Players added.",
5867
- roles: Array.isArray(body2.roles) ? body2.roles : void 0,
6526
+ roles: Array.isArray(body.roles) ? body.roles : void 0,
5868
6527
  clientRoles: Array.isArray(state.clientRoles) ? state.clientRoles : void 0,
5869
6528
  playerCount: typeof state.playerCount === "number" ? state.playerCount : void 0
5870
6529
  } : {
5871
6530
  success: false,
5872
6531
  action,
5873
- error: body2.error ?? "add_players_failed",
5874
- message: body2.success === true ? "Players did not finish joining before timeout." : body2.message ?? "Players were not added.",
5875
- roles: Array.isArray(body2.roles) ? body2.roles : void 0
6532
+ error: body.error ?? "add_players_failed",
6533
+ message: body.success === true ? "Players did not finish joining before timeout." : body.message ?? "Players were not added.",
6534
+ roles: Array.isArray(body.roles) ? body.roles : void 0
5876
6535
  });
5877
6536
  }
5878
6537
  if (action === "leave_client") {
5879
- const body2 = this._parseTextResult(await this.multiplayerTestLeaveClient(target ?? "client-1", timeout, instance_id));
5880
- return this._textResult(body2.success === true && body2.left === true ? {
6538
+ const body = this._parseTextResult(await this.multiplayerTestLeaveClient(target ?? "client-1", timeout, instance_id));
6539
+ return this._textResult(body.success === true && body.left === true ? {
5881
6540
  success: true,
5882
6541
  action,
5883
6542
  message: "Client left.",
5884
- roles: Array.isArray(body2.roles) ? body2.roles : void 0
6543
+ roles: Array.isArray(body.roles) ? body.roles : void 0
5885
6544
  } : {
5886
6545
  success: false,
5887
6546
  action,
5888
- error: body2.error ?? "leave_client_failed",
5889
- message: body2.message ?? "Client did not leave.",
5890
- roles: Array.isArray(body2.roles) ? body2.roles : void 0
6547
+ error: body.error ?? "leave_client_failed",
6548
+ message: body.message ?? "Client did not leave.",
6549
+ roles: Array.isArray(body.roles) ? body.roles : void 0
5891
6550
  });
5892
6551
  }
5893
- const body = this._parseTextResult(await this.multiplayerTestEnd(value, timeout, instance_id));
5894
- return this._textResult(body.success === true && body.ended === true ? {
5895
- success: true,
5896
- action,
5897
- message: "Multiplayer playtest ended."
5898
- } : {
5899
- success: false,
6552
+ return this._textResult({
5900
6553
  action,
5901
- error: body.error ?? "end_failed",
5902
- message: body.message ?? "Multiplayer playtest did not end.",
5903
- roles: Array.isArray(body.roles) ? body.roles : void 0,
5904
- editDone: body.editDone === false ? false : void 0
6554
+ ...multiplayerStopDisabledBody()
5905
6555
  });
5906
6556
  }
5907
- async multiplayerTestStart(numPlayers, testArgs, timeout, instance_id) {
6557
+ async multiplayerTestStart(numPlayers, testArgs, timeout, instance_id, force) {
5908
6558
  if (!Number.isInteger(numPlayers) || numPlayers < 1 || numPlayers > 8) {
5909
6559
  throw new Error("numPlayers must be an integer from 1 to 8");
5910
6560
  }
5911
6561
  const editTarget = this._resolveSingleTarget("edit", instance_id);
6562
+ if (force !== true) {
6563
+ return this._textResult({
6564
+ success: false,
6565
+ error: "multiplayer_force_required",
6566
+ message: MULTIPLAYER_FORCE_REQUIRED_MESSAGE,
6567
+ requiresForce: true,
6568
+ manualCleanupRequired: true
6569
+ });
6570
+ }
6571
+ const existingRuntime = this._runtimeTargetsForEquivalentInstances(editTarget.instanceId);
6572
+ if (existingRuntime.length > 0) {
6573
+ const roles = this._rolesForEquivalentInstances(editTarget.instanceId);
6574
+ return this._textResult({
6575
+ success: false,
6576
+ error: "Multiplayer playtest already running.",
6577
+ message: "A Studio runtime is already connected for this place. Close the existing playtest windows manually before starting another multiplayer playtest.",
6578
+ ready: true,
6579
+ timedOut: false,
6580
+ roles,
6581
+ runtimeRoles: existingRuntime.map((target) => target.role),
6582
+ manualCleanupRequired: true
6583
+ });
6584
+ }
6585
+ const startedAt = Date.now();
5912
6586
  const response = await this.client.request("/api/multiplayer-test-start", { numPlayers, testArgs: testArgs ?? {} }, editTarget.instanceId, editTarget.role);
5913
6587
  if (response?.error) {
5914
6588
  return { content: [{ type: "text", text: JSON.stringify(response) }] };
5915
6589
  }
5916
- const wait = await this._waitForMultiplayerStart(editTarget.instanceId, numPlayers, timeout ?? 30);
6590
+ const wait = await this._waitForMultiplayerStart(editTarget.instanceId, numPlayers, timeout ?? 60, startedAt);
6591
+ const launched = wait.ok;
5917
6592
  const state = await this._buildMultiplayerState(editTarget.instanceId);
6593
+ const success = response.success === true && wait.ok;
5918
6594
  return {
5919
6595
  content: [{
5920
6596
  type: "text",
5921
6597
  text: JSON.stringify({
5922
6598
  ...response,
6599
+ success,
5923
6600
  ready: wait.ok,
6601
+ launched,
6602
+ startRequested: response.success === true,
5924
6603
  timedOut: wait.timedOut,
5925
6604
  wait,
5926
6605
  roles: wait.roles,
5927
- state
6606
+ state,
6607
+ error: success ? void 0 : wait.error ?? "multiplayer_start_not_detected",
6608
+ message: success ? "Multiplayer Studio test started and runtime peers detected." : "Multiplayer Studio test start was requested, but MCP did not detect the required server/client peers before timeout. Close the multiplayer test windows manually if Studio launched them.",
6609
+ manualCleanupRequired: success || response.success === true ? true : void 0,
6610
+ startedAt
5928
6611
  })
5929
6612
  }]
5930
6613
  };
@@ -5985,27 +6668,10 @@ ${code}`
5985
6668
  };
5986
6669
  }
5987
6670
  async multiplayerTestEnd(value, timeout, instance_id) {
5988
- const serverTarget = this._resolveSingleTarget("server", instance_id);
5989
- const response = await this.client.request("/api/multiplayer-test-end", { value: value ?? "ended_by_mcp" }, serverTarget.instanceId, serverTarget.role);
5990
- if (response?.error) {
5991
- return { content: [{ type: "text", text: JSON.stringify(response) }] };
5992
- }
5993
- const editDone = await this._waitForMultiplayerEditDone(serverTarget.instanceId, timeout ?? 30);
5994
- const wait = await this._waitForRuntimeRoles(serverTarget.instanceId, { noRuntime: true }, timeout ?? 30);
5995
- const state = await this._buildMultiplayerState(serverTarget.instanceId);
5996
- return {
5997
- content: [{
5998
- type: "text",
5999
- text: JSON.stringify({
6000
- ...response,
6001
- ended: wait.ok,
6002
- editDone,
6003
- timedOut: wait.timedOut,
6004
- roles: wait.roles,
6005
- state
6006
- })
6007
- }]
6008
- };
6671
+ void value;
6672
+ void timeout;
6673
+ void instance_id;
6674
+ return this._textResult(multiplayerStopDisabledBody());
6009
6675
  }
6010
6676
  async getConnectedInstances() {
6011
6677
  const instances = this.bridge.getPublicInstances();
@@ -6041,14 +6707,14 @@ ${code}`
6041
6707
  };
6042
6708
  }
6043
6709
  static findProjectRoot(startDir) {
6044
- let dir = path3.resolve(startDir);
6710
+ let dir = path4.resolve(startDir);
6045
6711
  let previous = "";
6046
6712
  while (dir !== previous) {
6047
- if (fs2.existsSync(path3.join(dir, ".git")) || fs2.existsSync(path3.join(dir, "package.json"))) {
6713
+ if (fs3.existsSync(path4.join(dir, ".git")) || fs3.existsSync(path4.join(dir, "package.json"))) {
6048
6714
  return dir;
6049
6715
  }
6050
6716
  previous = dir;
6051
- dir = path3.dirname(dir);
6717
+ dir = path4.dirname(dir);
6052
6718
  }
6053
6719
  return null;
6054
6720
  }
@@ -6056,15 +6722,15 @@ ${code}`
6056
6722
  if (!candidate)
6057
6723
  return false;
6058
6724
  try {
6059
- return fs2.statSync(candidate).isDirectory();
6725
+ return fs3.statSync(candidate).isDirectory();
6060
6726
  } catch {
6061
6727
  return false;
6062
6728
  }
6063
6729
  }
6064
6730
  static ensureWritableDirectory(candidate, label) {
6065
- const resolved = path3.resolve(candidate);
6731
+ const resolved = path4.resolve(candidate);
6066
6732
  try {
6067
- fs2.mkdirSync(resolved, { recursive: true });
6733
+ fs3.mkdirSync(resolved, { recursive: true });
6068
6734
  } catch (error) {
6069
6735
  throw new Error(`Unable to create ${label} build-library directory at ${resolved}: ${error.message}`);
6070
6736
  }
@@ -6072,7 +6738,7 @@ ${code}`
6072
6738
  throw new Error(`${label} build-library path is not a directory: ${resolved}`);
6073
6739
  }
6074
6740
  try {
6075
- fs2.accessSync(resolved, fs2.constants.W_OK);
6741
+ fs3.accessSync(resolved, fs3.constants.W_OK);
6076
6742
  } catch (error) {
6077
6743
  throw new Error(`${label} build-library directory is not writable: ${resolved}. ${error.message}`);
6078
6744
  }
@@ -6083,25 +6749,25 @@ ${code}`
6083
6749
  if (_RobloxStudioTools._cachedLibraryPath)
6084
6750
  return _RobloxStudioTools._cachedLibraryPath;
6085
6751
  const overridePath = process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
6086
- const cwd = path3.resolve(process.cwd());
6752
+ const cwd = path4.resolve(process.cwd());
6087
6753
  const projectRoot = _RobloxStudioTools.findProjectRoot(cwd);
6088
- const homeLibraryPath = path3.join(os2.homedir(), ".robloxstudio-mcp", "build-library");
6089
- const projectLibraryPath = projectRoot ? path3.join(projectRoot, "build-library") : null;
6090
- const cwdLibraryPath = path3.join(cwd, "build-library");
6754
+ const homeLibraryPath = path4.join(os3.homedir(), ".robloxstudio-mcp", "build-library");
6755
+ const projectLibraryPath = projectRoot ? path4.join(projectRoot, "build-library") : null;
6756
+ const cwdLibraryPath = path4.join(cwd, "build-library");
6091
6757
  let result;
6092
6758
  if (overridePath) {
6093
6759
  result = _RobloxStudioTools.ensureWritableDirectory(overridePath, "override");
6094
6760
  } else {
6095
6761
  const existing = [projectLibraryPath, cwdLibraryPath].find((c) => c && _RobloxStudioTools.isDirectory(c) && (() => {
6096
6762
  try {
6097
- fs2.accessSync(c, fs2.constants.W_OK);
6763
+ fs3.accessSync(c, fs3.constants.W_OK);
6098
6764
  return true;
6099
6765
  } catch {
6100
6766
  return false;
6101
6767
  }
6102
6768
  })());
6103
6769
  if (existing) {
6104
- result = path3.resolve(existing);
6770
+ result = path4.resolve(existing);
6105
6771
  } else if (projectLibraryPath) {
6106
6772
  try {
6107
6773
  result = _RobloxStudioTools.ensureWritableDirectory(projectLibraryPath, "project-root");
@@ -6128,12 +6794,12 @@ ${code}`
6128
6794
  if (response && response.success && response.buildData) {
6129
6795
  const buildData = response.buildData;
6130
6796
  const buildId = buildData.id || `${style}/exported`;
6131
- const filePath = path3.join(_RobloxStudioTools.findLibraryPath(), `${buildId}.json`);
6132
- const dirPath = path3.dirname(filePath);
6133
- if (!fs2.existsSync(dirPath)) {
6134
- fs2.mkdirSync(dirPath, { recursive: true });
6797
+ const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${buildId}.json`);
6798
+ const dirPath = path4.dirname(filePath);
6799
+ if (!fs3.existsSync(dirPath)) {
6800
+ fs3.mkdirSync(dirPath, { recursive: true });
6135
6801
  }
6136
- fs2.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
6802
+ fs3.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
6137
6803
  response.savedTo = filePath;
6138
6804
  }
6139
6805
  return {
@@ -6230,12 +6896,12 @@ ${code}`
6230
6896
  const normalizedParts = this.normalizeBuildParts(parts, new Set(Object.keys(normalizedPalette)));
6231
6897
  const computedBounds = bounds || this.computeBounds(normalizedParts);
6232
6898
  const buildData = { id, style, bounds: computedBounds, palette: normalizedPalette, parts: normalizedParts };
6233
- const filePath = path3.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
6234
- const dirPath = path3.dirname(filePath);
6235
- if (!fs2.existsSync(dirPath)) {
6236
- fs2.mkdirSync(dirPath, { recursive: true });
6899
+ const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
6900
+ const dirPath = path4.dirname(filePath);
6901
+ if (!fs3.existsSync(dirPath)) {
6902
+ fs3.mkdirSync(dirPath, { recursive: true });
6237
6903
  }
6238
- fs2.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
6904
+ fs3.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
6239
6905
  return {
6240
6906
  content: [
6241
6907
  {
@@ -6289,12 +6955,12 @@ ${code}`
6289
6955
  };
6290
6956
  if (seed !== void 0)
6291
6957
  buildData.generatorSeed = seed;
6292
- const filePath = path3.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
6293
- const dirPath = path3.dirname(filePath);
6294
- if (!fs2.existsSync(dirPath)) {
6295
- fs2.mkdirSync(dirPath, { recursive: true });
6958
+ const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
6959
+ const dirPath = path4.dirname(filePath);
6960
+ if (!fs3.existsSync(dirPath)) {
6961
+ fs3.mkdirSync(dirPath, { recursive: true });
6296
6962
  }
6297
- fs2.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
6963
+ fs3.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
6298
6964
  return {
6299
6965
  content: [
6300
6966
  {
@@ -6318,17 +6984,17 @@ ${code}`
6318
6984
  }
6319
6985
  let resolved;
6320
6986
  if (typeof buildData === "string") {
6321
- const filePath = path3.join(_RobloxStudioTools.findLibraryPath(), `${buildData}.json`);
6322
- if (!fs2.existsSync(filePath)) {
6987
+ const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${buildData}.json`);
6988
+ if (!fs3.existsSync(filePath)) {
6323
6989
  throw new Error(`Build not found in library: ${buildData}`);
6324
6990
  }
6325
- resolved = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
6991
+ resolved = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
6326
6992
  } else if (buildData.id && !buildData.parts) {
6327
- const filePath = path3.join(_RobloxStudioTools.findLibraryPath(), `${buildData.id}.json`);
6328
- if (!fs2.existsSync(filePath)) {
6993
+ const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${buildData.id}.json`);
6994
+ if (!fs3.existsSync(filePath)) {
6329
6995
  throw new Error(`Build not found in library: ${buildData.id}`);
6330
6996
  }
6331
- resolved = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
6997
+ resolved = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
6332
6998
  } else {
6333
6999
  resolved = buildData;
6334
7000
  }
@@ -6351,13 +7017,13 @@ ${code}`
6351
7017
  const styles = style ? [style] : ["medieval", "modern", "nature", "scifi", "misc"];
6352
7018
  const builds = [];
6353
7019
  for (const s of styles) {
6354
- const dirPath = path3.join(libraryPath, s);
6355
- if (!fs2.existsSync(dirPath))
7020
+ const dirPath = path4.join(libraryPath, s);
7021
+ if (!fs3.existsSync(dirPath))
6356
7022
  continue;
6357
- const files = fs2.readdirSync(dirPath).filter((f) => f.endsWith(".json"));
7023
+ const files = fs3.readdirSync(dirPath).filter((f) => f.endsWith(".json"));
6358
7024
  for (const file of files) {
6359
7025
  try {
6360
- const content = fs2.readFileSync(path3.join(dirPath, file), "utf-8");
7026
+ const content = fs3.readFileSync(path4.join(dirPath, file), "utf-8");
6361
7027
  const data = JSON.parse(content);
6362
7028
  builds.push({
6363
7029
  id: data.id || `${s}/${file.replace(".json", "")}`,
@@ -6396,11 +7062,11 @@ ${code}`
6396
7062
  if (!id) {
6397
7063
  throw new Error("Build ID is required for get_build");
6398
7064
  }
6399
- const filePath = path3.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
6400
- if (!fs2.existsSync(filePath)) {
7065
+ const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
7066
+ if (!fs3.existsSync(filePath)) {
6401
7067
  throw new Error(`Build not found in library: ${id}`);
6402
7068
  }
6403
- const data = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
7069
+ const data = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
6404
7070
  const result = {
6405
7071
  id: data.id,
6406
7072
  style: data.style,
@@ -6483,11 +7149,11 @@ ${code}`
6483
7149
  if (!buildId) {
6484
7150
  throw new Error(`Invalid ${validatedKeyPath}: model key "${modelKey}" is not defined in sceneData.models`);
6485
7151
  }
6486
- const filePath = path3.join(libraryPath, `${buildId}.json`);
6487
- if (!fs2.existsSync(filePath)) {
7152
+ const filePath = path4.join(libraryPath, `${buildId}.json`);
7153
+ if (!fs3.existsSync(filePath)) {
6488
7154
  throw new Error(`Build not found in library: ${buildId}`);
6489
7155
  }
6490
- const content = fs2.readFileSync(filePath, "utf-8");
7156
+ const content = fs3.readFileSync(filePath, "utf-8");
6491
7157
  const buildData = JSON.parse(content);
6492
7158
  const buildName = buildId.split("/").pop() || buildId;
6493
7159
  expandedBuilds.push({
@@ -6827,11 +7493,11 @@ ${code}`
6827
7493
  return this.resolveUploadedReferenceImageId(decalId, instance_id);
6828
7494
  }
6829
7495
  async uploadAsset(filePath, assetType, displayName, description, userId, groupId) {
6830
- if (!fs2.existsSync(filePath)) {
7496
+ if (!fs3.existsSync(filePath)) {
6831
7497
  throw new Error(`File not found: ${filePath}`);
6832
7498
  }
6833
- const fileContent = fs2.readFileSync(filePath);
6834
- const fileName = path3.basename(filePath);
7499
+ const fileContent = fs3.readFileSync(filePath);
7500
+ const fileName = path4.basename(filePath);
6835
7501
  if (assetType === "Decal" && this.cookieClient.hasCookie()) {
6836
7502
  const result2 = await this.cookieClient.uploadDecal(fileContent, displayName, description || "");
6837
7503
  return {
@@ -7056,10 +7722,10 @@ ${code}`
7056
7722
  return { content: [{ type: "text", text: JSON.stringify({ error: "plugin returned no base64 payload" }) }] };
7057
7723
  }
7058
7724
  const bytes = Buffer.from(response.base64, "base64");
7059
- const resolved = path3.resolve(outputPath);
7725
+ const resolved = path4.resolve(outputPath);
7060
7726
  try {
7061
- fs2.mkdirSync(path3.dirname(resolved), { recursive: true });
7062
- fs2.writeFileSync(resolved, bytes);
7727
+ fs3.mkdirSync(path4.dirname(resolved), { recursive: true });
7728
+ fs3.writeFileSync(resolved, bytes);
7063
7729
  } catch (err) {
7064
7730
  return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err.message}` }) }] };
7065
7731
  }
@@ -7092,9 +7758,9 @@ ${code}`
7092
7758
  let bytes;
7093
7759
  let sourceLabel;
7094
7760
  if (source.path !== void 0) {
7095
- const resolved = path3.resolve(source.path);
7761
+ const resolved = path4.resolve(source.path);
7096
7762
  try {
7097
- bytes = fs2.readFileSync(resolved);
7763
+ bytes = fs3.readFileSync(resolved);
7098
7764
  } catch (err) {
7099
7765
  return { content: [{ type: "text", text: JSON.stringify({ error: `failed to read ${resolved}: ${err.message}` }) }] };
7100
7766
  }
@@ -7317,7 +7983,7 @@ var init_proxy_bridge_service = __esm({
7317
7983
  // ../core/dist/server.js
7318
7984
  import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
7319
7985
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7320
- import { CallToolRequestSchema as CallToolRequestSchema2, ErrorCode as ErrorCode2, ListToolsRequestSchema as ListToolsRequestSchema2, McpError as McpError2 } from "@modelcontextprotocol/sdk/types.js";
7986
+ import { CallToolRequestSchema as CallToolRequestSchema2, ErrorCode as ErrorCode3, ListToolsRequestSchema as ListToolsRequestSchema2, McpError as McpError3 } from "@modelcontextprotocol/sdk/types.js";
7321
7987
  var RobloxStudioMCPServer;
7322
7988
  var init_server = __esm({
7323
7989
  "../core/dist/server.js"() {
@@ -7326,6 +7992,7 @@ var init_server = __esm({
7326
7992
  init_tools();
7327
7993
  init_bridge_service();
7328
7994
  init_proxy_bridge_service();
7995
+ init_mcp_compat();
7329
7996
  RobloxStudioMCPServer = class {
7330
7997
  server;
7331
7998
  tools;
@@ -7345,6 +8012,7 @@ var init_server = __esm({
7345
8012
  });
7346
8013
  this.bridge = new BridgeService();
7347
8014
  this.tools = new RobloxStudioTools(this.bridge);
8015
+ registerEmptyResourceShim(this.server);
7348
8016
  this.setupToolHandlers();
7349
8017
  }
7350
8018
  setupToolHandlers() {
@@ -7360,11 +8028,11 @@ var init_server = __esm({
7360
8028
  this.server.setRequestHandler(CallToolRequestSchema2, async (request) => {
7361
8029
  const { name, arguments: args } = request.params;
7362
8030
  if (!this.allowedToolNames.has(name)) {
7363
- throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
8031
+ throw new McpError3(ErrorCode3.MethodNotFound, `Unknown tool: ${name}`);
7364
8032
  }
7365
8033
  const handler = TOOL_HANDLERS[name];
7366
8034
  if (!handler) {
7367
- throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
8035
+ throw new McpError3(ErrorCode3.MethodNotFound, `Unknown tool: ${name}`);
7368
8036
  }
7369
8037
  try {
7370
8038
  return await handler(this.tools, args ?? {});
@@ -7382,9 +8050,9 @@ var init_server = __esm({
7382
8050
  isError: true
7383
8051
  };
7384
8052
  }
7385
- if (error instanceof McpError2)
8053
+ if (error instanceof McpError3)
7386
8054
  throw error;
7387
- throw new McpError2(ErrorCode2.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
8055
+ throw new McpError3(ErrorCode3.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
7388
8056
  }
7389
8057
  });
7390
8058
  }
@@ -8049,7 +8717,7 @@ var init_definitions = __esm({
8049
8717
  {
8050
8718
  name: "get_script_source",
8051
8719
  category: "read",
8052
- description: 'Get script source. Returns "source" and "numberedSource" (line-numbered). Use startLine/endLine for large scripts.',
8720
+ description: 'Get script source. Returns "source" and "numberedSource" (line-numbered). Pass line_range for large scripts; without a range, large scripts are truncated (see the "truncated" flag and "note") to avoid flooding the context.',
8053
8721
  inputSchema: {
8054
8722
  type: "object",
8055
8723
  properties: {
@@ -8057,13 +8725,9 @@ var init_definitions = __esm({
8057
8725
  type: "string",
8058
8726
  description: "Canonical path to a LuaSourceContainer"
8059
8727
  },
8060
- startLine: {
8061
- type: "number",
8062
- description: "Start line (1-indexed)"
8063
- },
8064
- endLine: {
8065
- type: "number",
8066
- description: "End line (inclusive)"
8728
+ line_range: {
8729
+ type: "string",
8730
+ description: 'Line range to return: "start-end" (e.g. "100-200"), open-ended ("100-" or "-200"), or a single line ("42").'
8067
8731
  },
8068
8732
  instance_id: {
8069
8733
  type: "string",
@@ -8099,7 +8763,7 @@ var init_definitions = __esm({
8099
8763
  {
8100
8764
  name: "edit_script_lines",
8101
8765
  category: "write",
8102
- description: "Replace exact text in a script. Without startLine, old_string must match exactly once in the script. Pass startLine (1-indexed, from get_script_source) to anchor the edit to a specific line when old_string is ambiguous (e.g. repeated closing braces).",
8766
+ description: 'Replace exact text in a script. Without line_range, old_string must match exactly once in the script. Pass line_range as a single line (e.g. "42") to anchor the edit when old_string is ambiguous.',
8103
8767
  inputSchema: {
8104
8768
  type: "object",
8105
8769
  properties: {
@@ -8109,15 +8773,15 @@ var init_definitions = __esm({
8109
8773
  },
8110
8774
  old_string: {
8111
8775
  type: "string",
8112
- description: "Exact text to find and replace. Must be unique in the script unless startLine is provided."
8776
+ description: "Exact text to find and replace. Must be unique in the script unless line_range is provided."
8113
8777
  },
8114
8778
  new_string: {
8115
8779
  type: "string",
8116
8780
  description: "Replacement text"
8117
8781
  },
8118
- startLine: {
8119
- type: "number",
8120
- description: "Optional 1-indexed line where old_string begins. When provided, skips uniqueness check and requires old_string to match starting at that exact line."
8782
+ line_range: {
8783
+ type: "string",
8784
+ description: 'Optional single line where old_string begins, such as "42". When provided, skips uniqueness check and requires old_string to match starting at that exact line.'
8121
8785
  },
8122
8786
  instance_id: {
8123
8787
  type: "string",
@@ -8157,7 +8821,7 @@ var init_definitions = __esm({
8157
8821
  {
8158
8822
  name: "delete_script_lines",
8159
8823
  category: "write",
8160
- description: "Delete a range of lines. 1-indexed, inclusive.",
8824
+ description: "Delete a range of lines. line_range is 1-indexed and inclusive.",
8161
8825
  inputSchema: {
8162
8826
  type: "object",
8163
8827
  properties: {
@@ -8165,20 +8829,16 @@ var init_definitions = __esm({
8165
8829
  type: "string",
8166
8830
  description: "Canonical path to a LuaSourceContainer"
8167
8831
  },
8168
- startLine: {
8169
- type: "number",
8170
- description: "Start line (1-indexed)"
8171
- },
8172
- endLine: {
8173
- type: "number",
8174
- description: "End line (inclusive)"
8832
+ line_range: {
8833
+ type: "string",
8834
+ description: 'Line range to delete: "start-end" (e.g. "100-200") or a single line ("42"). Open-ended ranges are not accepted for deletion.'
8175
8835
  },
8176
8836
  instance_id: {
8177
8837
  type: "string",
8178
8838
  description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
8179
8839
  }
8180
8840
  },
8181
- required: ["instancePath", "startLine", "endLine"]
8841
+ required: ["instancePath", "line_range"]
8182
8842
  }
8183
8843
  },
8184
8844
  // === Attributes ===
@@ -8424,21 +9084,21 @@ var init_definitions = __esm({
8424
9084
  {
8425
9085
  name: "grep_scripts",
8426
9086
  category: "read",
8427
- description: "Ripgrep-inspired search across all script sources. Supports literal and Lua pattern matching, context lines, early termination, and results grouped by script with line/column numbers.",
9087
+ description: 'Ripgrep-inspired search across all script sources. Supports literal and Lua pattern matching (with top-level "|" alternation), context lines, early termination, and results grouped by script with line/column numbers.',
8428
9088
  inputSchema: {
8429
9089
  type: "object",
8430
9090
  properties: {
8431
9091
  pattern: {
8432
9092
  type: "string",
8433
- description: "Search pattern (literal string or Lua pattern)"
9093
+ description: 'Search text. Literal by default; with usePattern=true it is a case-sensitive Lua pattern with top-level "|" alternation (e.g. "foo|bar").'
8434
9094
  },
8435
9095
  caseSensitive: {
8436
9096
  type: "boolean",
8437
- description: "Case-sensitive search (default: false)"
9097
+ description: "Literal search case sensitivity (default: false). Lua pattern mode is always case-sensitive; passing false with usePattern=true is rejected."
8438
9098
  },
8439
9099
  usePattern: {
8440
9100
  type: "boolean",
8441
- description: "Use Lua pattern matching instead of literal (default: false)"
9101
+ description: 'Use case-sensitive Lua pattern matching instead of literal search (default: false). Supports top-level alternation: "a|b" matches a line containing "a" or "b". Note: Lua patterns are NOT PCRE \u2014 use %d/%a/%w classes and ".-" (not ".*?"); ^ $ ( ) . % + - * ? [ ] are magic.'
8442
9102
  },
8443
9103
  contextLines: {
8444
9104
  type: "number",
@@ -8477,7 +9137,7 @@ var init_definitions = __esm({
8477
9137
  {
8478
9138
  name: "manage_instance",
8479
9139
  category: "write",
8480
- description: 'Launch, close, inspect, and find revisions for Studio instances. Use action="list_place_versions" with place_id to retrieve version numbers through Open Cloud asset versions, then action="launch" with source="place_revision" 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.',
9140
+ 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.',
8481
9141
  inputSchema: {
8482
9142
  type: "object",
8483
9143
  properties: {
@@ -8489,7 +9149,7 @@ var init_definitions = __esm({
8489
9149
  source: {
8490
9150
  type: "string",
8491
9151
  enum: ["baseplate", "local_file", "published_place", "place_revision"],
8492
- description: 'Required for action="launch". published_place opens the latest place; place_revision opens a specific older version as an anonymous local copy.'
9152
+ description: 'Required for action="launch". baseplate/local_file do not use place_id; published_place opens the latest place; place_revision opens a specific older version as an anonymous local copy.'
8493
9153
  },
8494
9154
  local_place_file: {
8495
9155
  type: "string",
@@ -8497,11 +9157,7 @@ var init_definitions = __esm({
8497
9157
  },
8498
9158
  place_id: {
8499
9159
  type: "number",
8500
- description: 'Required for source="published_place", source="place_revision", and action="list_place_versions".'
8501
- },
8502
- universe_id: {
8503
- type: "number",
8504
- description: "Optional for published_place/place_revision launches; derived from place_id when omitted."
9160
+ description: 'Only used for source="published_place", source="place_revision", and action="list_place_versions". Do not pass for source="baseplate" or source="local_file".'
8505
9161
  },
8506
9162
  place_version: {
8507
9163
  type: "number",
@@ -8838,7 +9494,7 @@ var init_definitions = __esm({
8838
9494
  {
8839
9495
  name: "multiplayer_playtest",
8840
9496
  category: "write",
8841
- description: 'Start, inspect, add players to, remove a client from, or end a StudioTestService multiplayer playtest. Use action="start" with numPlayers, action="status", action="add_players" with numPlayers, action="leave_client" with target="client-N", or action="end". Returns brief lifecycle status only; read script output with get_runtime_logs.',
9497
+ description: 'Start or inspect a StudioTestService multiplayer playtest. Use action="start" with numPlayers and force=true only when you accept that MCP cannot stop it and you must manually close the multiplayer test windows afterward. action="status" inspects state, action="add_players" adds players, and action="leave_client" removes one client. action="end" is disabled for now and returns the StudioTestService:EndTest broken-API reason. Returns brief lifecycle status only; read script output with get_runtime_logs.',
8842
9498
  inputSchema: {
8843
9499
  type: "object",
8844
9500
  properties: {
@@ -8859,11 +9515,15 @@ var init_definitions = __esm({
8859
9515
  description: 'For action="start": JSON-compatible table passed to StudioTestService:GetTestArgs() on server and clients.'
8860
9516
  },
8861
9517
  value: {
8862
- description: 'For action="end": JSON-compatible value returned to the edit-side ExecuteMultiplayerTestAsync call.'
9518
+ description: 'Ignored while action="end" is disabled.'
9519
+ },
9520
+ force: {
9521
+ type: "boolean",
9522
+ description: 'Required for action="start". Pass true only if you understand StudioTestService:EndTest is broken in this flow and you will manually close the multiplayer test windows.'
8863
9523
  },
8864
9524
  timeout: {
8865
9525
  type: "number",
8866
- description: "Max seconds to wait for action completion. Defaults to 30."
9526
+ description: "Max seconds to wait for start peer detection or action completion. Defaults to 30."
8867
9527
  },
8868
9528
  instance_id: {
8869
9529
  type: "string",
@@ -10143,7 +10803,7 @@ part(0,2,0,2,1,1,"b")`,
10143
10803
  {
10144
10804
  name: "multiplayer_test_start",
10145
10805
  category: "write",
10146
- description: 'Deprecated. Use multiplayer_playtest with action="start" instead. Starts a StudioTestService multiplayer test.',
10806
+ description: 'Deprecated. Use multiplayer_playtest with action="start" instead. Starts a StudioTestService multiplayer test only when force=true acknowledges that MCP cannot stop it and the test windows must be closed manually.',
10147
10807
  inputSchema: {
10148
10808
  type: "object",
10149
10809
  properties: {
@@ -10154,6 +10814,10 @@ part(0,2,0,2,1,1,"b")`,
10154
10814
  testArgs: {
10155
10815
  description: "JSON-compatible table passed to StudioTestService:GetTestArgs() on server and clients."
10156
10816
  },
10817
+ force: {
10818
+ type: "boolean",
10819
+ description: "Required. Pass true only if you understand StudioTestService:EndTest is broken in this flow and you will manually close the multiplayer test windows."
10820
+ },
10157
10821
  timeout: {
10158
10822
  type: "number",
10159
10823
  description: "Max seconds to wait for server + clients to register (default 30)."
@@ -10163,7 +10827,7 @@ part(0,2,0,2,1,1,"b")`,
10163
10827
  description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
10164
10828
  }
10165
10829
  },
10166
- required: ["numPlayers"]
10830
+ required: ["numPlayers", "force"]
10167
10831
  }
10168
10832
  },
10169
10833
  {
@@ -10228,16 +10892,16 @@ part(0,2,0,2,1,1,"b")`,
10228
10892
  {
10229
10893
  name: "multiplayer_test_end",
10230
10894
  category: "write",
10231
- description: 'Deprecated. Use multiplayer_playtest with action="end" instead. Ends a running StudioTestService multiplayer test.',
10895
+ description: "Deprecated. Multiplayer StudioTestService stop/end is disabled for now because StudioTestService:EndTest is broken in this flow. This tool returns a disabled error and does not call EndTest.",
10232
10896
  inputSchema: {
10233
10897
  type: "object",
10234
10898
  properties: {
10235
10899
  value: {
10236
- description: "JSON-compatible value returned to the edit-side ExecuteMultiplayerTestAsync call."
10900
+ description: "Ignored while multiplayer stop/end is disabled."
10237
10901
  },
10238
10902
  timeout: {
10239
10903
  type: "number",
10240
- description: "Max seconds to wait for runtime peers to disconnect (default 30)."
10904
+ description: "Ignored while multiplayer stop/end is disabled."
10241
10905
  },
10242
10906
  instance_id: {
10243
10907
  type: "string",
@@ -10253,15 +10917,15 @@ part(0,2,0,2,1,1,"b")`,
10253
10917
  });
10254
10918
 
10255
10919
  // ../core/dist/install-plugin-helpers.js
10256
- import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
10920
+ import { existsSync as existsSync4, readFileSync as readFileSync5, unlinkSync } from "fs";
10257
10921
  import { execSync } from "child_process";
10258
- import { join as join3 } from "path";
10259
- import { homedir as homedir3 } from "os";
10922
+ import { join as join4 } from "path";
10923
+ import { homedir as homedir4 } from "os";
10260
10924
  function isWSL() {
10261
10925
  if (process.platform !== "linux")
10262
10926
  return false;
10263
10927
  try {
10264
- const v = readFileSync4("/proc/version", "utf8");
10928
+ const v = readFileSync5("/proc/version", "utf8");
10265
10929
  return /microsoft|wsl/i.test(v);
10266
10930
  } catch {
10267
10931
  return false;
@@ -10279,7 +10943,7 @@ function getWindowsUserPluginsDir() {
10279
10943
  }).toString().trim();
10280
10944
  if (!linuxPath)
10281
10945
  return null;
10282
- return join3(linuxPath, "Roblox", "Plugins");
10946
+ return join4(linuxPath, "Roblox", "Plugins");
10283
10947
  } catch {
10284
10948
  return null;
10285
10949
  }
@@ -10288,7 +10952,7 @@ function getPluginsFolder() {
10288
10952
  if (process.env.MCP_PLUGINS_DIR)
10289
10953
  return process.env.MCP_PLUGINS_DIR;
10290
10954
  if (process.platform === "win32") {
10291
- return join3(process.env.LOCALAPPDATA || join3(homedir3(), "AppData", "Local"), "Roblox", "Plugins");
10955
+ return join4(process.env.LOCALAPPDATA || join4(homedir4(), "AppData", "Local"), "Roblox", "Plugins");
10292
10956
  }
10293
10957
  if (isWSL()) {
10294
10958
  const win = getWindowsUserPluginsDir();
@@ -10296,10 +10960,10 @@ function getPluginsFolder() {
10296
10960
  return win;
10297
10961
  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.");
10298
10962
  }
10299
- return join3(homedir3(), "Documents", "Roblox", "Plugins");
10963
+ return join4(homedir4(), "Documents", "Roblox", "Plugins");
10300
10964
  }
10301
10965
  function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
10302
- const otherDest = join3(pluginsFolder, otherAssetName);
10966
+ const otherDest = join4(pluginsFolder, otherAssetName);
10303
10967
  if (!existsSync4(otherDest))
10304
10968
  return;
10305
10969
  if (replace) {
@@ -10345,8 +11009,8 @@ __export(install_plugin_exports, {
10345
11009
  installBundledPlugin: () => installBundledPlugin,
10346
11010
  installPlugin: () => installPlugin
10347
11011
  });
10348
- import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2 } from "fs";
10349
- import { dirname as dirname3, join as join4 } from "path";
11012
+ import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync6, unlinkSync as unlinkSync2 } from "fs";
11013
+ import { dirname as dirname3, join as join5 } from "path";
10350
11014
  import { fileURLToPath } from "url";
10351
11015
  import { get } from "https";
10352
11016
  function httpsGet(url) {
@@ -10407,7 +11071,7 @@ function prepareInstall({
10407
11071
  }) {
10408
11072
  const pluginsFolder = getPluginsFolder();
10409
11073
  if (!existsSync5(pluginsFolder)) {
10410
- mkdirSync3(pluginsFolder, { recursive: true });
11074
+ mkdirSync4(pluginsFolder, { recursive: true });
10411
11075
  }
10412
11076
  handleVariantConflict({
10413
11077
  pluginsFolder,
@@ -10421,21 +11085,21 @@ function prepareInstall({
10421
11085
  function bundledAssetPath() {
10422
11086
  const currentDir = dirname3(fileURLToPath(import.meta.url));
10423
11087
  const candidates = [
10424
- join4(currentDir, "..", "studio-plugin", ASSET_NAME),
10425
- join4(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
11088
+ join5(currentDir, "..", "studio-plugin", ASSET_NAME),
11089
+ join5(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
10426
11090
  ];
10427
11091
  return candidates.find((candidate) => existsSync5(candidate)) ?? null;
10428
11092
  }
10429
11093
  function packageVersion() {
10430
11094
  const currentDir = dirname3(fileURLToPath(import.meta.url));
10431
- const pkg = JSON.parse(readFileSync5(join4(currentDir, "..", "package.json"), "utf8"));
11095
+ const pkg = JSON.parse(readFileSync6(join5(currentDir, "..", "package.json"), "utf8"));
10432
11096
  if (!pkg.version) {
10433
11097
  throw new Error("Package version not found");
10434
11098
  }
10435
11099
  return pkg.version;
10436
11100
  }
10437
11101
  function bundledPluginVersion(source) {
10438
- const match = readFileSync5(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
11102
+ const match = readFileSync6(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
10439
11103
  return match ? match[1] : null;
10440
11104
  }
10441
11105
  function assertBundledPluginVersion(source) {
@@ -10449,8 +11113,8 @@ function assertBundledPluginVersion(source) {
10449
11113
  }
10450
11114
  function filesMatch(a, b) {
10451
11115
  if (!existsSync5(b)) return false;
10452
- const aBytes = readFileSync5(a);
10453
- const bBytes = readFileSync5(b);
11116
+ const aBytes = readFileSync6(a);
11117
+ const bBytes = readFileSync6(b);
10454
11118
  return aBytes.length === bBytes.length && aBytes.equals(bBytes);
10455
11119
  }
10456
11120
  async function installBundledPlugin(options = {}) {
@@ -10463,7 +11127,7 @@ async function installBundledPlugin(options = {}) {
10463
11127
  }
10464
11128
  assertBundledPluginVersion(source);
10465
11129
  const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
10466
- const dest = join4(pluginsFolder, ASSET_NAME);
11130
+ const dest = join5(pluginsFolder, ASSET_NAME);
10467
11131
  if (filesMatch(source, dest)) return;
10468
11132
  copyFileSync2(source, dest);
10469
11133
  log(`Installed ${ASSET_NAME} to ${dest}`);
@@ -10476,7 +11140,7 @@ async function installPlugin(options = {}) {
10476
11140
  const bundled = bundledAssetPath();
10477
11141
  if (bundled) {
10478
11142
  assertBundledPluginVersion(bundled);
10479
- const dest2 = join4(pluginsFolder, ASSET_NAME);
11143
+ const dest2 = join5(pluginsFolder, ASSET_NAME);
10480
11144
  if (filesMatch(bundled, dest2)) {
10481
11145
  log(`${ASSET_NAME} already installed.`);
10482
11146
  return;
@@ -10491,7 +11155,7 @@ async function installPlugin(options = {}) {
10491
11155
  if (!asset) {
10492
11156
  throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
10493
11157
  }
10494
- const dest = join4(pluginsFolder, ASSET_NAME);
11158
+ const dest = join5(pluginsFolder, ASSET_NAME);
10495
11159
  log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
10496
11160
  await download(asset.browser_download_url, dest);
10497
11161
  log(`Installed to ${dest}`);