@chrrxs/robloxstudio-mcp-inspector 3.1.4 → 3.1.5

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
@@ -2869,7 +2869,7 @@ var BridgeService = class {
2869
2869
  operation.status.executionStartedAt = now;
2870
2870
  else {
2871
2871
  operation.status.executionOutcome = outcome ?? "unknown";
2872
- if (outcome !== "not_executed")
2872
+ if (outcome === "success" || outcome === "error")
2873
2873
  operation.status.executionCompletedAt = now;
2874
2874
  }
2875
2875
  operation.updatedAt = now;
@@ -2885,7 +2885,7 @@ var BridgeService = class {
2885
2885
  requestId,
2886
2886
  targetPeerId: operation.status.targetPeerId,
2887
2887
  ...observations(operation.status),
2888
- executionCompletedAt: executionOutcome === "not_executed" ? void 0 : operation.status.executionCompletedAt ?? Date.now(),
2888
+ executionCompletedAt: executionOutcome === "success" || executionOutcome === "error" ? operation.status.executionCompletedAt ?? Date.now() : void 0,
2889
2889
  stage: "response_delivery",
2890
2890
  outcome: executionOutcome === "not_executed" ? "not_executed" : "unknown",
2891
2891
  executionOutcome
@@ -2913,13 +2913,15 @@ var BridgeService = class {
2913
2913
  operation.status.state = "settled";
2914
2914
  const localRejection = error instanceof RequestFailure && error.details.transportStage === "server_send";
2915
2915
  const responseOutcome = handlerOutcome(response);
2916
- const completedOutcome = !hasError && responseOutcome === "error" ? "error" : executionOutcome ?? (localRejection ? "not_executed" : hasError ? operation.status.executionOutcome === "success" ? "success" : "error" : responseOutcome);
2916
+ const completedOutcome = executionOutcome === "unknown" || executionOutcome === "not_executed" ? executionOutcome : !hasError && responseOutcome === "error" ? "error" : executionOutcome ?? (localRejection ? "not_executed" : hasError ? operation.status.executionOutcome === "success" ? "success" : "error" : responseOutcome);
2917
2917
  operation.status.executionOutcome = completedOutcome;
2918
- operation.status.outcome = hasError || completedOutcome === "error" || completedOutcome === "not_executed" ? "error" : "success";
2918
+ operation.status.outcome = hasError || responseOutcome === "error" || completedOutcome === "error" || completedOutcome === "not_executed" ? "error" : "success";
2919
2919
  if (!localRejection) {
2920
2920
  operation.status.stage = "response_delivery";
2921
- if (completedOutcome !== "not_executed")
2921
+ if (completedOutcome === "success" || completedOutcome === "error")
2922
2922
  operation.status.executionCompletedAt ??= now;
2923
+ else
2924
+ delete operation.status.executionCompletedAt;
2923
2925
  }
2924
2926
  operation.status.settledAt = now;
2925
2927
  operation.updatedAt = now;
@@ -3322,6 +3324,10 @@ execute_luau runs through the Studio plugin. eval_server_runtime and eval_client
3322
3324
 
3323
3325
  Read output with get_runtime_logs. Reuse nextCursor as cursor for one Instance, or nextCursorByInstance as cursor_by_instance for a Multiplayer Group, instead of requesting the full process streams again.
3324
3326
 
3327
+ - Each Peer VM retains approximately 64 KiB of message text, rather than a fixed message count. Reads do not consume or clear the buffer; incoming messages evict old entries.
3328
+ - totalDropped sums lifetime eviction counts from successfully read Peers. It counts messages evicted, not messages missed by this caller, and may decrease when Peers fail, disconnect, or reload.
3329
+ - For successfully read Peers, nextCursor advances past all captured entries, including those omitted by filter or tail. It is a continuation watermark, not pagination through omitted results; continuing from it will not return those entries.
3330
+
3325
3331
  ## Simulation and input
3326
3332
 
3327
3333
  - Inspect current settings with get_simulation_state before changing them.
@@ -3950,6 +3956,40 @@ var StudioLaunchPreDispatchError = class extends Error {
3950
3956
  };
3951
3957
  }
3952
3958
  };
3959
+ var StudioCloseVerificationError = class extends Error {
3960
+ observation;
3961
+ constructor(message, observation) {
3962
+ super(message);
3963
+ this.observation = observation;
3964
+ }
3965
+ };
3966
+ async function beforeCloseDeadline(operation, deadline, processId) {
3967
+ const { promise, reject } = Promise.withResolvers();
3968
+ const timeoutError = () => new StudioCloseVerificationError(`Timed out verifying Studio process ${processId} termination; process state is unknown.`, { status: "unknown", observedAt: Date.now(), error: "Close operation deadline exceeded." });
3969
+ if (Date.now() >= deadline)
3970
+ throw timeoutError();
3971
+ const timer = setTimeout(() => reject(timeoutError()), deadline - Date.now());
3972
+ try {
3973
+ return await Promise.race([operation(), promise]);
3974
+ } finally {
3975
+ clearTimeout(timer);
3976
+ }
3977
+ }
3978
+ function observePosixProcess(processId) {
3979
+ try {
3980
+ process.kill(processId, 0);
3981
+ return { status: "running", observedAt: Date.now() };
3982
+ } catch (error) {
3983
+ if (error instanceof Error && "code" in error && error.code === "ESRCH") {
3984
+ return { status: "not_running", observedAt: Date.now(), reason: "missing" };
3985
+ }
3986
+ return {
3987
+ status: "unknown",
3988
+ observedAt: Date.now(),
3989
+ error: error instanceof Error ? error.message : String(error)
3990
+ };
3991
+ }
3992
+ }
3953
3993
  function run(command, args, options = {}) {
3954
3994
  return execFileSync2(command, args, {
3955
3995
  encoding: "utf8",
@@ -3971,9 +4011,10 @@ async function runAsync(command, args, options = {}) {
3971
4011
  function isWsl() {
3972
4012
  return getStudioPlatformCapabilities().isWsl;
3973
4013
  }
3974
- async function powershellAsync(script) {
4014
+ async function powershellAsync(script, timeoutMs = 15e3) {
3975
4015
  return runAsync("powershell.exe", ["-NoProfile", "-Command", script], {
3976
- cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
4016
+ cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
4017
+ timeout: timeoutMs
3977
4018
  });
3978
4019
  }
3979
4020
  function windowsLocalAppData() {
@@ -4108,8 +4149,15 @@ function quoteWindowsCommandLineArg(value) {
4108
4149
  }
4109
4150
  return `${quoted}${"\\".repeat(backslashes * 2)}"`;
4110
4151
  }
4111
- function buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment, windowsWorkingDirectory) {
4152
+ function parseStudioTestWorkerJobName(name) {
4153
+ if (name !== void 0 && !/^Local\\RsmcpStudioWorker-[a-f0-9]{32}$/u.test(name)) {
4154
+ throw new Error("Invalid Studio test worker job name.");
4155
+ }
4156
+ return name;
4157
+ }
4158
+ function buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment, windowsWorkingDirectory, testWorkerJobName = process.env.RSMCP_STUDIO_TEST_WORKER_JOB) {
4112
4159
  const environmentPatch = parseStudioProcessEnvironmentPatch(processEnvironment);
4160
+ parseStudioTestWorkerJobName(testWorkerJobName);
4113
4161
  const commandLine = [windowsExe, ...args].map(quoteWindowsCommandLineArg).join(" ");
4114
4162
  return [
4115
4163
  "$ErrorActionPreference = 'Stop'",
@@ -4222,6 +4270,15 @@ public sealed class McpSuspendedStudio : IDisposable
4222
4270
  [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
4223
4271
  private static extern IntPtr CreateJobObjectW(IntPtr jobAttributes, string name);
4224
4272
 
4273
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
4274
+ private static extern IntPtr OpenJobObjectW(uint access, bool inheritHandle, string name);
4275
+
4276
+ [DllImport("kernel32.dll", SetLastError = true)]
4277
+ private static extern bool GetHandleInformation(IntPtr handle, out uint flags);
4278
+
4279
+ [DllImport("kernel32.dll", SetLastError = true)]
4280
+ private static extern bool QueryInformationJobObject(IntPtr job, int informationClass, IntPtr information, uint informationLength, out uint returned);
4281
+
4225
4282
  [DllImport("kernel32.dll", SetLastError = true)]
4226
4283
  private static extern bool SetInformationJobObject(
4227
4284
  IntPtr job,
@@ -4309,20 +4366,60 @@ public sealed class McpSuspendedStudio : IDisposable
4309
4366
  throw new InvalidOperationException("Unexpected Studio process wait result: " + wait);
4310
4367
  }
4311
4368
 
4312
- public static McpSuspendedStudio Start(string application, string commandLine, string currentDirectory)
4369
+ private static Win32Exception TestWorkerError(string operation)
4370
+ {
4371
+ int error = Marshal.GetLastWin32Error();
4372
+ return new Win32Exception(error, operation + " (Win32 " + error + "): " + new Win32Exception(error).Message);
4373
+ }
4374
+
4375
+ private static IntPtr OpenTestWorkerJob(string name)
4376
+ {
4377
+ if (name == null) return IntPtr.Zero;
4378
+ if (!System.Text.RegularExpressions.Regex.IsMatch(name, @"\\ALocal\\\\RsmcpStudioWorker-[a-f0-9]{32}\\z"))
4379
+ throw new ArgumentException("Invalid Studio test worker job name");
4380
+ IntPtr handle = OpenJobObjectW(5, false, name);
4381
+ if (handle == IntPtr.Zero)
4382
+ throw TestWorkerError("Open test worker job failed");
4383
+ try
4384
+ {
4385
+ uint flags;
4386
+ if (!GetHandleInformation(handle, out flags))
4387
+ throw TestWorkerError("Get test worker job handle flags failed");
4388
+ if ((flags & 1) != 0)
4389
+ throw new InvalidOperationException("Test worker job handle must not be inherited");
4390
+ int length = Marshal.SizeOf(typeof(ExtendedLimitInformation));
4391
+ IntPtr buffer = Marshal.AllocHGlobal(length);
4392
+ try
4393
+ {
4394
+ uint returned;
4395
+ if (!QueryInformationJobObject(handle, JOB_OBJECT_EXTENDED_LIMIT_INFORMATION, buffer, (uint)length, out returned))
4396
+ throw TestWorkerError("Query test worker job limits failed");
4397
+ var information = (ExtendedLimitInformation)Marshal.PtrToStructure(buffer, typeof(ExtendedLimitInformation));
4398
+ if (returned != length || information.BasicLimitInformation.LimitFlags != JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)
4399
+ throw new InvalidOperationException("Test worker job must retain kill-on-close without breakaway");
4400
+ }
4401
+ finally { Marshal.FreeHGlobal(buffer); }
4402
+ return handle;
4403
+ }
4404
+ catch { CloseHandle(handle); throw; }
4405
+ }
4406
+
4407
+ public static McpSuspendedStudio Start(string application, string commandLine, string currentDirectory, string testWorkerJobName)
4313
4408
  {
4314
4409
  if (String.IsNullOrEmpty(currentDirectory))
4315
4410
  currentDirectory = null;
4316
4411
  IntPtr jobHandle = CreateJobObjectW(IntPtr.Zero, null);
4317
4412
  if (jobHandle == IntPtr.Zero)
4318
4413
  throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateJobObjectW failed");
4414
+ IntPtr workerJob = IntPtr.Zero;
4319
4415
  try
4320
4416
  {
4321
4417
  ConfigureKillOnClose(jobHandle, true);
4418
+ workerJob = OpenTestWorkerJob(testWorkerJobName);
4322
4419
  var startup = new StartupInfo();
4323
4420
  startup.cb = Marshal.SizeOf(startup);
4324
4421
  ProcessInformation created;
4325
- uint flags = CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB;
4422
+ uint flags = CREATE_SUSPENDED | (workerJob == IntPtr.Zero ? CREATE_BREAKAWAY_FROM_JOB : 0);
4326
4423
  bool started = CreateProcessW(application, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, false, flags, IntPtr.Zero, currentDirectory, ref startup, out created);
4327
4424
  if (!started && Marshal.GetLastWin32Error() == 5)
4328
4425
  started = CreateProcessW(application, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, false, CREATE_SUSPENDED, IntPtr.Zero, currentDirectory, ref startup, out created);
@@ -4330,6 +4427,10 @@ public sealed class McpSuspendedStudio : IDisposable
4330
4427
  throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateProcessW failed");
4331
4428
  try
4332
4429
  {
4430
+ // Worker lifetime encloses the private launch-authorization job.
4431
+ // COMPLETE may release authorization, never test-worker ownership.
4432
+ if (workerJob != IntPtr.Zero && !AssignProcessToJobObject(workerJob, created.hProcess))
4433
+ throw TestWorkerError("Assign Studio to test worker job failed");
4333
4434
  if (!AssignProcessToJobObject(jobHandle, created.hProcess))
4334
4435
  throw new Win32Exception(Marshal.GetLastWin32Error(), "AssignProcessToJobObject failed");
4335
4436
  return new McpSuspendedStudio(created, jobHandle);
@@ -4353,6 +4454,10 @@ public sealed class McpSuspendedStudio : IDisposable
4353
4454
  CloseHandle(jobHandle);
4354
4455
  throw;
4355
4456
  }
4457
+ finally
4458
+ {
4459
+ if (workerJob != IntPtr.Zero) CloseHandle(workerJob);
4460
+ }
4356
4461
  }
4357
4462
 
4358
4463
  public void Resume()
@@ -4402,7 +4507,7 @@ public sealed class McpSuspendedStudio : IDisposable
4402
4507
  }
4403
4508
  }
4404
4509
  '@`,
4405
- `$launch = [McpSuspendedStudio]::Start(${powershellStringLiteral(windowsExe)}, ${powershellStringLiteral(commandLine)}, ${windowsWorkingDirectory === void 0 ? "$null" : powershellStringLiteral(windowsWorkingDirectory)})`,
4510
+ `$launch = [McpSuspendedStudio]::Start(${powershellStringLiteral(windowsExe)}, ${powershellStringLiteral(commandLine)}, ${windowsWorkingDirectory === void 0 ? "$null" : powershellStringLiteral(windowsWorkingDirectory)}, ${testWorkerJobName === void 0 ? "$null" : powershellStringLiteral(testWorkerJobName)})`,
4406
4511
  "try {",
4407
4512
  "[Console]::Out.WriteLine((ConvertTo-Json @{ pid = $launch.ProcessId; started = [string]$launch.StartedAtFileTime } -Compress)); [Console]::Out.Flush()",
4408
4513
  "$accepted = $false",
@@ -4441,13 +4546,14 @@ function buildWindowsStudioStopScript(processId, startedAt) {
4441
4546
  "} finally { if ($null -ne $studio) { $studio.Dispose() } }"
4442
4547
  ].join("; ");
4443
4548
  }
4444
- async function stopWindowsStudio(processId, startedAt) {
4445
- await powershellAsync(buildWindowsStudioStopScript(processId, startedAt));
4549
+ async function stopWindowsStudio(processId, startedAt, timeoutMs = 15e3) {
4550
+ await powershellAsync(buildWindowsStudioStopScript(processId, startedAt), timeoutMs);
4446
4551
  }
4447
4552
  async function spawnWindowsStudio(exe, args, processEnvironment, studioWorkingDirectory) {
4448
4553
  const windowsExe = await toStudioLaunchArgAsync(exe);
4449
4554
  const windowsWorkingDirectory = studioWorkingDirectory ? await toStudioLaunchArgAsync(studioWorkingDirectory) : void 0;
4450
- const script = buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment, windowsWorkingDirectory);
4555
+ const testWorkerJobName = process.env.RSMCP_STUDIO_TEST_WORKER_JOB;
4556
+ const script = buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment, windowsWorkingDirectory, testWorkerJobName);
4451
4557
  const launcher = spawn("powershell.exe", ["-NoProfile", "-Command", script], {
4452
4558
  cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
4453
4559
  stdio: ["pipe", "pipe", "pipe"],
@@ -4754,6 +4860,24 @@ function prepareStudioLaunchOptions(options) {
4754
4860
  localPlaceFile: createBaseplatePlaceFile()
4755
4861
  };
4756
4862
  }
4863
+ function resolveStudioExeFromVersions(root) {
4864
+ if (!existsSync2(root)) {
4865
+ throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
4866
+ }
4867
+ const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
4868
+ if (candidates.length === 0) {
4869
+ throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
4870
+ }
4871
+ const exe = candidates[0];
4872
+ const installationDir = path2.dirname(exe);
4873
+ const hasIncompleteDownload = readdirSync(installationDir).some((name) => /\.crdownload$/i.test(name));
4874
+ const settings = statSync(path2.join(installationDir, "AppSettings.xml"), { throwIfNoEntry: false });
4875
+ if (hasIncompleteDownload || !settings?.isFile() || settings.size === 0) {
4876
+ const reason = hasIncompleteDownload ? "an unfinished .crdownload download is present" : "AppSettings.xml is missing, empty, or not a regular file";
4877
+ throw new Error(`Roblox Studio installation/update is incomplete at ${installationDir}: ${reason}. Complete the installation/update or reinstall Roblox Studio under the owning Windows account before launching.`);
4878
+ }
4879
+ return exe;
4880
+ }
4757
4881
  function resolveStudioExe() {
4758
4882
  if (process.env.ROBLOX_STUDIO_EXE)
4759
4883
  return process.env.ROBLOX_STUDIO_EXE;
@@ -4765,14 +4889,7 @@ function resolveStudioExe() {
4765
4889
  }
4766
4890
  const localAppData = windowsLocalAppData();
4767
4891
  const root = localAppData ? path2.join(toWslPath(localAppData), "Roblox", "Versions") : path2.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
4768
- if (!existsSync2(root)) {
4769
- throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
4770
- }
4771
- const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
4772
- if (candidates.length === 0) {
4773
- throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
4774
- }
4775
- return candidates[0];
4892
+ return resolveStudioExeFromVersions(root);
4776
4893
  }
4777
4894
  async function resolveStudioExeAsync() {
4778
4895
  if (process.env.ROBLOX_STUDIO_EXE)
@@ -4785,14 +4902,7 @@ async function resolveStudioExeAsync() {
4785
4902
  }
4786
4903
  const localAppData = await windowsLocalAppDataAsync();
4787
4904
  const root = localAppData ? path2.join(await toWslPathAsync(localAppData), "Roblox", "Versions") : path2.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
4788
- if (!existsSync2(root)) {
4789
- throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
4790
- }
4791
- const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
4792
- if (candidates.length === 0) {
4793
- throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
4794
- }
4795
- return candidates[0];
4905
+ return resolveStudioExeFromVersions(root);
4796
4906
  }
4797
4907
  var WINDOWS_STUDIO_PROCESS_QUERY = [
4798
4908
  "$ErrorActionPreference = 'Stop'",
@@ -4809,13 +4919,13 @@ function parseWindowsStudioProcesses(output) {
4809
4919
  }
4810
4920
  return processes;
4811
4921
  }
4812
- async function observeStudioProcesses() {
4922
+ async function observeStudioProcesses(timeoutMs = 15e3) {
4813
4923
  const observedAt = Date.now();
4814
4924
  try {
4815
4925
  if (process.platform === "darwin") {
4816
4926
  let out2 = "";
4817
4927
  try {
4818
- out2 = await runAsync("pgrep", ["-fl", "RobloxStudio"]);
4928
+ out2 = await runAsync("pgrep", ["-fl", "RobloxStudio"], { timeout: timeoutMs });
4819
4929
  } catch (error) {
4820
4930
  const code = error.code;
4821
4931
  if (Number(code) === 1)
@@ -4831,7 +4941,7 @@ async function observeStudioProcesses() {
4831
4941
  if (process.platform !== "win32" && !isWsl()) {
4832
4942
  return { status: "ok", observedAt, processes: [] };
4833
4943
  }
4834
- const out = await powershellAsync(WINDOWS_STUDIO_PROCESS_QUERY);
4944
+ const out = await powershellAsync(WINDOWS_STUDIO_PROCESS_QUERY, timeoutMs);
4835
4945
  return { status: "ok", observedAt, processes: parseWindowsStudioProcesses(out) };
4836
4946
  } catch (error) {
4837
4947
  return {
@@ -4915,6 +5025,7 @@ var StudioInstanceManager = class {
4915
5025
  confirmedExitGraceMs;
4916
5026
  snapshotCacheMs;
4917
5027
  launchCompletionTimeoutMs;
5028
+ closeTimeoutMs;
4918
5029
  coordinatorTimer;
4919
5030
  coordinatorRefresh;
4920
5031
  cachedSnapshot;
@@ -4929,6 +5040,7 @@ var StudioInstanceManager = class {
4929
5040
  this.confirmedExitGraceMs = options.confirmedExitGraceMs ?? 5e3;
4930
5041
  this.snapshotCacheMs = options.snapshotCacheMs ?? 0;
4931
5042
  this.launchCompletionTimeoutMs = options.launchCompletionTimeoutMs ?? LAUNCH_COMPLETION_TIMEOUT_MS;
5043
+ this.closeTimeoutMs = options.closeTimeoutMs ?? 1e4;
4932
5044
  }
4933
5045
  getLifecycleCapabilities() {
4934
5046
  const platform = this.platformCapabilities ?? getStudioPlatformCapabilities();
@@ -5326,58 +5438,57 @@ var StudioInstanceManager = class {
5326
5438
  instanceId: record.instanceId
5327
5439
  };
5328
5440
  }
5441
+ const deadline = Date.now() + this.closeTimeoutMs;
5329
5442
  const processId = record.nativeProcessId ?? record.spawnPid;
5330
5443
  if (!processId) {
5331
5444
  throw new Error(`Cannot close ${record.instanceId ?? "Studio launch"} because its process id was not detected.`);
5332
5445
  }
5333
5446
  const control = record.recordId ? this.launchControls.get(record.recordId) : void 0;
5334
- if (record.processAuthorizationState !== "released" && control) {
5335
- await control.abort();
5336
- const closedAt2 = Date.now();
5337
- record.closedAt = closedAt2;
5338
- record.exitedAt = closedAt2;
5339
- if (record.state !== "failed")
5340
- record.state = "exited";
5341
- record.processObservationStatus = "not_running";
5342
- record.lastProcessObservationAt = closedAt2;
5343
- record.lastSuccessfulProcessObservationAt = closedAt2;
5344
- record.lastProcessObservationError = void 0;
5345
- this.cleanupManagedRecord(record);
5346
- this.markClosedInMemory(record);
5347
- await this.persist(record);
5348
- return {
5349
- status: "closed",
5350
- launchId: record.recordId,
5351
- instanceId: record.instanceId
5352
- };
5353
- }
5354
- const snapshot = await this.getProcessSnapshot(true);
5355
- const observation = this.observeRecord(record, snapshot);
5356
- if (observation.status === "unknown") {
5357
- await this.applyProcessObservation(record, observation);
5358
- throw new Error(`Cannot verify the managed Studio process because process observation failed: ${observation.error}`);
5359
- }
5360
- if (observation.status === "not_running") {
5361
- await this.markProcessExited(record, void 0, observation.reason === "identity_mismatch" ? "Studio process identity changed; the retained PID was not reused." : record.failureReason);
5362
- await this.registry.logEvent({
5363
- event: "registry_close_already_stopped",
5364
- recordId: record.recordId,
5365
- instanceId: record.instanceId,
5366
- source: record.source,
5367
- reason: observation.reason === "identity_mismatch" ? "identity_mismatch" : "pid_not_running",
5368
- action: "marked_closed_and_cleaned_baseplate"
5369
- });
5370
- return {
5371
- status: "already_closed",
5372
- launchId: record.recordId,
5373
- instanceId: record.instanceId
5374
- };
5447
+ const abort = record.processAuthorizationState !== "released" && control ? () => control.abort() : void 0;
5448
+ if (!abort) {
5449
+ let snapshot;
5450
+ try {
5451
+ snapshot = await beforeCloseDeadline(() => this.readProcessSnapshot(deadline - Date.now()), deadline, processId);
5452
+ } catch (error) {
5453
+ if (error instanceof StudioCloseVerificationError) {
5454
+ await this.applyProcessObservation(record, error.observation);
5455
+ }
5456
+ throw error;
5457
+ }
5458
+ const observation = this.observeRecord(record, snapshot);
5459
+ if (observation.status === "unknown") {
5460
+ await this.applyProcessObservation(record, observation);
5461
+ throw new Error(`Cannot verify the managed Studio process because process observation failed: ${observation.error}`);
5462
+ }
5463
+ if (observation.status === "not_running") {
5464
+ await this.markProcessExited(record, void 0, observation.reason === "identity_mismatch" ? "Studio process identity changed; the retained PID was not reused." : record.failureReason);
5465
+ await this.registry.logEvent({
5466
+ event: "registry_close_already_stopped",
5467
+ recordId: record.recordId,
5468
+ instanceId: record.instanceId,
5469
+ source: record.source,
5470
+ reason: observation.reason === "identity_mismatch" ? "identity_mismatch" : "pid_not_running",
5471
+ action: "marked_closed_and_cleaned_baseplate"
5472
+ });
5473
+ return {
5474
+ status: "already_closed",
5475
+ launchId: record.recordId,
5476
+ instanceId: record.instanceId
5477
+ };
5478
+ }
5375
5479
  }
5376
5480
  try {
5377
- await this.closeProcess(processId, record.nativeProcessStartedAt);
5481
+ await this.closeProcess(processId, record.nativeProcessStartedAt, abort, deadline);
5378
5482
  } catch (error) {
5379
- const retry = await this.getProcessSnapshot(true);
5483
+ if (error instanceof StudioCloseVerificationError) {
5484
+ await this.applyProcessObservation(record, error.observation);
5485
+ throw error;
5486
+ }
5487
+ if (abort)
5488
+ throw error;
5489
+ const retry = await beforeCloseDeadline(() => this.readProcessSnapshot(deadline - Date.now()), deadline, processId);
5380
5490
  const retryObservation = this.observeRecord(record, retry);
5491
+ await this.applyProcessObservation(record, retryObservation);
5381
5492
  if (retryObservation.status === "running" || retryObservation.status === "unknown")
5382
5493
  throw error;
5383
5494
  await this.registry.logEvent({
@@ -5414,7 +5525,8 @@ var StudioInstanceManager = class {
5414
5525
  };
5415
5526
  }
5416
5527
  async closeConnectedInstance(instance) {
5417
- const snapshot = await this.getProcessSnapshot(true);
5528
+ const deadline = Date.now() + this.closeTimeoutMs;
5529
+ const snapshot = await beforeCloseDeadline(() => this.readProcessSnapshot(deadline - Date.now()), deadline, 0);
5418
5530
  if (snapshot.status === "error") {
5419
5531
  throw new Error(`Could not enumerate Studio processes: ${snapshot.error}`);
5420
5532
  }
@@ -5422,27 +5534,65 @@ var StudioInstanceManager = class {
5422
5534
  if (!process2) {
5423
5535
  throw new Error(`Could not find a Studio process for connected instance "${instance.instanceId}".`);
5424
5536
  }
5425
- await this.closeProcess(process2.Id, process2.StartTimeUtcFileTime);
5537
+ await this.closeProcess(process2.Id, process2.StartTimeUtcFileTime, void 0, deadline);
5426
5538
  }
5427
- async closeProcess(processId, startedAt) {
5428
- if (this.processAdapter.stopProcess) {
5429
- await this.processAdapter.stopProcess(processId, startedAt);
5430
- return;
5431
- }
5432
- if (process.platform === "win32" || isWsl()) {
5433
- if (startedAt === void 0) {
5434
- throw new Error("Cannot stop a Windows Studio process without its creation-time identity.");
5539
+ async closeProcess(processId, startedAt, stop, deadline = Date.now() + this.closeTimeoutMs) {
5540
+ this.cachedSnapshot = void 0;
5541
+ await beforeCloseDeadline(async () => {
5542
+ if (stop) {
5543
+ await stop();
5544
+ } else if (this.processAdapter.stopProcess) {
5545
+ await this.processAdapter.stopProcess(processId, startedAt);
5546
+ } else if (process.platform === "win32" || isWsl()) {
5547
+ if (startedAt === void 0) {
5548
+ throw new Error("Cannot stop a Windows Studio process without its creation-time identity.");
5549
+ }
5550
+ await stopWindowsStudio(processId, startedAt, Math.max(1, deadline - Date.now()));
5551
+ } else {
5552
+ try {
5553
+ process.kill(processId, "SIGTERM");
5554
+ } catch (error) {
5555
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ESRCH")
5556
+ throw error;
5557
+ }
5435
5558
  }
5436
- await stopWindowsStudio(processId, startedAt);
5437
- } else {
5438
- try {
5439
- process.kill(processId, "SIGTERM");
5440
- } catch (error) {
5441
- if (error.code !== "ESRCH")
5442
- throw error;
5559
+ }, deadline, processId);
5560
+ while (true) {
5561
+ const observation = await beforeCloseDeadline(() => this.observeClosingProcess(processId, startedAt, deadline), deadline, processId);
5562
+ if (observation.status === "not_running")
5563
+ return;
5564
+ if (observation.status === "unknown") {
5565
+ throw new StudioCloseVerificationError(`Could not verify Studio process ${processId} termination: ${observation.error}`, observation);
5566
+ }
5567
+ const remaining = deadline - Date.now();
5568
+ if (remaining <= 0) {
5569
+ throw new StudioCloseVerificationError(`Timed out closing Studio process ${processId}: the process is still running. Close can be retried.`, observation);
5570
+ }
5571
+ await delay2(Math.min(100, remaining));
5572
+ if (Date.now() >= deadline) {
5573
+ throw new StudioCloseVerificationError(`Timed out closing Studio process ${processId}: the process is still running. Close can be retried.`, observation);
5443
5574
  }
5444
5575
  }
5445
5576
  }
5577
+ async observeClosingProcess(processId, startedAt, deadline) {
5578
+ if (process.platform === "darwin" && !this.processAdapter.observeStudioProcesses && !this.processAdapter.listStudioProcesses) {
5579
+ return observePosixProcess(processId);
5580
+ }
5581
+ const snapshot = await this.readProcessSnapshot(Math.max(1, deadline - Date.now()));
5582
+ if (snapshot.status === "error") {
5583
+ return { status: "unknown", observedAt: snapshot.observedAt, error: snapshot.error };
5584
+ }
5585
+ const target = snapshot.processes.find((candidate) => candidate.Id === processId);
5586
+ if (!target)
5587
+ return { status: "not_running", observedAt: snapshot.observedAt, reason: "missing" };
5588
+ if (startedAt !== void 0 && target.StartTimeUtcFileTime !== startedAt) {
5589
+ if (target.StartTimeUtcFileTime === void 0) {
5590
+ return { status: "unknown", observedAt: snapshot.observedAt, error: "Process creation-time identity is unavailable." };
5591
+ }
5592
+ return { status: "not_running", observedAt: snapshot.observedAt, reason: "identity_mismatch" };
5593
+ }
5594
+ return { status: "running", observedAt: snapshot.observedAt };
5595
+ }
5446
5596
  findProcessForConnectedInstance(instance, processes) {
5447
5597
  if (processes.length === 0)
5448
5598
  return void 0;
@@ -5471,24 +5621,7 @@ var StudioInstanceManager = class {
5471
5621
  return this.snapshotInFlight;
5472
5622
  this.snapshotInFlight = (async () => {
5473
5623
  try {
5474
- let snapshot;
5475
- if (this.processAdapter.observeStudioProcesses) {
5476
- snapshot = await this.processAdapter.observeStudioProcesses();
5477
- } else if (this.processAdapter.listStudioProcesses) {
5478
- const observedAt = Date.now();
5479
- const processes = await this.processAdapter.listStudioProcesses();
5480
- snapshot = { status: "ok", observedAt, processes };
5481
- } else {
5482
- snapshot = await observeStudioProcesses();
5483
- }
5484
- this.cachedSnapshot = snapshot;
5485
- return snapshot;
5486
- } catch (error) {
5487
- const snapshot = {
5488
- status: "error",
5489
- observedAt: Date.now(),
5490
- error: error instanceof Error ? error.message : String(error)
5491
- };
5624
+ const snapshot = await this.readProcessSnapshot();
5492
5625
  this.cachedSnapshot = snapshot;
5493
5626
  return snapshot;
5494
5627
  } finally {
@@ -5497,6 +5630,25 @@ var StudioInstanceManager = class {
5497
5630
  })();
5498
5631
  return this.snapshotInFlight;
5499
5632
  }
5633
+ async readProcessSnapshot(timeoutMs = 15e3) {
5634
+ try {
5635
+ if (this.processAdapter.observeStudioProcesses) {
5636
+ return await this.processAdapter.observeStudioProcesses();
5637
+ }
5638
+ if (this.processAdapter.listStudioProcesses) {
5639
+ const observedAt = Date.now();
5640
+ const processes = await this.processAdapter.listStudioProcesses();
5641
+ return { status: "ok", observedAt, processes };
5642
+ }
5643
+ return await observeStudioProcesses(Math.max(1, timeoutMs));
5644
+ } catch (error) {
5645
+ return {
5646
+ status: "error",
5647
+ observedAt: Date.now(),
5648
+ error: error instanceof Error ? error.message : String(error)
5649
+ };
5650
+ }
5651
+ }
5500
5652
  async getCurrentBootId() {
5501
5653
  return this.processAdapter.currentBootId ? await this.processAdapter.currentBootId() : currentBootIdAsync();
5502
5654
  }
@@ -5562,8 +5714,16 @@ var StudioInstanceManager = class {
5562
5714
  if (!processId)
5563
5715
  return { status: "running", observedAt: snapshot.observedAt };
5564
5716
  const studioProcess = snapshot.processes.find((candidate) => candidate.Id === processId);
5565
- if (!studioProcess)
5717
+ if (!studioProcess) {
5718
+ if (process.platform === "darwin" && !this.processAdapter.observeStudioProcesses && !this.processAdapter.listStudioProcesses) {
5719
+ const observation = observePosixProcess(processId);
5720
+ return observation.status === "running" ? { status: "unknown", observedAt: observation.observedAt, error: `Studio PID ${processId} is still alive but missing from process enumeration.` } : observation;
5721
+ }
5566
5722
  return { status: "not_running", observedAt: snapshot.observedAt, reason: "missing" };
5723
+ }
5724
+ if (record.nativeProcessStartedAt !== void 0 && studioProcess.StartTimeUtcFileTime === void 0) {
5725
+ return { status: "unknown", observedAt: snapshot.observedAt, error: "Process creation-time identity is unavailable." };
5726
+ }
5567
5727
  return this.verifyProcessForRecord(record, studioProcess) ? { status: "running", observedAt: snapshot.observedAt } : { status: "not_running", observedAt: snapshot.observedAt, reason: "identity_mismatch" };
5568
5728
  }
5569
5729
  verifyProcessForRecord(record, studioProcess) {
@@ -6488,7 +6648,7 @@ var TOOL_PROXY_ENDPOINTS = {
6488
6648
  insert_asset: ["/api/insert-asset"],
6489
6649
  generate_model: ["/api/generate-model"],
6490
6650
  preview_asset: ["/api/preview-asset"],
6491
- capture_screenshot: ["/api/capture-studio", "/api/capture-screenshot", "/api/capture-begin", "/api/capture-read"],
6651
+ capture_screenshot: ["/api/capture-studio", "/api/capture-screenshot", "/api/capture-begin", "/api/capture-read", "/api/capture-markers"],
6492
6652
  simulate_mouse_input: ["/api/simulate-mouse-input"],
6493
6653
  simulate_keyboard_input: ["/api/simulate-keyboard-input"],
6494
6654
  get_memory_breakdown: ["/api/get-memory-breakdown"],
@@ -6830,8 +6990,8 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
6830
6990
  });
6831
6991
  const authToken = security?.authToken;
6832
6992
  const authRequired = (requestPath) => {
6833
- const path6 = requestPath.toLowerCase().replace(/\/+$/, "");
6834
- return path6 === "/mcp" || path6.startsWith("/mcp/") || path6 === "/proxy" || path6 === "/topology" || path6 === "/request-status" || path6 === "/unregister-instance-id" || path6 === "/create-multiplayer-group" || path6 === "/remove-multiplayer-group";
6993
+ const path7 = requestPath.toLowerCase().replace(/\/+$/, "");
6994
+ return path7 === "/mcp" || path7.startsWith("/mcp/") || path7 === "/proxy" || path7 === "/topology" || path7 === "/request-status" || path7 === "/unregister-instance-id" || path7 === "/create-multiplayer-group" || path7 === "/remove-multiplayer-group";
6835
6995
  };
6836
6996
  app.use((req, res, next) => {
6837
6997
  if (!authToken || !authRequired(req.path)) {
@@ -7824,12 +7984,10 @@ var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
7824
7984
  var MAX_EDITABLE_IMAGE_DIMENSION = 1024;
7825
7985
  var MAX_PNG_DIMENSION = 16 * 1024;
7826
7986
  var MAX_PNG_PIXELS = 8 * 1024 * 1024;
7987
+ var MAX_SCREENSHOT_PNG_PIXELS = 32 * 1024 * 1024;
7827
7988
  var MAX_PNG_INPUT_BYTES = 32 * 1024 * 1024;
7828
7989
  var MAX_PNG_BASE64_CHARACTERS = 4 * Math.ceil(MAX_PNG_INPUT_BYTES / 3);
7829
- var MAX_PNG_CHUNK_BYTES = 32 * 1024 * 1024;
7830
- var MAX_PNG_COMPRESSED_BYTES = 32 * 1024 * 1024;
7831
7990
  var MAX_PNG_CHUNKS = 65536;
7832
- var MAX_PNG_INFLATED_BYTES = MAX_PNG_PIXELS * 4 + MAX_PNG_DIMENSION;
7833
7991
  function paethPredictor(a, b, c) {
7834
7992
  const p = a + b - c;
7835
7993
  const pa = Math.abs(p - a);
@@ -7852,12 +8010,12 @@ function bytesPerPixel(colorType) {
7852
8010
  return 2;
7853
8011
  throw new Error(`Unsupported PNG color type ${colorType}. Supported color types: 0, 2, 4, 6.`);
7854
8012
  }
7855
- function validatePngInputSize(byteLength) {
8013
+ function validatePngInputSize(byteLength, maxInputBytes = MAX_PNG_INPUT_BYTES) {
7856
8014
  if (!Number.isSafeInteger(byteLength) || byteLength < 0) {
7857
8015
  throw new Error(`Invalid PNG input length ${byteLength}.`);
7858
8016
  }
7859
- if (byteLength > MAX_PNG_INPUT_BYTES) {
7860
- throw new Error(`PNG input length ${byteLength} exceeds the ${MAX_PNG_INPUT_BYTES}-byte limit.`);
8017
+ if (byteLength > maxInputBytes) {
8018
+ throw new Error(`PNG input length ${byteLength} exceeds the ${maxInputBytes}-byte limit.`);
7861
8019
  }
7862
8020
  }
7863
8021
  function getBase64DecodedLength(encoded) {
@@ -7919,7 +8077,7 @@ function readPngFileWithinLimit(resolved, imagePath) {
7919
8077
  fs2.closeSync(descriptor);
7920
8078
  }
7921
8079
  }
7922
- function validateSourceDimensions(width, height) {
8080
+ function validateSourceDimensions(width, height, maxPixels) {
7923
8081
  if (width <= 0 || height <= 0) {
7924
8082
  throw new Error("PNG IHDR dimensions must be positive.");
7925
8083
  }
@@ -7927,78 +8085,65 @@ function validateSourceDimensions(width, height) {
7927
8085
  throw new Error(`PNG dimensions ${width}x${height} exceed the ${MAX_PNG_DIMENSION}-pixel dimension limit.`);
7928
8086
  }
7929
8087
  const pixelCount = width * height;
7930
- if (!Number.isSafeInteger(pixelCount) || pixelCount > MAX_PNG_PIXELS) {
7931
- throw new Error(`PNG pixel count ${pixelCount} exceeds the ${MAX_PNG_PIXELS}-pixel limit.`);
8088
+ if (!Number.isSafeInteger(pixelCount) || pixelCount > maxPixels) {
8089
+ throw new Error(`PNG pixel count ${pixelCount} exceeds the ${maxPixels}-pixel limit.`);
7932
8090
  }
7933
8091
  }
7934
- function getExpectedScanlineLength(width, height, bpp) {
8092
+ function getExpectedScanlineLength(width, height, bpp, maxPixels) {
8093
+ const maxInflatedBytes = maxPixels * 4 + MAX_PNG_DIMENSION;
7935
8094
  const stride = width * bpp;
7936
8095
  const expected = height * (stride + 1);
7937
- if (!Number.isSafeInteger(stride) || !Number.isSafeInteger(expected) || expected > MAX_PNG_INFLATED_BYTES) {
7938
- throw new Error(`PNG scanline data length ${expected} exceeds the ${MAX_PNG_INFLATED_BYTES}-byte limit.`);
8096
+ if (!Number.isSafeInteger(stride) || !Number.isSafeInteger(expected) || expected > maxInflatedBytes) {
8097
+ throw new Error(`PNG scanline data length ${expected} exceeds the ${maxInflatedBytes}-byte limit.`);
7939
8098
  }
7940
8099
  return expected;
7941
8100
  }
7942
- function convertScanlinesToRgba(raw, width, height, colorType, bpp, expectedLength) {
7943
- const stride = width * bpp;
7944
- if (raw.length !== expectedLength) {
7945
- if (raw.length > expectedLength) {
7946
- throw new Error(`Inflated PNG data exceeds the expected scanline length of ${expectedLength} bytes.`);
7947
- }
7948
- throw new Error(`PNG data ended early. Expected ${expectedLength} bytes after inflate, got ${raw.length}.`);
7949
- }
7950
- const unfiltered = Buffer.alloc(width * height * bpp);
7951
- let srcOffset = 0;
7952
- for (let y = 0; y < height; y++) {
7953
- const filter = raw[srcOffset++];
7954
- const rowOffset = y * stride;
7955
- const prevRowOffset = (y - 1) * stride;
7956
- for (let x = 0; x < stride; x++) {
7957
- const value = raw[srcOffset++];
7958
- const left = x >= bpp ? unfiltered[rowOffset + x - bpp] : 0;
7959
- const up = y > 0 ? unfiltered[prevRowOffset + x] : 0;
7960
- const upLeft = y > 0 && x >= bpp ? unfiltered[prevRowOffset + x - bpp] : 0;
7961
- let decoded;
7962
- if (filter === 0) {
7963
- decoded = value;
7964
- } else if (filter === 1) {
7965
- decoded = value + left;
7966
- } else if (filter === 2) {
7967
- decoded = value + up;
7968
- } else if (filter === 3) {
7969
- decoded = value + Math.floor((left + up) / 2);
7970
- } else if (filter === 4) {
7971
- decoded = value + paethPredictor(left, up, upLeft);
7972
- } else {
7973
- throw new Error(`Unsupported PNG filter type ${filter}.`);
7974
- }
7975
- unfiltered[rowOffset + x] = decoded & 255;
8101
+ function unfilterPngRow(raw, rowOffset, stride, bpp) {
8102
+ const filter = raw[rowOffset - 1];
8103
+ if (filter === 0)
8104
+ return;
8105
+ const prevRowOffset = rowOffset - stride - 1;
8106
+ for (let x = 0; x < stride; x++) {
8107
+ const left = x >= bpp ? raw[rowOffset + x - bpp] : 0;
8108
+ const up = prevRowOffset > 0 ? raw[prevRowOffset + x] : 0;
8109
+ const upLeft = prevRowOffset > 0 && x >= bpp ? raw[prevRowOffset + x - bpp] : 0;
8110
+ let predictor;
8111
+ if (filter === 1) {
8112
+ predictor = left;
8113
+ } else if (filter === 2) {
8114
+ predictor = up;
8115
+ } else if (filter === 3) {
8116
+ predictor = Math.floor((left + up) / 2);
8117
+ } else {
8118
+ predictor = paethPredictor(left, up, upLeft);
7976
8119
  }
8120
+ raw[rowOffset + x] = raw[rowOffset + x] + predictor & 255;
7977
8121
  }
7978
- if (colorType === 6)
7979
- return unfiltered;
8122
+ }
8123
+ function convertScanlinesToRgba({ raw, width, height, colorType, bpp }) {
8124
+ const stride = width * bpp;
7980
8125
  const rgba = Buffer.alloc(width * height * 4);
7981
- let si = 0;
7982
8126
  let di = 0;
7983
- for (let i = 0; i < width * height; i++) {
7984
- if (colorType === 2) {
7985
- rgba[di++] = unfiltered[si++];
7986
- rgba[di++] = unfiltered[si++];
7987
- rgba[di++] = unfiltered[si++];
7988
- rgba[di++] = 255;
7989
- } else if (colorType === 0) {
7990
- const gray = unfiltered[si++];
7991
- rgba[di++] = gray;
7992
- rgba[di++] = gray;
7993
- rgba[di++] = gray;
7994
- rgba[di++] = 255;
7995
- } else {
7996
- const gray = unfiltered[si++];
7997
- const alpha = unfiltered[si++];
7998
- rgba[di++] = gray;
7999
- rgba[di++] = gray;
8000
- rgba[di++] = gray;
8001
- rgba[di++] = alpha;
8127
+ for (let rowOffset = 1; rowOffset < raw.length; rowOffset += stride + 1) {
8128
+ unfilterPngRow(raw, rowOffset, stride, bpp);
8129
+ if (colorType === 6) {
8130
+ raw.copy(rgba, di, rowOffset, rowOffset + stride);
8131
+ di += stride;
8132
+ continue;
8133
+ }
8134
+ for (let si = rowOffset; si < rowOffset + stride; si += bpp) {
8135
+ if (colorType === 2) {
8136
+ rgba[di++] = raw[si];
8137
+ rgba[di++] = raw[si + 1];
8138
+ rgba[di++] = raw[si + 2];
8139
+ rgba[di++] = 255;
8140
+ } else {
8141
+ const gray = raw[si];
8142
+ rgba[di++] = gray;
8143
+ rgba[di++] = gray;
8144
+ rgba[di++] = gray;
8145
+ rgba[di++] = colorType === 4 ? raw[si + 1] : 255;
8146
+ }
8002
8147
  }
8003
8148
  }
8004
8149
  return rgba;
@@ -8025,8 +8170,9 @@ function resizeRgbaNearest(image) {
8025
8170
  }
8026
8171
  return { width, height, rgba };
8027
8172
  }
8028
- function decodePngToRgba(data) {
8029
- validatePngInputSize(data.length);
8173
+ function inflatePng(data, maxPixels) {
8174
+ const maxInputBytes = maxPixels * 4;
8175
+ validatePngInputSize(data.length, maxInputBytes);
8030
8176
  if (data.length < PNG_SIGNATURE.length || !data.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) {
8031
8177
  throw new Error("Unsupported image format. generate_model currently supports PNG images.");
8032
8178
  }
@@ -8047,8 +8193,8 @@ function decodePngToRgba(data) {
8047
8193
  }
8048
8194
  const length = data.readUInt32BE(offset);
8049
8195
  const type = data.toString("ascii", offset + 4, offset + 8);
8050
- if (length > MAX_PNG_CHUNK_BYTES) {
8051
- throw new Error(`PNG chunk ${type} length ${length} exceeds the ${MAX_PNG_CHUNK_BYTES}-byte chunk limit.`);
8196
+ if (length > maxInputBytes) {
8197
+ throw new Error(`PNG chunk ${type} length ${length} exceeds the ${maxInputBytes}-byte chunk limit.`);
8052
8198
  }
8053
8199
  if (++chunkCount > MAX_PNG_CHUNKS) {
8054
8200
  throw new Error(`PNG contains more than the ${MAX_PNG_CHUNKS}-chunk limit.`);
@@ -8084,12 +8230,12 @@ function decodePngToRgba(data) {
8084
8230
  if (interlace !== 0)
8085
8231
  throw new Error("Interlaced PNG images are not supported.");
8086
8232
  bpp = bytesPerPixel(colorType);
8087
- validateSourceDimensions(width, height);
8088
- expectedScanlineLength = getExpectedScanlineLength(width, height, bpp);
8233
+ validateSourceDimensions(width, height, maxPixels);
8234
+ expectedScanlineLength = getExpectedScanlineLength(width, height, bpp, maxPixels);
8089
8235
  sawIhdr = true;
8090
8236
  } else if (type === "IDAT") {
8091
- if (length > MAX_PNG_COMPRESSED_BYTES - idatLength) {
8092
- throw new Error(`PNG image data exceeds the ${MAX_PNG_COMPRESSED_BYTES}-byte compressed-data limit.`);
8237
+ if (length > maxInputBytes - idatLength) {
8238
+ throw new Error(`PNG image data exceeds the ${maxInputBytes}-byte compressed-data limit.`);
8093
8239
  }
8094
8240
  idatLength += length;
8095
8241
  idatChunks.push(chunk);
@@ -8117,8 +8263,43 @@ function decodePngToRgba(data) {
8117
8263
  const detail = error instanceof Error ? error.message : String(error);
8118
8264
  throw new Error(`Invalid PNG compressed data: ${detail}`);
8119
8265
  }
8120
- const rgba = convertScanlinesToRgba(raw, width, height, colorType, bpp, expectedScanlineLength);
8121
- return resizeRgbaNearest({ width, height, rgba });
8266
+ if (raw.length !== expectedScanlineLength) {
8267
+ throw new Error(`PNG data ended early. Expected ${expectedScanlineLength} bytes after inflate, got ${raw.length}.`);
8268
+ }
8269
+ const rowLength = width * bpp + 1;
8270
+ for (let rowOffset = 0; rowOffset < raw.length; rowOffset += rowLength) {
8271
+ if (raw[rowOffset] > 4) {
8272
+ throw new Error(`Unsupported PNG filter type ${raw[rowOffset]}.`);
8273
+ }
8274
+ }
8275
+ return { width, height, colorType, bpp, raw };
8276
+ }
8277
+ function isUniformPng(data) {
8278
+ const { raw, width, colorType, bpp } = inflatePng(data, MAX_SCREENSHOT_PNG_PIXELS);
8279
+ const stride = width * bpp;
8280
+ unfilterPngRow(raw, 1, stride, bpp);
8281
+ const red = raw[1];
8282
+ const grayscale = colorType === 0 || colorType === 4;
8283
+ const green = grayscale ? red : raw[2];
8284
+ const blue = grayscale ? red : raw[3];
8285
+ for (let rowOffset = 1; rowOffset < raw.length; rowOffset += stride + 1) {
8286
+ if (rowOffset !== 1)
8287
+ unfilterPngRow(raw, rowOffset, stride, bpp);
8288
+ for (let offset = rowOffset; offset < rowOffset + stride; offset += bpp) {
8289
+ if (raw[offset] !== red || !grayscale && (raw[offset + 1] !== green || raw[offset + 2] !== blue)) {
8290
+ return false;
8291
+ }
8292
+ }
8293
+ }
8294
+ return true;
8295
+ }
8296
+ function decodePngToRgba(data) {
8297
+ const png = inflatePng(data, MAX_PNG_PIXELS);
8298
+ return resizeRgbaNearest({
8299
+ width: png.width,
8300
+ height: png.height,
8301
+ rgba: convertScanlinesToRgba(png)
8302
+ });
8122
8303
  }
8123
8304
  function decodePngBase64ToRgba(encoded) {
8124
8305
  const expectedLength = getBase64DecodedLength(encoded);
@@ -10102,9 +10283,308 @@ function rgbaToPng(rgba, width, height) {
10102
10283
  ]);
10103
10284
  }
10104
10285
 
10105
- // ../core/dist/tools/index.js
10286
+ // ../core/dist/host-capture.js
10287
+ import { spawn as spawn2 } from "child_process";
10106
10288
  import * as fs3 from "fs";
10289
+ import * as os3 from "os";
10107
10290
  import * as path5 from "path";
10291
+ var HOST_CAPTURE_TIMEOUT_MS = 2e4;
10292
+ var MARKER_MIN_RB = 200;
10293
+ var MARKER_MAX_G = 60;
10294
+ var MARKER_SCALE_TOLERANCE = 0.03;
10295
+ function isHostCaptureDisabled(env = process.env) {
10296
+ const value = (env.ROBLOX_STUDIO_HOST_CAPTURE ?? "").trim().toLowerCase();
10297
+ return value === "0" || value === "false" || value === "off";
10298
+ }
10299
+ function isHostCaptureSupported(platform = process.platform) {
10300
+ return platform === "win32";
10301
+ }
10302
+ function hostCaptureUnsupportedReason(platform = process.platform) {
10303
+ return `host window capture is only implemented on Windows (this is ${platform})`;
10304
+ }
10305
+ function isUniformFrame(rgba, width, height) {
10306
+ const pixels = width * height;
10307
+ if (pixels <= 0 || rgba.length < pixels * 4)
10308
+ return false;
10309
+ const r = rgba[0];
10310
+ const g = rgba[1];
10311
+ const b = rgba[2];
10312
+ for (let i = 0; i < pixels; i++) {
10313
+ const o = i * 4;
10314
+ if (rgba[o] !== r || rgba[o + 1] !== g || rgba[o + 2] !== b)
10315
+ return false;
10316
+ }
10317
+ return true;
10318
+ }
10319
+ function isMarkerPixel(rgba, offset) {
10320
+ return rgba[offset] >= MARKER_MIN_RB && rgba[offset + 1] <= MARKER_MAX_G && rgba[offset + 2] >= MARKER_MIN_RB;
10321
+ }
10322
+ function findViewportRect(rgba, width, height, hint) {
10323
+ let minX = width;
10324
+ let minY = height;
10325
+ let maxX = -1;
10326
+ let maxY = -1;
10327
+ for (let y = 0; y < height; y++) {
10328
+ const row = y * width * 4;
10329
+ for (let x = 0; x < width; x++) {
10330
+ if (!isMarkerPixel(rgba, row + x * 4))
10331
+ continue;
10332
+ if (x < minX)
10333
+ minX = x;
10334
+ if (x > maxX)
10335
+ maxX = x;
10336
+ if (y < minY)
10337
+ minY = y;
10338
+ if (y > maxY)
10339
+ maxY = y;
10340
+ }
10341
+ }
10342
+ if (maxX < 0) {
10343
+ return { error: "no viewport markers were visible in the Studio window capture" };
10344
+ }
10345
+ const rect = { x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1 };
10346
+ const inset = Math.max(1, Math.floor(hint.markerSize / 4));
10347
+ const corners = [
10348
+ [minX + inset, minY + inset],
10349
+ [maxX - inset, minY + inset],
10350
+ [minX + inset, maxY - inset],
10351
+ [maxX - inset, maxY - inset]
10352
+ ];
10353
+ for (const [x, y] of corners) {
10354
+ if (!isMarkerPixel(rgba, (y * width + x) * 4)) {
10355
+ return { error: `viewport markers were found but do not form a rectangle (box ${rect.width}x${rect.height} at ${rect.x},${rect.y})` };
10356
+ }
10357
+ }
10358
+ const scaleX = rect.width / hint.viewportWidth;
10359
+ const scaleY = rect.height / hint.viewportHeight;
10360
+ if (Math.abs(scaleX - scaleY) > MARKER_SCALE_TOLERANCE * Math.max(scaleX, scaleY)) {
10361
+ return {
10362
+ error: `viewport marker box ${rect.width}x${rect.height} does not match the reported ${hint.viewportWidth}x${hint.viewportHeight} viewport aspect`
10363
+ };
10364
+ }
10365
+ return { rect };
10366
+ }
10367
+ function cropToViewport(rgba, width, height, rect, targetWidth, targetHeight) {
10368
+ const x0 = Math.max(0, Math.min(width - 1, rect.x));
10369
+ const y0 = Math.max(0, Math.min(height - 1, rect.y));
10370
+ const srcW = Math.max(1, Math.min(rect.width, width - x0));
10371
+ const srcH = Math.max(1, Math.min(rect.height, height - y0));
10372
+ const exact = srcW === targetWidth && srcH === targetHeight;
10373
+ const outW = targetWidth;
10374
+ const outH = targetHeight;
10375
+ const out = Buffer.alloc(outW * outH * 4);
10376
+ if (exact) {
10377
+ for (let y = 0; y < outH; y++) {
10378
+ rgba.copy(out, y * outW * 4, ((y0 + y) * width + x0) * 4, ((y0 + y) * width + x0 + outW) * 4);
10379
+ }
10380
+ return { width: outW, height: outH, rgba: out };
10381
+ }
10382
+ const sx = srcW / outW;
10383
+ const sy = srcH / outH;
10384
+ for (let y = 0; y < outH; y++) {
10385
+ const fy = Math.min(srcH - 1, (y + 0.5) * sy - 0.5);
10386
+ const iy = Math.max(0, Math.floor(fy));
10387
+ const ny = Math.min(srcH - 1, iy + 1);
10388
+ const wy = Math.max(0, fy - iy);
10389
+ for (let x = 0; x < outW; x++) {
10390
+ const fx = Math.min(srcW - 1, (x + 0.5) * sx - 0.5);
10391
+ const ix = Math.max(0, Math.floor(fx));
10392
+ const nx = Math.min(srcW - 1, ix + 1);
10393
+ const wx = Math.max(0, fx - ix);
10394
+ const o00 = ((y0 + iy) * width + x0 + ix) * 4;
10395
+ const o10 = ((y0 + iy) * width + x0 + nx) * 4;
10396
+ const o01 = ((y0 + ny) * width + x0 + ix) * 4;
10397
+ const o11 = ((y0 + ny) * width + x0 + nx) * 4;
10398
+ const dst = (y * outW + x) * 4;
10399
+ for (let c = 0; c < 4; c++) {
10400
+ const top = rgba[o00 + c] * (1 - wx) + rgba[o10 + c] * wx;
10401
+ const bottom = rgba[o01 + c] * (1 - wx) + rgba[o11 + c] * wx;
10402
+ out[dst + c] = Math.round(top * (1 - wy) + bottom * wy);
10403
+ }
10404
+ }
10405
+ }
10406
+ return { width: outW, height: outH, rgba: out };
10407
+ }
10408
+ var WINDOWS_CAPTURE_SCRIPT = String.raw`
10409
+ $ErrorActionPreference = 'Stop'
10410
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
10411
+ Add-Type -AssemblyName System.Drawing
10412
+ Add-Type -TypeDefinition @"
10413
+ using System;
10414
+ using System.Collections.Generic;
10415
+ using System.Runtime.InteropServices;
10416
+ using System.Text;
10417
+ public static class McpStudioCapture {
10418
+ [DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc cb, IntPtr lParam);
10419
+ [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr hWnd);
10420
+ [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);
10421
+ [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
10422
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowTextW(IntPtr hWnd, StringBuilder text, int count);
10423
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowTextLengthW(IntPtr hWnd);
10424
+ [DllImport("user32.dll")] public static extern bool GetClientRect(IntPtr hWnd, out RECT rect);
10425
+ [DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr hWnd, IntPtr hdc, uint flags);
10426
+ delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
10427
+ [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
10428
+ public class Candidate { public IntPtr Handle; public uint Pid; public string Title; }
10429
+ public static List<Candidate> Find(string processName) {
10430
+ var found = new List<Candidate>();
10431
+ EnumWindows((hWnd, lParam) => {
10432
+ if (!IsWindowVisible(hWnd)) return true;
10433
+ int length = GetWindowTextLengthW(hWnd);
10434
+ if (length <= 0) return true;
10435
+ var sb = new StringBuilder(length + 1);
10436
+ GetWindowTextW(hWnd, sb, sb.Capacity);
10437
+ string title = sb.ToString();
10438
+ if (!title.EndsWith("Roblox Studio", StringComparison.Ordinal)) return true;
10439
+ uint pid;
10440
+ GetWindowThreadProcessId(hWnd, out pid);
10441
+ try {
10442
+ var proc = System.Diagnostics.Process.GetProcessById((int)pid);
10443
+ if (!string.Equals(proc.ProcessName, processName, StringComparison.OrdinalIgnoreCase)) return true;
10444
+ } catch { return true; }
10445
+ found.Add(new Candidate { Handle = hWnd, Pid = pid, Title = title });
10446
+ return true;
10447
+ }, IntPtr.Zero);
10448
+ return found;
10449
+ }
10450
+ }
10451
+ "@
10452
+ function Emit($obj) { Write-Output ($obj | ConvertTo-Json -Compress) }
10453
+ $hint = $env:MCP_CAPTURE_TITLE_HINT
10454
+ $outFile = $env:MCP_CAPTURE_OUT
10455
+ $candidates = [McpStudioCapture]::Find('RobloxStudioBeta')
10456
+ if ($candidates.Count -eq 0) { Emit @{ ok = $false; error = 'no visible Roblox Studio window was found' }; exit 0 }
10457
+ $pick = $null
10458
+ if ($hint) { $pick = $candidates | Where-Object { $_.Title.StartsWith($hint) } | Select-Object -First 1 }
10459
+ if ($pick -eq $null -and $candidates.Count -eq 1) { $pick = $candidates[0] }
10460
+ if ($pick -eq $null) {
10461
+ $titles = ($candidates | ForEach-Object { $_.Title }) -join ' | '
10462
+ Emit @{ ok = $false; error = "could not pick a Studio window for '$hint' among: $titles" }; exit 0
10463
+ }
10464
+ if ([McpStudioCapture]::IsIconic($pick.Handle)) {
10465
+ Emit @{ ok = $false; error = "the Studio window '$($pick.Title)' is minimized; restore it (it may stay behind other windows)" }; exit 0
10466
+ }
10467
+ $rect = New-Object McpStudioCapture+RECT
10468
+ [void][McpStudioCapture]::GetClientRect($pick.Handle, [ref]$rect)
10469
+ $w = $rect.Right - $rect.Left; $h = $rect.Bottom - $rect.Top
10470
+ if ($w -le 0 -or $h -le 0) { Emit @{ ok = $false; error = "the Studio window client area is empty ($w x $h)" }; exit 0 }
10471
+ $bmp = New-Object System.Drawing.Bitmap $w, $h, ([System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
10472
+ $g = [System.Drawing.Graphics]::FromImage($bmp)
10473
+ $hdc = $g.GetHdc()
10474
+ # PW_CLIENTONLY (1) | PW_RENDERFULLCONTENT (2): DWM-composited client area, occluded windows included.
10475
+ $printed = [McpStudioCapture]::PrintWindow($pick.Handle, $hdc, 3)
10476
+ $g.ReleaseHdc($hdc); $g.Dispose()
10477
+ if (-not $printed) { $bmp.Dispose(); Emit @{ ok = $false; error = 'PrintWindow failed for the Studio window' }; exit 0 }
10478
+ $bounds = New-Object System.Drawing.Rectangle 0, 0, $w, $h
10479
+ $data = $bmp.LockBits($bounds, [System.Drawing.Imaging.ImageLockMode]::ReadOnly, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
10480
+ $bytes = New-Object byte[] ($data.Stride * $h)
10481
+ [System.Runtime.InteropServices.Marshal]::Copy($data.Scan0, $bytes, 0, $bytes.Length)
10482
+ $stride = $data.Stride
10483
+ $bmp.UnlockBits($data); $bmp.Dispose()
10484
+ [System.IO.File]::WriteAllBytes($outFile, $bytes)
10485
+ Emit @{ ok = $true; width = $w; height = $h; stride = $stride; title = $pick.Title }
10486
+ `;
10487
+ function powershellPath() {
10488
+ const systemRoot = process.env.SystemRoot ?? process.env.windir;
10489
+ if (systemRoot) {
10490
+ const candidate = path5.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
10491
+ if (fs3.existsSync(candidate))
10492
+ return candidate;
10493
+ }
10494
+ return "powershell.exe";
10495
+ }
10496
+ function runWindowsCapture(titleHint, outFile) {
10497
+ return new Promise((resolve5, reject) => {
10498
+ const encoded = Buffer.from(WINDOWS_CAPTURE_SCRIPT, "utf16le").toString("base64");
10499
+ const child = spawn2(powershellPath(), ["-NoProfile", "-NonInteractive", "-NoLogo", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded], {
10500
+ env: { ...process.env, MCP_CAPTURE_TITLE_HINT: titleHint ?? "", MCP_CAPTURE_OUT: outFile },
10501
+ stdio: ["ignore", "pipe", "pipe"],
10502
+ windowsHide: true
10503
+ });
10504
+ let stdout = "";
10505
+ let stderr = "";
10506
+ let settled = false;
10507
+ const timer = setTimeout(() => {
10508
+ if (settled)
10509
+ return;
10510
+ settled = true;
10511
+ child.kill();
10512
+ reject(new Error(`host window capture timed out after ${HOST_CAPTURE_TIMEOUT_MS}ms`));
10513
+ }, HOST_CAPTURE_TIMEOUT_MS);
10514
+ child.stdout.setEncoding("utf8");
10515
+ child.stderr.setEncoding("utf8");
10516
+ child.stdout.on("data", (chunk) => {
10517
+ stdout += chunk;
10518
+ });
10519
+ child.stderr.on("data", (chunk) => {
10520
+ stderr += chunk;
10521
+ });
10522
+ child.on("error", (error) => {
10523
+ if (settled)
10524
+ return;
10525
+ settled = true;
10526
+ clearTimeout(timer);
10527
+ reject(new Error(`could not start PowerShell for host window capture: ${error.message}`));
10528
+ });
10529
+ child.on("close", (code) => {
10530
+ if (settled)
10531
+ return;
10532
+ settled = true;
10533
+ clearTimeout(timer);
10534
+ const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
10535
+ const last = lines[lines.length - 1];
10536
+ if (last && last.startsWith("{")) {
10537
+ try {
10538
+ resolve5(JSON.parse(last));
10539
+ return;
10540
+ } catch {
10541
+ }
10542
+ }
10543
+ const detail = (stderr.trim() || stdout.trim()).slice(0, 600);
10544
+ reject(new Error(`host window capture helper exited with code ${code}${detail ? `: ${detail}` : ""}`));
10545
+ });
10546
+ });
10547
+ }
10548
+ async function captureStudioWindow(titleHint) {
10549
+ if (isHostCaptureDisabled()) {
10550
+ return { ok: false, error: "host window capture is disabled by ROBLOX_STUDIO_HOST_CAPTURE" };
10551
+ }
10552
+ if (!isHostCaptureSupported()) {
10553
+ return { ok: false, error: hostCaptureUnsupportedReason() };
10554
+ }
10555
+ const outFile = path5.join(os3.tmpdir(), `robloxstudio-mcp-capture-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.bgra`);
10556
+ try {
10557
+ const report = await runWindowsCapture(titleHint, outFile);
10558
+ if (!report.ok || !report.width || !report.height || !report.stride) {
10559
+ return { ok: false, error: report.error ?? "host window capture returned no image" };
10560
+ }
10561
+ const raw = fs3.readFileSync(outFile);
10562
+ const { width, height, stride } = report;
10563
+ const rgba = Buffer.alloc(width * height * 4);
10564
+ for (let y = 0; y < height; y++) {
10565
+ const src = y * stride;
10566
+ const dst = y * width * 4;
10567
+ for (let x = 0; x < width; x++) {
10568
+ const s = src + x * 4;
10569
+ const d = dst + x * 4;
10570
+ rgba[d] = raw[s + 2];
10571
+ rgba[d + 1] = raw[s + 1];
10572
+ rgba[d + 2] = raw[s];
10573
+ rgba[d + 3] = 255;
10574
+ }
10575
+ }
10576
+ return { ok: true, capture: { width, height, rgba, title: report.title ?? "" } };
10577
+ } catch (error) {
10578
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
10579
+ } finally {
10580
+ fs3.rmSync(outFile, { force: true });
10581
+ }
10582
+ }
10583
+
10584
+ // ../core/dist/tools/index.js
10585
+ import * as fs4 from "fs";
10586
+ import * as path6 from "path";
10587
+ var HOST_VIEWPORT_RECT_TTL_MS = 6e4;
10108
10588
  var MAX_INLINE_IMAGE_BYTES = 6e6;
10109
10589
  var MAX_MATRIX_IMAGE_BYTES = 8e6;
10110
10590
  var DEFAULT_ASSET_AUDIO_PREVIEWS = 3;
@@ -10283,9 +10763,9 @@ function compactSoundReference(sound) {
10283
10763
  name: stringField(sound, "name"),
10284
10764
  className: stringField(sound, "className")
10285
10765
  };
10286
- const path6 = stringField(sound, "path");
10287
- if (path6)
10288
- compact.path = path6;
10766
+ const path7 = stringField(sound, "path");
10767
+ if (path7)
10768
+ compact.path = path7;
10289
10769
  const assetId = robloxAssetIdFromContentId(sound.assetId ?? sound.soundId ?? sound.asset);
10290
10770
  if (assetId !== void 0)
10291
10771
  compact.assetId = assetId;
@@ -10348,8 +10828,8 @@ function loadMicroProfilerBaseline(source, sourcePath) {
10348
10828
  if (typeof sourcePath !== "string" || sourcePath === "") {
10349
10829
  throw new Error("baseline_path must be a non-empty string when provided");
10350
10830
  }
10351
- const resolved = path5.resolve(sourcePath);
10352
- const parsed = JSON.parse(fs3.readFileSync(resolved, "utf8"));
10831
+ const resolved = path6.resolve(sourcePath);
10832
+ const parsed = JSON.parse(fs4.readFileSync(resolved, "utf8"));
10353
10833
  const record = asRecord(parsed);
10354
10834
  if (!record)
10355
10835
  throw new Error(`baseline_path did not contain a JSON object: ${resolved}`);
@@ -10851,6 +11331,9 @@ var RobloxStudioTools = class {
10851
11331
  cookieClient;
10852
11332
  instanceManager;
10853
11333
  managedConnectionAssociations = Promise.resolve();
11334
+ hostWindowCapture = captureStudioWindow;
11335
+ hostViewportRects = /* @__PURE__ */ new Map();
11336
+ viewportCaptureQueues = /* @__PURE__ */ new Map();
10854
11337
  constructor(bridge) {
10855
11338
  this.client = new StudioHttpClient(bridge);
10856
11339
  this.bridge = bridge;
@@ -11290,8 +11773,8 @@ var RobloxStudioTools = class {
11290
11773
  timedOut: true
11291
11774
  };
11292
11775
  }
11293
- async getFileTree(path6 = "", instance_id) {
11294
- const response = await this._callSingle("/api/file-tree", { path: path6 }, void 0, instance_id);
11776
+ async getFileTree(path7 = "", instance_id) {
11777
+ const response = await this._callSingle("/api/file-tree", { path: path7 }, void 0, instance_id);
11295
11778
  return {
11296
11779
  content: [
11297
11780
  {
@@ -11383,9 +11866,9 @@ var RobloxStudioTools = class {
11383
11866
  ]
11384
11867
  };
11385
11868
  }
11386
- async getProjectStructure(path6, maxDepth, scriptsOnly, instance_id) {
11869
+ async getProjectStructure(path7, maxDepth, scriptsOnly, instance_id) {
11387
11870
  const response = await this._callSingle("/api/project-structure", {
11388
- path: path6,
11871
+ path: path7,
11389
11872
  maxDepth,
11390
11873
  scriptsOnly
11391
11874
  }, void 0, instance_id);
@@ -11546,7 +12029,7 @@ var RobloxStudioTools = class {
11546
12029
  };
11547
12030
  }
11548
12031
  async setSelection(paths, mode, instance_id) {
11549
- if (!Array.isArray(paths) || paths.some((path6) => typeof path6 !== "string" || path6.length === 0)) {
12032
+ if (!Array.isArray(paths) || paths.some((path7) => typeof path7 !== "string" || path7.length === 0)) {
11550
12033
  throw new Error("selection paths must contain only non-empty instance paths");
11551
12034
  }
11552
12035
  const selectionMode = mode ?? "set";
@@ -12283,9 +12766,9 @@ var RobloxStudioTools = class {
12283
12766
  const rawJson = mutable.raw_json;
12284
12767
  if (typeof rawJson === "string") {
12285
12768
  if (typeof outputPath === "string" && outputPath !== "") {
12286
- const resolvedOutputPath = path5.resolve(outputPath);
12287
- fs3.mkdirSync(path5.dirname(resolvedOutputPath), { recursive: true });
12288
- fs3.writeFileSync(resolvedOutputPath, rawJson, "utf8");
12769
+ const resolvedOutputPath = path6.resolve(outputPath);
12770
+ fs4.mkdirSync(path6.dirname(resolvedOutputPath), { recursive: true });
12771
+ fs4.writeFileSync(resolvedOutputPath, rawJson, "utf8");
12289
12772
  mutable.output_path = resolvedOutputPath;
12290
12773
  }
12291
12774
  delete mutable.raw_json;
@@ -12346,9 +12829,9 @@ var RobloxStudioTools = class {
12346
12829
  const rawSnapshotBase64 = mutable.raw_snapshot_base64;
12347
12830
  if (typeof rawSnapshotBase64 === "string") {
12348
12831
  if (typeof outputPath === "string" && outputPath !== "") {
12349
- const resolvedOutputPath = path5.resolve(outputPath);
12350
- fs3.mkdirSync(path5.dirname(resolvedOutputPath), { recursive: true });
12351
- fs3.writeFileSync(resolvedOutputPath, Buffer.from(rawSnapshotBase64, "base64"));
12832
+ const resolvedOutputPath = path6.resolve(outputPath);
12833
+ fs4.mkdirSync(path6.dirname(resolvedOutputPath), { recursive: true });
12834
+ fs4.writeFileSync(resolvedOutputPath, Buffer.from(rawSnapshotBase64, "base64"));
12352
12835
  mutable.output_path = resolvedOutputPath;
12353
12836
  }
12354
12837
  delete mutable.raw_snapshot_base64;
@@ -12362,9 +12845,9 @@ var RobloxStudioTools = class {
12362
12845
  });
12363
12846
  }
12364
12847
  if (typeof summaryOutputPath === "string" && summaryOutputPath !== "") {
12365
- const resolvedSummaryPath = path5.resolve(summaryOutputPath);
12366
- fs3.mkdirSync(path5.dirname(resolvedSummaryPath), { recursive: true });
12367
- fs3.writeFileSync(resolvedSummaryPath, JSON.stringify(mutable, null, 2), "utf8");
12848
+ const resolvedSummaryPath = path6.resolve(summaryOutputPath);
12849
+ fs4.mkdirSync(path6.dirname(resolvedSummaryPath), { recursive: true });
12850
+ fs4.writeFileSync(resolvedSummaryPath, JSON.stringify(mutable, null, 2), "utf8");
12368
12851
  mutable.summary_output_path = resolvedSummaryPath;
12369
12852
  }
12370
12853
  if (!includeComparisonIndex) {
@@ -12426,7 +12909,7 @@ var RobloxStudioTools = class {
12426
12909
  return record.placeId !== void 0 && instance.placeId === record.placeId;
12427
12910
  }
12428
12911
  if ((record.source === "baseplate" || record.source === "local_file") && record.localPlaceFile) {
12429
- const expectedName = path5.basename(record.localPlaceFile);
12912
+ const expectedName = path6.basename(record.localPlaceFile);
12430
12913
  return instance.placeName === expectedName || instance.dataModelName === expectedName;
12431
12914
  }
12432
12915
  return true;
@@ -12573,6 +13056,7 @@ var RobloxStudioTools = class {
12573
13056
  });
12574
13057
  }
12575
13058
  return this._textResult({
13059
+ test_worker_job_name: parseStudioTestWorkerJobName(process.env.RSMCP_STUDIO_TEST_WORKER_JOB),
12576
13060
  managed: (await this.instanceManager.list()).filter((record2) => record2.closedAt === void 0).map((record2) => this._managedStatus(record2)),
12577
13061
  connected: this.bridge.getPublicInstances().map((instance) => ({
12578
13062
  instance_id: instance.id,
@@ -12651,10 +13135,9 @@ var RobloxStudioTools = class {
12651
13135
  }
12652
13136
  record2 = active[0];
12653
13137
  }
12654
- if (record2.instanceId)
12655
- await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
12656
13138
  const closeResult = await this.instanceManager.close(record2);
12657
13139
  if (record2.instanceId) {
13140
+ await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
12658
13141
  await sleep(500);
12659
13142
  await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
12660
13143
  }
@@ -13757,11 +14240,11 @@ var RobloxStudioTools = class {
13757
14240
  return this.resolveUploadedReferenceImageId(decalId, instance_id);
13758
14241
  }
13759
14242
  async uploadAsset(filePath, assetType, displayName, description, userId, groupId) {
13760
- if (!fs3.existsSync(filePath)) {
14243
+ if (!fs4.existsSync(filePath)) {
13761
14244
  throw new Error(`File not found: ${filePath}`);
13762
14245
  }
13763
- const fileContent = fs3.readFileSync(filePath);
13764
- const fileName = path5.basename(filePath);
14246
+ const fileContent = fs4.readFileSync(filePath);
14247
+ const fileName = path6.basename(filePath);
13765
14248
  const resolvedGroupId = groupId || process.env.ROBLOX_CREATOR_GROUP_ID;
13766
14249
  const resolvedUserId = userId || process.env.ROBLOX_CREATOR_USER_ID;
13767
14250
  if (assetType === "Decal" && this.cookieClient.hasCookie()) {
@@ -13977,10 +14460,10 @@ var RobloxStudioTools = class {
13977
14460
  return { content: [{ type: "text", text: JSON.stringify({ error: "plugin returned no base64 payload" }) }] };
13978
14461
  }
13979
14462
  const bytes = Buffer.from(response.base64, "base64");
13980
- const resolved = path5.resolve(outputPath);
14463
+ const resolved = path6.resolve(outputPath);
13981
14464
  try {
13982
- fs3.mkdirSync(path5.dirname(resolved), { recursive: true });
13983
- fs3.writeFileSync(resolved, bytes);
14465
+ fs4.mkdirSync(path6.dirname(resolved), { recursive: true });
14466
+ fs4.writeFileSync(resolved, bytes);
13984
14467
  } catch (err2) {
13985
14468
  return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err2.message}` }) }] };
13986
14469
  }
@@ -14013,9 +14496,9 @@ var RobloxStudioTools = class {
14013
14496
  let bytes;
14014
14497
  let sourceLabel;
14015
14498
  if (source.path !== void 0) {
14016
- const resolved = path5.resolve(source.path);
14499
+ const resolved = path6.resolve(source.path);
14017
14500
  try {
14018
- bytes = fs3.readFileSync(resolved);
14501
+ bytes = fs4.readFileSync(resolved);
14019
14502
  } catch (err2) {
14020
14503
  return { content: [{ type: "text", text: JSON.stringify({ error: `failed to read ${resolved}: ${err2.message}` }) }] };
14021
14504
  }
@@ -14065,7 +14548,21 @@ var RobloxStudioTools = class {
14065
14548
  }, tgt, instance_id);
14066
14549
  return { content: [{ type: "text", text: JSON.stringify(response) }] };
14067
14550
  }
14551
+ // Even native capture must wait while another call has fitted the simulator
14552
+ // or drawn markers. Otherwise it could observe temporary UI or dimensions.
14068
14553
  async _captureViewportImage(instanceId, targetRole, format, quality, maxBytes = MAX_INLINE_IMAGE_BYTES) {
14554
+ const previous = this.viewportCaptureQueues.get(instanceId) ?? Promise.resolve();
14555
+ const capture = previous.then(() => this._captureViewportImageNow(instanceId, targetRole, format, quality, maxBytes));
14556
+ const settled = capture.then(() => void 0, () => void 0);
14557
+ this.viewportCaptureQueues.set(instanceId, settled);
14558
+ try {
14559
+ return await capture;
14560
+ } finally {
14561
+ if (this.viewportCaptureQueues.get(instanceId) === settled)
14562
+ this.viewportCaptureQueues.delete(instanceId);
14563
+ }
14564
+ }
14565
+ async _captureViewportImageNow(instanceId, targetRole, format, quality, maxBytes = MAX_INLINE_IMAGE_BYTES) {
14069
14566
  const fmt = format === "png" ? "png" : "jpeg";
14070
14567
  const q = quality === void 0 ? 92 : Math.max(1, Math.min(100, Math.floor(quality)));
14071
14568
  let response = await this._callSingle("/api/capture-studio", { encoding: fmt === "png" ? "png" : "rgba8" }, targetRole, instanceId);
@@ -14084,6 +14581,25 @@ var RobloxStudioTools = class {
14084
14581
  response = await this._callSingle("/api/capture-screenshot", {}, "edit", instanceId);
14085
14582
  }
14086
14583
  }
14584
+ let hostNote = "";
14585
+ let hostReason;
14586
+ try {
14587
+ hostReason = response.error ? `Studio's capture failed (${response.error})` : this._isUniformCaptureResponse(response) ? "Studio's CaptureService returned a blank (single-colour) frame" : void 0;
14588
+ } catch (error) {
14589
+ response = { ...response, error: `Could not decode Studio's screenshot: ${error instanceof Error ? error.message : String(error)}` };
14590
+ hostReason = `Studio's capture failed (${response.error})`;
14591
+ }
14592
+ if (hostReason !== void 0) {
14593
+ const host = await this._captureViewportFromHostWindow(instanceId, targetRole);
14594
+ if (host.success) {
14595
+ response = host.response;
14596
+ hostNote = ` Captured from the Studio window through the host OS because ${hostReason}.`;
14597
+ } else if (response.error) {
14598
+ response = { ...response, error: `${response.error} Host window capture also failed: ${host.error}.` };
14599
+ } else {
14600
+ hostNote = ` Warning: ${hostReason} and host window capture also failed (${host.error}), so this image may be blank.`;
14601
+ }
14602
+ }
14087
14603
  if (response.error) {
14088
14604
  let text = response.error;
14089
14605
  if (targetRole.startsWith("client-") && response.error.includes("Failed to load texture, unexpected format") && await this._isMultiplayerTestRunning(instanceId)) {
@@ -14133,7 +14649,7 @@ var RobloxStudioTools = class {
14133
14649
  }
14134
14650
  const nativeW = response.nativeWidth ?? w;
14135
14651
  const nativeH = response.nativeHeight ?? h;
14136
- const message = nativeW !== w || nativeH !== h ? `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}, downscaled from the ${nativeW}x${nativeH} viewport to fit transport limits. For simulate_mouse_input, multiply x read off this image by ${(nativeW / w).toFixed(4)} and y by ${(nativeH / h).toFixed(4)} to get viewport pixel coordinates ((0,0) at the top-left).` : `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}. For simulate_mouse_input, x/y are pixel coordinates in this exact image with (0,0) at the top-left; it is not downscaled, so use coordinates as you read them off the image.`;
14652
+ const message = (nativeW !== w || nativeH !== h ? `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}, downscaled from the ${nativeW}x${nativeH} viewport to fit transport limits. For simulate_mouse_input, multiply x read off this image by ${(nativeW / w).toFixed(4)} and y by ${(nativeH / h).toFixed(4)} to get viewport pixel coordinates ((0,0) at the top-left).` : `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}. For simulate_mouse_input, x/y are pixel coordinates in this exact image with (0,0) at the top-left; it is not downscaled, so use coordinates as you read them off the image.`) + hostNote;
14137
14653
  return {
14138
14654
  success: true,
14139
14655
  width: w,
@@ -14146,6 +14662,149 @@ var RobloxStudioTools = class {
14146
14662
  message
14147
14663
  };
14148
14664
  }
14665
+ _isUniformCaptureResponse(response) {
14666
+ if (!response.data || !response.width || !response.height)
14667
+ return false;
14668
+ const data = Buffer.from(response.data, "base64");
14669
+ if (response.encoding === "png")
14670
+ return isUniformPng(data);
14671
+ return isUniformFrame(data, response.width, response.height);
14672
+ }
14673
+ _hostCaptureTitleHint(instanceId) {
14674
+ const peers = this.bridge.getPeers().filter((peer) => peer.instanceId === instanceId);
14675
+ const edit = peers.find((peer) => peer.role === "edit") ?? peers[0];
14676
+ const name = edit?.dataModelName || edit?.placeName;
14677
+ return name ? name : void 0;
14678
+ }
14679
+ async _callViewportMarkers(action, targetRole, instanceId, captureId) {
14680
+ return await this._callSingle("/api/capture-markers", { action, ...captureId ? { captureId } : {} }, targetRole, instanceId);
14681
+ }
14682
+ // Always restore the simulator, including failures and cached window grabs.
14683
+ async _captureViewportFromHostWindow(instanceId, targetRole) {
14684
+ let prepared;
14685
+ let result;
14686
+ try {
14687
+ prepared = await this._callViewportMarkers("prepare", targetRole, instanceId);
14688
+ if (prepared.error || !prepared.captureId || !prepared.viewportWidth || !prepared.viewportHeight) {
14689
+ result = { success: false, error: prepared.error ?? "the Studio plugin could not prepare host capture (update the plugin)" };
14690
+ } else {
14691
+ if (prepared.viewportChanged)
14692
+ this.hostViewportRects.delete(`${instanceId}|${targetRole}`);
14693
+ result = await this._capturePreparedViewportFromHostWindow(instanceId, targetRole, prepared);
14694
+ }
14695
+ } catch (error) {
14696
+ result = { success: false, error: `host capture failed: ${error instanceof Error ? error.message : String(error)}` };
14697
+ } finally {
14698
+ if (prepared?.captureId) {
14699
+ try {
14700
+ const restored = await this._callViewportMarkers("finish", targetRole, instanceId, prepared.captureId);
14701
+ if (restored.error)
14702
+ result = { success: false, error: `could not restore the Studio viewport: ${restored.error}` };
14703
+ else if (restored.stale || !restored.success)
14704
+ result = { success: false, error: "the host capture transaction expired or lost ownership; retry the capture" };
14705
+ } catch (error) {
14706
+ result = { success: false, error: `could not restore the Studio viewport: ${error instanceof Error ? error.message : String(error)}` };
14707
+ }
14708
+ }
14709
+ }
14710
+ if (!result.success)
14711
+ this.hostViewportRects.delete(`${instanceId}|${targetRole}`);
14712
+ return result;
14713
+ }
14714
+ // Grabs the Studio window through the host OS and crops it to the viewport
14715
+ // of `targetRole`. The plugin pins magenta markers to the viewport corners
14716
+ // so the crop is exact regardless of Studio's dock layout or DPI scale; the
14717
+ // located rect is cached briefly so repeated captures cost one window grab.
14718
+ async _capturePreparedViewportFromHostWindow(instanceId, targetRole, prepared) {
14719
+ const titleHint = this._hostCaptureTitleHint(instanceId);
14720
+ const cacheKey = `${instanceId}|${targetRole}`;
14721
+ const cached = this.hostViewportRects.get(cacheKey);
14722
+ let sizeInfo;
14723
+ try {
14724
+ sizeInfo = await this._callViewportMarkers("query", targetRole, instanceId, prepared.captureId);
14725
+ } catch (error) {
14726
+ return { success: false, error: `viewport markers unavailable: ${error instanceof Error ? error.message : String(error)}` };
14727
+ }
14728
+ if (sizeInfo.error || !sizeInfo.viewportWidth || !sizeInfo.viewportHeight) {
14729
+ return {
14730
+ success: false,
14731
+ error: sizeInfo.error ?? "the Studio plugin did not report a viewport size (update the plugin to a build with /api/capture-markers)"
14732
+ };
14733
+ }
14734
+ const viewportWidth = prepared.viewportWidth ?? sizeInfo.viewportWidth;
14735
+ const viewportHeight = prepared.viewportHeight ?? sizeInfo.viewportHeight;
14736
+ if (cached && Date.now() - cached.cachedAt < HOST_VIEWPORT_RECT_TTL_MS && cached.viewportWidth === viewportWidth && cached.viewportHeight === viewportHeight) {
14737
+ const grab = await this.hostWindowCapture(titleHint);
14738
+ if (!grab.ok)
14739
+ return { success: false, error: grab.error };
14740
+ if (grab.capture.width === cached.windowWidth && grab.capture.height === cached.windowHeight) {
14741
+ return {
14742
+ success: true,
14743
+ response: this._hostResponseFromCrop(grab.capture.rgba, grab.capture.width, grab.capture.height, cached.rect, viewportWidth, viewportHeight)
14744
+ };
14745
+ }
14746
+ this.hostViewportRects.delete(cacheKey);
14747
+ }
14748
+ let shown;
14749
+ try {
14750
+ shown = await this._callViewportMarkers("show", targetRole, instanceId, prepared.captureId);
14751
+ } catch (error) {
14752
+ return { success: false, error: `could not draw viewport markers: ${error instanceof Error ? error.message : String(error)}` };
14753
+ }
14754
+ if (shown.error)
14755
+ return { success: false, error: `could not draw viewport markers: ${shown.error}` };
14756
+ const markerSize = shown.markerSize ?? 12;
14757
+ let markerGrab;
14758
+ let hidden;
14759
+ try {
14760
+ markerGrab = await this.hostWindowCapture(titleHint);
14761
+ } finally {
14762
+ hidden = await this._callViewportMarkers("hide", targetRole, instanceId, prepared.captureId);
14763
+ }
14764
+ if (hidden.error)
14765
+ return { success: false, error: `could not hide viewport markers: ${hidden.error}` };
14766
+ if (!markerGrab.ok)
14767
+ return { success: false, error: markerGrab.error };
14768
+ const located = findViewportRect(markerGrab.capture.rgba, markerGrab.capture.width, markerGrab.capture.height, {
14769
+ viewportWidth: shown.viewportWidth ?? viewportWidth,
14770
+ viewportHeight: shown.viewportHeight ?? viewportHeight,
14771
+ markerSize
14772
+ });
14773
+ if ("error" in located)
14774
+ return { success: false, error: located.error };
14775
+ this.hostViewportRects.set(cacheKey, {
14776
+ rect: located.rect,
14777
+ windowWidth: markerGrab.capture.width,
14778
+ windowHeight: markerGrab.capture.height,
14779
+ viewportWidth,
14780
+ viewportHeight,
14781
+ cachedAt: Date.now()
14782
+ });
14783
+ const cleanGrab = await this.hostWindowCapture(titleHint);
14784
+ if (!cleanGrab.ok)
14785
+ return { success: false, error: cleanGrab.error };
14786
+ if (cleanGrab.capture.width !== markerGrab.capture.width || cleanGrab.capture.height !== markerGrab.capture.height) {
14787
+ this.hostViewportRects.delete(cacheKey);
14788
+ return { success: false, error: "the Studio window was resized while locating the viewport; retry the capture" };
14789
+ }
14790
+ return {
14791
+ success: true,
14792
+ response: this._hostResponseFromCrop(cleanGrab.capture.rgba, cleanGrab.capture.width, cleanGrab.capture.height, located.rect, viewportWidth, viewportHeight)
14793
+ };
14794
+ }
14795
+ _hostResponseFromCrop(rgba, width, height, rect, viewportWidth, viewportHeight) {
14796
+ const cropped = cropToViewport(rgba, width, height, rect, viewportWidth, viewportHeight);
14797
+ return {
14798
+ success: true,
14799
+ encoding: "rgba8",
14800
+ source: "host-window",
14801
+ width: cropped.width,
14802
+ height: cropped.height,
14803
+ nativeWidth: viewportWidth,
14804
+ nativeHeight: viewportHeight,
14805
+ data: cropped.rgba.toString("base64")
14806
+ };
14807
+ }
14149
14808
  async captureScreenshot(instance_id, format, quality) {
14150
14809
  const refresh = this.bridge.refreshTopologyForRouting();
14151
14810
  if (refresh)
@@ -16689,10 +17348,10 @@ var getReadOnlyTools = () => TOOL_DEFINITIONS.filter((t) => t.category === "read
16689
17348
 
16690
17349
  // ../core/dist/install-plugin-helpers.js
16691
17350
  var import_saxes = __toESM(require_saxes(), 1);
16692
- import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync, rmSync as rmSync2, rmdirSync, statSync as statSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
17351
+ import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync7, renameSync, rmSync as rmSync3, rmdirSync, statSync as statSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
16693
17352
  import { execSync } from "child_process";
16694
17353
  import { randomUUID as randomUUID5 } from "crypto";
16695
- import { basename as basename3, join as join5 } from "path";
17354
+ import { basename as basename3, join as join6 } from "path";
16696
17355
  import { homedir as homedir4 } from "os";
16697
17356
  import { isUtf8 } from "buffer";
16698
17357
  var DEFAULT_PLUGIN_PORT = 58741;
@@ -16744,15 +17403,15 @@ function getPluginsFolder() {
16744
17403
  if (process.env.MCP_PLUGINS_DIR)
16745
17404
  return process.env.MCP_PLUGINS_DIR;
16746
17405
  if (process.platform === "win32") {
16747
- return join5(process.env.LOCALAPPDATA || join5(homedir4(), "AppData", "Local"), "Roblox", "Plugins");
17406
+ return join6(process.env.LOCALAPPDATA || join6(homedir4(), "AppData", "Local"), "Roblox", "Plugins");
16748
17407
  }
16749
17408
  if (isWSL()) {
16750
17409
  const localAppData = getWindowsUserLocalAppData();
16751
17410
  if (localAppData)
16752
- return join5(localAppData.linuxPath, "Roblox", "Plugins");
17411
+ return join6(localAppData.linuxPath, "Roblox", "Plugins");
16753
17412
  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.");
16754
17413
  }
16755
- return join5(homedir4(), "Documents", "Roblox", "Plugins");
17414
+ return join6(homedir4(), "Documents", "Roblox", "Plugins");
16756
17415
  }
16757
17416
  var STALE_TEST_PLUGINS_DIRECTORY = "RsmcpIsolatedPlugins";
16758
17417
  function pluginDirectorySettingPattern() {
@@ -16762,12 +17421,12 @@ function escapeXmlText(value) {
16762
17421
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
16763
17422
  }
16764
17423
  function repairStaleStudioPluginDirectorySetting({ settingsPath, studioPluginsDirectory }) {
16765
- if (!existsSync6(settingsPath))
17424
+ if (!existsSync7(settingsPath))
16766
17425
  return false;
16767
17426
  if (studioPluginsDirectory === "") {
16768
17427
  throw new Error("studioPluginsDirectory must be a non-empty path.");
16769
17428
  }
16770
- const contents = readFileSync6(settingsPath, "utf8");
17429
+ const contents = readFileSync7(settingsPath, "utf8");
16771
17430
  const matches = [...contents.matchAll(pluginDirectorySettingPattern())];
16772
17431
  if (matches.length !== 1) {
16773
17432
  throw new Error(`Expected exactly one Studio.PluginsDir QDir in ${settingsPath}; found ${matches.length}.`);
@@ -16784,7 +17443,7 @@ function repairStaleStudioPluginDirectorySetting({ settingsPath, studioPluginsDi
16784
17443
  });
16785
17444
  renameSync(temporaryPath, settingsPath);
16786
17445
  } finally {
16787
- rmSync2(temporaryPath, { force: true });
17446
+ rmSync3(temporaryPath, { force: true });
16788
17447
  }
16789
17448
  return true;
16790
17449
  }
@@ -16795,16 +17454,16 @@ function repairDefaultStudioPluginDirectorySetting(pluginsFolder, log) {
16795
17454
  let defaultPluginsFolder;
16796
17455
  let studioPluginsDirectory;
16797
17456
  if (process.platform === "win32") {
16798
- const localAppData = process.env.LOCALAPPDATA || join5(homedir4(), "AppData", "Local");
16799
- defaultPluginsFolder = join5(localAppData, "Roblox", "Plugins");
16800
- settingsPath = join5(localAppData, "Roblox", "GlobalSettings_13.xml");
17457
+ const localAppData = process.env.LOCALAPPDATA || join6(homedir4(), "AppData", "Local");
17458
+ defaultPluginsFolder = join6(localAppData, "Roblox", "Plugins");
17459
+ settingsPath = join6(localAppData, "Roblox", "GlobalSettings_13.xml");
16801
17460
  studioPluginsDirectory = defaultPluginsFolder.replaceAll("\\", "/");
16802
17461
  } else if (isWSL()) {
16803
17462
  const localAppData = getWindowsUserLocalAppData();
16804
17463
  if (!localAppData)
16805
17464
  return;
16806
- defaultPluginsFolder = join5(localAppData.linuxPath, "Roblox", "Plugins");
16807
- settingsPath = join5(localAppData.linuxPath, "Roblox", "GlobalSettings_13.xml");
17465
+ defaultPluginsFolder = join6(localAppData.linuxPath, "Roblox", "Plugins");
17466
+ settingsPath = join6(localAppData.linuxPath, "Roblox", "GlobalSettings_13.xml");
16808
17467
  studioPluginsDirectory = [
16809
17468
  localAppData.windowsPath.replace(/[\\/]+$/u, ""),
16810
17469
  "Roblox",
@@ -16820,8 +17479,8 @@ function repairDefaultStudioPluginDirectorySetting(pluginsFolder, log) {
16820
17479
  }
16821
17480
  }
16822
17481
  function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
16823
- const otherDest = join5(pluginsFolder, otherAssetName);
16824
- if (!existsSync6(otherDest))
17482
+ const otherDest = join6(pluginsFolder, otherAssetName);
17483
+ if (!existsSync7(otherDest))
16825
17484
  return;
16826
17485
  if (replace) {
16827
17486
  try {
@@ -16846,11 +17505,11 @@ function pluginInstallLockError(lockPath, reason) {
16846
17505
  return new Error(`${reason}: ${lockPath}. If no installer or lock recovery is running, remove that lock directory and retry.`);
16847
17506
  }
16848
17507
  function readPluginInstallLockOwner(lockPath) {
16849
- const ownerPath = join5(lockPath, "owner.json");
17508
+ const ownerPath = join6(lockPath, "owner.json");
16850
17509
  if (!lstatSync(lockPath).isDirectory() || !lstatSync(ownerPath).isFile()) {
16851
17510
  throw new Error("Lock and owner metadata must be a directory and regular file");
16852
17511
  }
16853
- const owner = JSON.parse(readFileSync6(ownerPath, "utf8"));
17512
+ const owner = JSON.parse(readFileSync7(ownerPath, "utf8"));
16854
17513
  if (owner === null || typeof owner !== "object" || !("pid" in owner) || typeof owner.pid !== "number" || !Number.isSafeInteger(owner.pid) || owner.pid <= 0 || owner.pid > 2147483647 || !("token" in owner) || typeof owner.token !== "string" || !/^[a-zA-Z0-9-]{1,128}$/.test(owner.token) || !("createdAt" in owner) || typeof owner.createdAt !== "number" || !Number.isFinite(owner.createdAt) || owner.createdAt < 0) {
16855
17514
  throw new Error("Invalid lock owner metadata");
16856
17515
  }
@@ -16876,7 +17535,7 @@ function recoverPluginInstallLock(lockPath, warn) {
16876
17535
  throw pluginInstallLockError(lockPath, `Cannot safely read plugin installation lock owner (${error})`);
16877
17536
  }
16878
17537
  assertPluginInstallOwnerExited(lockPath, owner);
16879
- const claimPath = join5(lockPath, `recovery-${owner.token}`);
17538
+ const claimPath = join6(lockPath, `recovery-${owner.token}`);
16880
17539
  try {
16881
17540
  mkdirSync4(claimPath);
16882
17541
  } catch (error) {
@@ -16909,14 +17568,14 @@ function recoverPluginInstallLock(lockPath, warn) {
16909
17568
  }
16910
17569
  }
16911
17570
  try {
16912
- rmSync2(abandonedPath, { recursive: true, force: true });
17571
+ rmSync3(abandonedPath, { recursive: true, force: true });
16913
17572
  } catch (error) {
16914
17573
  warn(`[install-plugin] Could not clean abandoned install lock ${abandonedPath}: ${error}`);
16915
17574
  }
16916
17575
  }
16917
17576
  function acquirePluginInstallLock(pluginsFolder, warn) {
16918
- const lockPath = join5(pluginsFolder, PLUGIN_INSTALL_LOCK_NAME);
16919
- const ownerPath = join5(lockPath, "owner.json");
17577
+ const lockPath = join6(pluginsFolder, PLUGIN_INSTALL_LOCK_NAME);
17578
+ const ownerPath = join6(lockPath, "owner.json");
16920
17579
  const owner = {
16921
17580
  pid: process.pid,
16922
17581
  token: randomUUID5(),
@@ -16944,7 +17603,7 @@ function acquirePluginInstallLock(pluginsFolder, warn) {
16944
17603
  });
16945
17604
  } catch (error) {
16946
17605
  try {
16947
- rmSync2(lockPath, { recursive: true, force: true });
17606
+ rmSync3(lockPath, { recursive: true, force: true });
16948
17607
  } catch (cleanupError) {
16949
17608
  warn(`[install-plugin] Could not clean failed install lock ${lockPath}: ${cleanupError}`);
16950
17609
  }
@@ -16954,7 +17613,7 @@ function acquirePluginInstallLock(pluginsFolder, warn) {
16954
17613
  try {
16955
17614
  const currentOwner = readPluginInstallLockOwner(lockPath);
16956
17615
  if (currentOwner.pid === owner.pid && currentOwner.token === owner.token) {
16957
- rmSync2(lockPath, { recursive: true, force: true });
17616
+ rmSync3(lockPath, { recursive: true, force: true });
16958
17617
  }
16959
17618
  } catch (error) {
16960
17619
  if (errorCode(error) !== "ENOENT") {
@@ -17046,11 +17705,11 @@ function assertPluginAssetIdentity(source, assetName, expectedVersion, expectedV
17046
17705
  }
17047
17706
  }
17048
17707
  function installedFileMatches(source, destination) {
17049
- if (!existsSync6(destination))
17708
+ if (!existsSync7(destination))
17050
17709
  return false;
17051
17710
  if (!lstatSync(destination).isFile())
17052
17711
  return false;
17053
- const installed = readFileSync6(destination);
17712
+ const installed = readFileSync7(destination);
17054
17713
  return installed.length === source.length && installed.equals(source);
17055
17714
  }
17056
17715
  function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source, expectedVersion, expectedVariant, rawPort, replaceVariant = true, log = console.log, warn = console.warn }) {
@@ -17062,12 +17721,12 @@ function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source,
17062
17721
  const configured = configurePluginAssetForPort(source, rawPort);
17063
17722
  assertPluginAssetIdentity(configured, assetName, expectedVersion, expectedVariant);
17064
17723
  mkdirSync4(pluginsFolder, { recursive: true });
17065
- const destination = join5(pluginsFolder, assetName);
17724
+ const destination = join6(pluginsFolder, assetName);
17066
17725
  const releaseLock = acquirePluginInstallLock(pluginsFolder, warn);
17067
17726
  try {
17068
17727
  let installed = false;
17069
17728
  if (!installedFileMatches(configured, destination)) {
17070
- const staged = join5(pluginsFolder, `.${assetName}.${process.pid}.${randomUUID5()}.tmp`);
17729
+ const staged = join6(pluginsFolder, `.${assetName}.${process.pid}.${randomUUID5()}.tmp`);
17071
17730
  try {
17072
17731
  writeFileSync3(staged, configured, { flag: "wx" });
17073
17732
  renameSync(staged, destination);
@@ -17100,8 +17759,8 @@ function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source,
17100
17759
  import { createRequire } from "module";
17101
17760
 
17102
17761
  // src/install-plugin.ts
17103
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
17104
- import { dirname as dirname5, join as join6 } from "path";
17762
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
17763
+ import { dirname as dirname5, join as join7 } from "path";
17105
17764
  import { fileURLToPath } from "url";
17106
17765
  import { get } from "https";
17107
17766
  var REPO = "chrrxs/robloxstudio-mcp";
@@ -17161,18 +17820,18 @@ async function fetchJson(url) {
17161
17820
  function bundledAssetPath() {
17162
17821
  const currentDir = dirname5(fileURLToPath(import.meta.url));
17163
17822
  const candidates = [
17164
- join6(currentDir, "..", "studio-plugin", ASSET_NAME),
17165
- join6(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
17823
+ join7(currentDir, "..", "studio-plugin", ASSET_NAME),
17824
+ join7(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
17166
17825
  ];
17167
- return candidates.find((candidate) => existsSync7(candidate)) ?? null;
17826
+ return candidates.find((candidate) => existsSync8(candidate)) ?? null;
17168
17827
  }
17169
17828
  function resolvePluginAssetPath(sourcePath) {
17170
17829
  if (sourcePath === void 0) return bundledAssetPath();
17171
- return existsSync7(sourcePath) ? sourcePath : null;
17830
+ return existsSync8(sourcePath) ? sourcePath : null;
17172
17831
  }
17173
17832
  function packageVersion() {
17174
17833
  const currentDir = dirname5(fileURLToPath(import.meta.url));
17175
- const pkg = JSON.parse(readFileSync7(join6(currentDir, "..", "package.json"), "utf8"));
17834
+ const pkg = JSON.parse(readFileSync8(join7(currentDir, "..", "package.json"), "utf8"));
17176
17835
  if (!pkg.version) {
17177
17836
  throw new Error("Package version not found");
17178
17837
  }
@@ -17192,7 +17851,7 @@ async function installBundledPlugin(options = {}) {
17192
17851
  pluginsFolder: getPluginsFolder(),
17193
17852
  assetName: ASSET_NAME,
17194
17853
  otherAssetName: OTHER_VARIANT,
17195
- source: readFileSync7(sourcePath),
17854
+ source: readFileSync8(sourcePath),
17196
17855
  expectedVersion: packageVersion(),
17197
17856
  expectedVariant: "inspector",
17198
17857
  replaceVariant,
@@ -17216,7 +17875,7 @@ async function installPlugin(options = {}) {
17216
17875
  pluginsFolder: getPluginsFolder(),
17217
17876
  assetName: ASSET_NAME,
17218
17877
  otherAssetName: OTHER_VARIANT,
17219
- source: readFileSync7(bundled),
17878
+ source: readFileSync8(bundled),
17220
17879
  expectedVersion: packageVersion(),
17221
17880
  expectedVariant: "inspector",
17222
17881
  replaceVariant,