@chrrxs/robloxstudio-mcp 3.1.6 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3346,6 +3346,7 @@ Read output with get_runtime_logs. Reuse nextCursor as cursor for one Instance,
3346
3346
  - breakpoints clear removes only MCP-created breakpoints unless clear_all is true. clear_all also removes user-created breakpoints.
3347
3347
  - capture_script_profiler ranks Luau functions by CPU time. Use output_path when the raw capture is needed.
3348
3348
  - capture_micro_profiler attributes frame time across engine and game work. Its rows are inclusive or cumulative views, so do not sum them as disjoint totals.
3349
+ - MicroProfiler records frames only while Studio renders. Studio stops rendering when the display turns off or Studio is minimized, and the capture then fails with micro_profiler_no_frames.
3349
3350
  - Use baseline_path or baseline for before-and-after MicroProfiler comparisons.
3350
3351
  - Use get_memory_breakdown for memory categories and get_scene_analysis for instance, script, triangle, animation, or audio cost.
3351
3352
 
@@ -3357,6 +3358,16 @@ Studio must allow third-party asset loading for public third-party previews and
3357
3358
 
3358
3359
  generate_model stages generated content under ServerStorage for review. upload_asset sends an explicit local file to the chosen Roblox user or group.
3359
3360
 
3361
+ ## Monetization
3362
+
3363
+ manage_monetization lists, reads, creates, and updates developer products and game passes through Roblox Open Cloud. ROBLOX_OPEN_CLOUD_API_KEY needs developer-product:read and developer-product:write, or game-pass:read and game-pass:write, with the experience added to the key. universe_id defaults to the connected Studio place; an unpublished place has no universe.
3364
+
3365
+ - Roblox cannot delete developer products or game passes. To retire one, update it with for_sale=false.
3366
+ - create keeps new items off sale unless for_sale=true, which also needs a price.
3367
+ - create refuses a name that already exists in the universe and returns the existing item. If a create times out, list the universe before retrying.
3368
+ - update changes only the arguments supplied, then reads the item back.
3369
+ - image_path uploads a local PNG, JPEG, or BMP icon. Roblox shows icons at 512x512 inside a circle, so keep details away from the corners.
3370
+
3360
3371
  ## RBXM files
3361
3372
 
3362
3373
  - export_rbxm writes selected instances to an explicit local path. It can read the edit DataModel or a live server DataModel.
@@ -3414,11 +3425,11 @@ function registerResourceHandlers(server) {
3414
3425
  }
3415
3426
 
3416
3427
  // ../core/dist/studio-instance-manager.js
3417
- import { execFile, execFileSync as execFileSync2, spawn } from "child_process";
3418
- import { copyFileSync, existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, realpathSync, rmSync, statSync } from "fs";
3419
- import { randomUUID as randomUUID3 } from "crypto";
3428
+ import { execFile, execFileSync as execFileSync2, spawn as spawn2 } from "child_process";
3429
+ import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync, rmSync as rmSync2, statSync as statSync2 } from "fs";
3430
+ import { randomUUID as randomUUID4 } from "crypto";
3420
3431
  import * as os2 from "os";
3421
- import * as path3 from "path";
3432
+ import * as path5 from "path";
3422
3433
  import { promisify } from "util";
3423
3434
 
3424
3435
  // ../core/dist/studio-sign-in.js
@@ -3664,7 +3675,9 @@ var ManagedInstanceRegistry = class {
3664
3675
  const currentOwner = await this.readLockOwner(lockDir, stat2.isDirectory());
3665
3676
  const ownerIsActive = currentOwner?.pid === process.pid ? activeLockTokens.has(currentOwner.token) : currentOwner ? isProcessAlive(currentOwner.pid) : void 0;
3666
3677
  if (ownerIsActive === false || currentOwner === void 0 && !activeLockPaths.has(lockDir) && Date.now() - stat2.mtimeMs > LOCK_STALE_MS) {
3667
- await fs.rm(lockDir, { recursive: true, force: true });
3678
+ if (await this.isInspectedLock(lockDir, stat2, currentOwner)) {
3679
+ await fs.rm(lockDir, { recursive: true, force: true });
3680
+ }
3668
3681
  continue;
3669
3682
  }
3670
3683
  } catch {
@@ -3701,6 +3714,16 @@ var ManagedInstanceRegistry = class {
3701
3714
  return void 0;
3702
3715
  }
3703
3716
  }
3717
+ // The inspected owner can release, and another holder can take the lock, while this
3718
+ // waiter decides. Reclaim only the instance that was judged abandoned: the same owner
3719
+ // token, or for an unreadable owner the same unchanged directory entry.
3720
+ async isInspectedLock(lockPath, inspected, inspectedOwner) {
3721
+ const current = await fs.stat(lockPath);
3722
+ const currentOwner = await this.readLockOwner(lockPath, current.isDirectory());
3723
+ if (inspectedOwner)
3724
+ return currentOwner?.token === inspectedOwner.token;
3725
+ return currentOwner === void 0 && current.ino === inspected.ino && current.mtimeMs === inspected.mtimeMs;
3726
+ }
3704
3727
  async ensureDir() {
3705
3728
  await fs.mkdir(this.dir, { recursive: true });
3706
3729
  }
@@ -3909,7 +3932,8 @@ var ManagedInstanceRegistry = class {
3909
3932
 
3910
3933
  // ../core/dist/studio-platform.js
3911
3934
  import { execFileSync } from "child_process";
3912
- import { existsSync, readFileSync } from "fs";
3935
+ import { accessSync, constants, existsSync, readFileSync, statSync } from "fs";
3936
+ import * as path3 from "path";
3913
3937
  var WSL_KERNEL_PATTERN = /microsoft|wsl/i;
3914
3938
  var WINDOWS_INTEROP_PROBE_TOKEN = "ROBLOXSTUDIO_MCP_WINDOWS_INTEROP_OK";
3915
3939
  var WINDOWS_POWERSHELL = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe";
@@ -3950,7 +3974,20 @@ function detectStudioPlatform(evidence) {
3950
3974
  }
3951
3975
  };
3952
3976
  }
3953
- const reason = hasWslKernel ? "The Linux kernel has a WSL signature, but the live process cannot execute the Windows launcher." : "The retained Windows Studio launcher is unavailable on native Linux.";
3977
+ if (!hasWslKernel && evidence.wineLauncher && evidence.wineLauncherExecutable === true) {
3978
+ return {
3979
+ hostPlatform: "linux",
3980
+ isWsl: false,
3981
+ windowsInteropAvailable: false,
3982
+ processIdentity: {
3983
+ supported: true,
3984
+ launcher: "wine-retained"
3985
+ },
3986
+ // Wine hosts are always Linux; keep POSIX semantics when tests run elsewhere.
3987
+ wineLauncher: path3.posix.resolve(evidence.wineLauncher)
3988
+ };
3989
+ }
3990
+ const reason = hasWslKernel ? "The Linux kernel has a WSL signature, but the live process cannot execute the Windows launcher." : evidence.wineLauncher ? `The retained Windows Studio launcher is unavailable on native Linux, and ROBLOX_STUDIO_WINE_LAUNCHER (${evidence.wineLauncher}) is not an executable file.` : "The retained Windows Studio launcher is unavailable on native Linux. Set ROBLOX_STUDIO_WINE_LAUNCHER to an executable Wine launcher to run Studio under Wine.";
3954
3991
  return {
3955
3992
  hostPlatform: "linux",
3956
3993
  isWsl: false,
@@ -4000,11 +4037,22 @@ function probeWindowsInterop(kernelVersion) {
4000
4037
  }
4001
4038
  return false;
4002
4039
  }
4040
+ function isExecutableFile(file) {
4041
+ try {
4042
+ if (!statSync(file).isFile())
4043
+ return false;
4044
+ accessSync(file, constants.X_OK);
4045
+ return true;
4046
+ } catch {
4047
+ return false;
4048
+ }
4049
+ }
4003
4050
  var cachedStudioPlatformCapabilities;
4004
4051
  function getStudioPlatformCapabilities() {
4005
4052
  if (cachedStudioPlatformCapabilities)
4006
4053
  return cachedStudioPlatformCapabilities;
4007
4054
  const kernelVersion = readKernelVersion();
4055
+ const wineLauncher = process.env.ROBLOX_STUDIO_WINE_LAUNCHER || void 0;
4008
4056
  cachedStudioPlatformCapabilities = detectStudioPlatform({
4009
4057
  platform: process.platform,
4010
4058
  kernelVersion,
@@ -4012,13 +4060,435 @@ function getStudioPlatformCapabilities() {
4012
4060
  wslDistroName: process.env.WSL_DISTRO_NAME,
4013
4061
  windowsRootPresent: existsSync("/mnt/c/Windows"),
4014
4062
  wslPathPresent: existsSync("/usr/bin/wslpath") || existsSync("/bin/wslpath"),
4015
- windowsInteropAvailable: probeWindowsInterop(kernelVersion)
4063
+ windowsInteropAvailable: probeWindowsInterop(kernelVersion),
4064
+ wineLauncher,
4065
+ wineLauncherExecutable: process.platform === "linux" && wineLauncher !== void 0 && isExecutableFile(wineLauncher)
4016
4066
  });
4017
4067
  return cachedStudioPlatformCapabilities;
4018
4068
  }
4019
4069
 
4070
+ // ../core/dist/studio-wine.js
4071
+ import { spawn } from "child_process";
4072
+ import { randomUUID as randomUUID3 } from "crypto";
4073
+ import { closeSync, fstatSync, linkSync, mkdirSync, openSync, readdirSync, readFileSync as readFileSync2, readSync, rmSync, unlinkSync, writeFileSync } from "fs";
4074
+ import * as path4 from "path";
4075
+ var DEFAULT_PROC_ROOT = "/proc";
4076
+ var STUDIO_EXECUTABLE_NAME = "robloxstudiobeta.exe";
4077
+ var GATE_SCRIPT = 'kill -STOP $$ && exec "$0" "$@"';
4078
+ var GATE_TIMEOUT_MS = 5e3;
4079
+ var STUDIO_EXEC_TIMEOUT_MS = 6e4;
4080
+ var MAX_STUDIO_EXEC_TIMEOUT_MS = 15e4;
4081
+ var STUDIO_EXEC_TIMEOUT_ENV = "ROBLOX_STUDIO_WINE_START_TIMEOUT_MS";
4082
+ var ABORT_TIMEOUT_MS = 1e4;
4083
+ var TERMINATE_GRACE_MS = 5e3;
4084
+ var POLL_INTERVAL_MS = 50;
4085
+ var LAUNCHER_LOGS_KEPT = 10;
4086
+ var LAUNCHER_LOG_TAIL_BYTES = 4096;
4087
+ var LAUNCHER_LOG_TAIL_LINES = 20;
4088
+ var BOOT_ANCHOR_PREFIX = "wine-boot-time-";
4089
+ var FILETIME_UNIX_EPOCH2 = 116444736000000000n;
4090
+ var FILETIME_TICKS_PER_SECOND = 10000000n;
4091
+ var USER_HZ = 100n;
4092
+ var bootTimeByProcRoot = /* @__PURE__ */ new Map();
4093
+ function delay3(ms) {
4094
+ const { promise, resolve: resolve6 } = Promise.withResolvers();
4095
+ setTimeout(resolve6, ms);
4096
+ return promise;
4097
+ }
4098
+ function isMissingProcessError(error) {
4099
+ const code = error?.code;
4100
+ return code === "ENOENT" || code === "ESRCH";
4101
+ }
4102
+ function procStartTimeToFileTime(bootTimeSeconds2, startTimeTicks) {
4103
+ return (FILETIME_UNIX_EPOCH2 + bootTimeSeconds2 * FILETIME_TICKS_PER_SECOND + startTimeTicks * (FILETIME_TICKS_PER_SECOND / USER_HZ)).toString();
4104
+ }
4105
+ function toWineWindowsPath(unixPath) {
4106
+ return `Z:${unixPath.replace(/\//g, "\\")}`;
4107
+ }
4108
+ function isWineStudioCommand(argv0) {
4109
+ return argv0 !== void 0 && argv0.split(/[\\/]/u).pop()?.toLowerCase() === STUDIO_EXECUTABLE_NAME;
4110
+ }
4111
+ function parseProcStat(content) {
4112
+ const commEnd = content.lastIndexOf(")");
4113
+ const fields = commEnd < 0 ? [] : content.slice(commEnd + 1).trim().split(/\s+/u);
4114
+ const state = fields[0];
4115
+ const startTime = fields[19];
4116
+ if (!state || !startTime || !/^\d+$/u.test(startTime)) {
4117
+ throw new Error("Malformed /proc/<pid>/stat.");
4118
+ }
4119
+ return { state, startTimeTicks: BigInt(startTime) };
4120
+ }
4121
+ function readProcStat(procRoot, pid) {
4122
+ let content;
4123
+ try {
4124
+ content = readFileSync2(`${procRoot}/${pid}/stat`, "utf8");
4125
+ } catch (error) {
4126
+ if (isMissingProcessError(error))
4127
+ return void 0;
4128
+ throw error;
4129
+ }
4130
+ return parseProcStat(content);
4131
+ }
4132
+ function readCmdline(procRoot, pid) {
4133
+ let content;
4134
+ try {
4135
+ content = readFileSync2(`${procRoot}/${pid}/cmdline`, "utf8");
4136
+ } catch (error) {
4137
+ if (isMissingProcessError(error))
4138
+ return void 0;
4139
+ throw error;
4140
+ }
4141
+ const args = content.split("\0");
4142
+ if (args[args.length - 1] === "")
4143
+ args.pop();
4144
+ return args;
4145
+ }
4146
+ function readArgv0(procRoot, pid) {
4147
+ return readCmdline(procRoot, pid)?.[0] || void 0;
4148
+ }
4149
+ function readRealUid(procRoot, pid) {
4150
+ let content;
4151
+ try {
4152
+ content = readFileSync2(`${procRoot}/${pid}/status`, "utf8");
4153
+ } catch (error) {
4154
+ if (isMissingProcessError(error))
4155
+ return void 0;
4156
+ throw error;
4157
+ }
4158
+ const match = /^Uid:\s+(\d+)/mu.exec(content);
4159
+ if (!match)
4160
+ throw new Error(`Malformed ${procRoot}/${pid}/status.`);
4161
+ return Number(match[1]);
4162
+ }
4163
+ function readLiveBootTime(procRoot) {
4164
+ const match = /^btime\s+(\d+)\s*$/mu.exec(readFileSync2(`${procRoot}/stat`, "utf8"));
4165
+ if (!match)
4166
+ throw new Error(`${procRoot}/stat has no btime.`);
4167
+ return BigInt(match[1]);
4168
+ }
4169
+ function readBootId(procRoot) {
4170
+ try {
4171
+ const bootId = readFileSync2(`${procRoot}/sys/kernel/random/boot_id`, "utf8").trim();
4172
+ return /^[0-9a-f-]+$/iu.test(bootId) ? bootId : void 0;
4173
+ } catch {
4174
+ return void 0;
4175
+ }
4176
+ }
4177
+ function readBootAnchor(file) {
4178
+ let content;
4179
+ try {
4180
+ content = readFileSync2(file, "utf8");
4181
+ } catch (error) {
4182
+ if (error.code === "ENOENT")
4183
+ return void 0;
4184
+ throw error;
4185
+ }
4186
+ const match = /^(\d+)\n?$/u.exec(content);
4187
+ if (!match)
4188
+ throw new Error(`${file} is not a boot time.`);
4189
+ return BigInt(match[1]);
4190
+ }
4191
+ function removeStaleBootAnchors(anchorDir, currentName) {
4192
+ try {
4193
+ for (const name of readdirSync(anchorDir)) {
4194
+ if (!name.startsWith(BOOT_ANCHOR_PREFIX) || name.startsWith(currentName))
4195
+ continue;
4196
+ rmSync(path4.join(anchorDir, name), { force: true });
4197
+ }
4198
+ } catch {
4199
+ }
4200
+ }
4201
+ function anchoredBootTime(procRoot, liveBootTime) {
4202
+ const bootId = readBootId(procRoot);
4203
+ if (bootId === void 0)
4204
+ return liveBootTime;
4205
+ const anchorDir = defaultManagedInstanceRegistryDir();
4206
+ const anchorName = `${BOOT_ANCHOR_PREFIX}${bootId}`;
4207
+ const anchorFile = path4.join(anchorDir, anchorName);
4208
+ try {
4209
+ const existing = readBootAnchor(anchorFile);
4210
+ if (existing !== void 0)
4211
+ return existing;
4212
+ mkdirSync(anchorDir, { recursive: true });
4213
+ const tmpFile = `${anchorFile}.${process.pid}.${randomUUID3()}.tmp`;
4214
+ writeFileSync(tmpFile, `${liveBootTime}
4215
+ `);
4216
+ try {
4217
+ linkSync(tmpFile, anchorFile);
4218
+ } catch (error) {
4219
+ if (error.code !== "EEXIST")
4220
+ throw error;
4221
+ } finally {
4222
+ unlinkSync(tmpFile);
4223
+ }
4224
+ removeStaleBootAnchors(anchorDir, anchorName);
4225
+ return readBootAnchor(anchorFile) ?? liveBootTime;
4226
+ } catch (error) {
4227
+ console.error(`[studio] Could not persist the Wine boot-time anchor at ${anchorFile}: ${error instanceof Error ? error.message : String(error)}. Studio identities may not survive a broker restart after a clock change.`);
4228
+ return liveBootTime;
4229
+ }
4230
+ }
4231
+ function bootTimeSeconds(procRoot) {
4232
+ const cached = bootTimeByProcRoot.get(procRoot);
4233
+ if (cached !== void 0)
4234
+ return cached;
4235
+ const bootTime = anchoredBootTime(procRoot, readLiveBootTime(procRoot));
4236
+ bootTimeByProcRoot.set(procRoot, bootTime);
4237
+ return bootTime;
4238
+ }
4239
+ function readWineProcessStartTime(pid, procRoot = DEFAULT_PROC_ROOT) {
4240
+ const stat2 = readProcStat(procRoot, pid);
4241
+ if (!stat2 || stat2.state === "Z" || stat2.state === "X")
4242
+ return void 0;
4243
+ return procStartTimeToFileTime(bootTimeSeconds(procRoot), stat2.startTimeTicks);
4244
+ }
4245
+ function listWineStudioProcesses(procRoot = DEFAULT_PROC_ROOT, uid = process.getuid?.()) {
4246
+ const processes = [];
4247
+ for (const entry of readdirSync(procRoot)) {
4248
+ if (!/^\d+$/u.test(entry))
4249
+ continue;
4250
+ const pid = Number(entry);
4251
+ let argv0;
4252
+ let startedAt;
4253
+ try {
4254
+ if (uid !== void 0 && readRealUid(procRoot, pid) !== uid)
4255
+ continue;
4256
+ argv0 = readArgv0(procRoot, pid);
4257
+ if (!isWineStudioCommand(argv0))
4258
+ continue;
4259
+ startedAt = readWineProcessStartTime(pid, procRoot);
4260
+ } catch (error) {
4261
+ if (error.code === "EACCES" || isMissingProcessError(error))
4262
+ continue;
4263
+ throw error;
4264
+ }
4265
+ if (startedAt === void 0)
4266
+ continue;
4267
+ processes.push({
4268
+ Id: pid,
4269
+ Name: "RobloxStudioBeta",
4270
+ Path: argv0,
4271
+ MainWindowTitle: "",
4272
+ StartTimeUtcFileTime: startedAt
4273
+ });
4274
+ }
4275
+ return processes;
4276
+ }
4277
+ function signalProcess(pid, signal) {
4278
+ try {
4279
+ process.kill(pid, signal);
4280
+ } catch (error) {
4281
+ if (!isMissingProcessError(error))
4282
+ throw error;
4283
+ }
4284
+ }
4285
+ async function stopWineStudio(pid, startedAt, timeoutMs, procRoot = DEFAULT_PROC_ROOT) {
4286
+ if (!Number.isSafeInteger(pid) || pid <= 0)
4287
+ throw new Error("processId must be a positive integer.");
4288
+ if (!/^[1-9]\d*$/u.test(startedAt))
4289
+ throw new Error("startedAt must be a positive FILETIME string.");
4290
+ const isTarget = () => readWineProcessStartTime(pid, procRoot) === startedAt;
4291
+ if (!isTarget())
4292
+ return;
4293
+ signalProcess(pid, "SIGTERM");
4294
+ const graceDeadline = Date.now() + Math.min(TERMINATE_GRACE_MS, timeoutMs / 2);
4295
+ while (Date.now() < graceDeadline) {
4296
+ await delay3(POLL_INTERVAL_MS);
4297
+ if (!isTarget())
4298
+ return;
4299
+ }
4300
+ if (isTarget())
4301
+ signalProcess(pid, "SIGKILL");
4302
+ }
4303
+ function killOrphanedWineLaunchGate(pid, startedAt, procRoot = DEFAULT_PROC_ROOT) {
4304
+ if (!Number.isSafeInteger(pid) || pid <= 0)
4305
+ return false;
4306
+ const stat2 = readProcStat(procRoot, pid);
4307
+ if (stat2?.state !== "T")
4308
+ return false;
4309
+ if (procStartTimeToFileTime(bootTimeSeconds(procRoot), stat2.startTimeTicks) !== startedAt)
4310
+ return false;
4311
+ const cmdline = readCmdline(procRoot, pid);
4312
+ if (cmdline?.[1] !== "-c" || cmdline[2] !== GATE_SCRIPT)
4313
+ return false;
4314
+ signalProcess(pid, "SIGKILL");
4315
+ return true;
4316
+ }
4317
+ function studioExecTimeoutMs(explicit) {
4318
+ if (explicit !== void 0)
4319
+ return explicit;
4320
+ const configured = process.env[STUDIO_EXEC_TIMEOUT_ENV];
4321
+ if (configured === void 0 || configured === "")
4322
+ return STUDIO_EXEC_TIMEOUT_MS;
4323
+ const value = /^\d+$/u.test(configured) ? Number(configured) : NaN;
4324
+ if (!Number.isSafeInteger(value) || value < 1 || value > MAX_STUDIO_EXEC_TIMEOUT_MS) {
4325
+ throw new Error(`${STUDIO_EXEC_TIMEOUT_ENV} must be a whole number of milliseconds from 1 to ${MAX_STUDIO_EXEC_TIMEOUT_MS}; got "${configured}".`);
4326
+ }
4327
+ return value;
4328
+ }
4329
+ function openLauncherLog(logDir) {
4330
+ try {
4331
+ mkdirSync(logDir, { recursive: true });
4332
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/gu, "-");
4333
+ const file = path4.join(logDir, `${stamp}-${randomUUID3().slice(0, 8)}.log`);
4334
+ const fd = openSync(file, "a", 384);
4335
+ const logs = readdirSync(logDir).filter((name) => name.endsWith(".log")).sort();
4336
+ for (const name of logs.slice(0, Math.max(0, logs.length - LAUNCHER_LOGS_KEPT))) {
4337
+ rmSync(path4.join(logDir, name), { force: true });
4338
+ }
4339
+ return { file, fd };
4340
+ } catch (error) {
4341
+ console.error(`[studio] Could not open a Wine launcher log in ${logDir}; launcher output is discarded: ${error instanceof Error ? error.message : String(error)}`);
4342
+ return void 0;
4343
+ }
4344
+ }
4345
+ function launcherLogTail(file) {
4346
+ if (file === void 0)
4347
+ return "";
4348
+ let tail;
4349
+ try {
4350
+ const fd = openSync(file, "r");
4351
+ try {
4352
+ const size = fstatSync(fd).size;
4353
+ const length = Math.min(size, LAUNCHER_LOG_TAIL_BYTES);
4354
+ const buffer = Buffer.alloc(length);
4355
+ readSync(fd, buffer, 0, length, size - length);
4356
+ tail = buffer.toString("utf8");
4357
+ } finally {
4358
+ closeSync(fd);
4359
+ }
4360
+ } catch {
4361
+ return ` Launcher log: ${file}.`;
4362
+ }
4363
+ const lines = tail.trimEnd().split("\n").slice(-LAUNCHER_LOG_TAIL_LINES).join("\n");
4364
+ return lines ? ` Launcher output (${file}):
4365
+ ${lines}` : ` The launcher wrote no output (log: ${file}).`;
4366
+ }
4367
+ async function spawnWineStudio(launcher, exe, args, spawnOptions, options = {}) {
4368
+ const procRoot = options.procRoot ?? DEFAULT_PROC_ROOT;
4369
+ const execTimeoutMs = studioExecTimeoutMs(options.studioExecTimeoutMs);
4370
+ const log = options.logDir === void 0 ? void 0 : openLauncherLog(options.logDir);
4371
+ let child;
4372
+ try {
4373
+ child = spawn("/bin/sh", ["-c", GATE_SCRIPT, launcher, exe, ...args], {
4374
+ cwd: spawnOptions.cwd,
4375
+ env: spawnOptions.env,
4376
+ detached: true,
4377
+ stdio: log ? ["ignore", log.fd, log.fd] : "ignore"
4378
+ });
4379
+ } finally {
4380
+ if (log)
4381
+ closeSync(log.fd);
4382
+ }
4383
+ const logTail = () => launcherLogTail(log?.file);
4384
+ let exitDetail;
4385
+ const { promise: exited, resolve: markExited } = Promise.withResolvers();
4386
+ child.once("exit", (code, signal) => {
4387
+ exitDetail ??= signal ? `signal ${signal}` : `code ${code}`;
4388
+ markExited();
4389
+ });
4390
+ child.once("error", (error) => {
4391
+ exitDetail ??= error.message;
4392
+ });
4393
+ const pid = child.pid;
4394
+ if (pid === void 0) {
4395
+ await delay3(0);
4396
+ throw new Error(`Failed to start the Wine Studio launch gate: ${exitDetail ?? "no PID was assigned"}.`);
4397
+ }
4398
+ const gateDeadline = Date.now() + (options.gateTimeoutMs ?? GATE_TIMEOUT_MS);
4399
+ let gated;
4400
+ try {
4401
+ while (exitDetail === void 0 && Date.now() < gateDeadline) {
4402
+ const stat2 = readProcStat(procRoot, pid);
4403
+ if (stat2?.state === "T") {
4404
+ gated = stat2;
4405
+ break;
4406
+ }
4407
+ await delay3(10);
4408
+ }
4409
+ } catch (error) {
4410
+ child.kill("SIGKILL");
4411
+ throw error;
4412
+ }
4413
+ if (!gated) {
4414
+ child.kill("SIGKILL");
4415
+ throw new Error(exitDetail === void 0 ? "Timed out waiting for the Wine Studio launch gate to stop; the gated process was killed." : `Wine Studio launch gate exited before stopping (${exitDetail}).${logTail()}`);
4416
+ }
4417
+ const nativeStartedAt = procStartTimeToFileTime(bootTimeSeconds(procRoot), gated.startTimeTicks);
4418
+ let controlState = "pending";
4419
+ const authorize = async () => {
4420
+ if (controlState === "resumed")
4421
+ return;
4422
+ if (controlState !== "pending") {
4423
+ throw new Error(`Studio launch cannot be authorized from ${controlState}.`);
4424
+ }
4425
+ controlState = "authorizing";
4426
+ child.kill("SIGCONT");
4427
+ const deadline = Date.now() + execTimeoutMs;
4428
+ try {
4429
+ while (exitDetail === void 0 && Date.now() < deadline) {
4430
+ if (readWineProcessStartTime(pid, procRoot) === nativeStartedAt && isWineStudioCommand(readArgv0(procRoot, pid))) {
4431
+ controlState = "resumed";
4432
+ return;
4433
+ }
4434
+ await delay3(POLL_INTERVAL_MS);
4435
+ }
4436
+ } catch (error) {
4437
+ controlState = "aborted";
4438
+ child.kill("SIGKILL");
4439
+ throw error;
4440
+ }
4441
+ controlState = "aborted";
4442
+ if (exitDetail !== void 0) {
4443
+ throw new Error(`Wine Studio process ${pid} exited (${exitDetail}) before RobloxStudioBeta.exe started.${logTail()}`);
4444
+ }
4445
+ child.kill("SIGKILL");
4446
+ throw new Error(`Wine launcher did not exec RobloxStudioBeta.exe in gated process ${pid} within ${execTimeoutMs} ms; the process was killed. ROBLOX_STUDIO_WINE_LAUNCHER must exec Wine so Studio keeps the launcher PID; if Wine is only slow to start, raise ${STUDIO_EXEC_TIMEOUT_ENV}.${logTail()}`);
4447
+ };
4448
+ const release = async () => {
4449
+ if (controlState === "released")
4450
+ return;
4451
+ if (controlState !== "resumed") {
4452
+ throw new Error(`Studio launch cannot be released from ${controlState}.`);
4453
+ }
4454
+ controlState = "released";
4455
+ };
4456
+ const abort = async () => {
4457
+ if (controlState === "released")
4458
+ return;
4459
+ controlState = "aborted";
4460
+ if (exitDetail !== void 0)
4461
+ return;
4462
+ child.kill("SIGKILL");
4463
+ const { promise: timedOut, resolve: markTimedOut } = Promise.withResolvers();
4464
+ const timer = setTimeout(() => markTimedOut(true), ABORT_TIMEOUT_MS);
4465
+ try {
4466
+ if (await Promise.race([exited.then(() => false), timedOut])) {
4467
+ throw new Error(`Timed out aborting Wine Studio process ${pid}.`);
4468
+ }
4469
+ } finally {
4470
+ clearTimeout(timer);
4471
+ }
4472
+ };
4473
+ return {
4474
+ pid,
4475
+ nativePid: pid,
4476
+ nativeStartedAt,
4477
+ unref: () => child.unref(),
4478
+ authorize,
4479
+ release,
4480
+ abort,
4481
+ onExit: (listener) => {
4482
+ child.once("exit", listener);
4483
+ },
4484
+ onError: (listener) => {
4485
+ child.once("error", listener);
4486
+ }
4487
+ };
4488
+ }
4489
+
4020
4490
  // ../core/dist/studio-instance-manager.js
4021
- var BASEPLATE_TEMP_DIR = path3.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
4491
+ var BASEPLATE_TEMP_DIR = path5.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
4022
4492
  var BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
4023
4493
  var BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
4024
4494
  var retainedLaunchControls = /* @__PURE__ */ new Map();
@@ -4101,6 +4571,9 @@ async function runAsync(command, args, options = {}) {
4101
4571
  function isWsl() {
4102
4572
  return getStudioPlatformCapabilities().isWsl;
4103
4573
  }
4574
+ function isWineHost() {
4575
+ return getStudioPlatformCapabilities().processIdentity.launcher === "wine-retained";
4576
+ }
4104
4577
  async function powershellAsync(script, timeoutMs = 15e3) {
4105
4578
  return runAsync("powershell.exe", ["-NoProfile", "-Command", script], {
4106
4579
  cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
@@ -4144,7 +4617,9 @@ async function toWslPathAsync(windowsPath) {
4144
4617
  return runAsync("wslpath", ["-u", windowsPath]);
4145
4618
  }
4146
4619
  async function toStudioLaunchArgAsync(arg) {
4147
- if (!isWsl() || !path3.isAbsolute(arg) || !existsSync2(arg))
4620
+ if (isWineHost() && path5.isAbsolute(arg) && existsSync2(arg))
4621
+ return toWineWindowsPath(arg);
4622
+ if (!isWsl() || !path5.isAbsolute(arg) || !existsSync2(arg))
4148
4623
  return arg;
4149
4624
  return runAsync("wslpath", ["-w", arg]);
4150
4625
  }
@@ -4464,7 +4939,8 @@ public sealed class McpSuspendedStudio : IDisposable
4464
4939
 
4465
4940
  private static IntPtr OpenTestWorkerJob(string name)
4466
4941
  {
4467
- if (name == null) return IntPtr.Zero;
4942
+ // PowerShell passes $null to a .NET string parameter as "", so both mean "no worker job".
4943
+ if (String.IsNullOrEmpty(name)) return IntPtr.Zero;
4468
4944
  if (!System.Text.RegularExpressions.Regex.IsMatch(name, @"\\ALocal\\\\RsmcpStudioWorker-[a-f0-9]{32}\\z"))
4469
4945
  throw new ArgumentException("Invalid Studio test worker job name");
4470
4946
  IntPtr handle = OpenJobObjectW(5, false, name);
@@ -4640,7 +5116,7 @@ var studioLogsRoot;
4640
5116
  async function settleWindowsStudioSignIn(startedAt) {
4641
5117
  studioLogsRoot ??= (async () => {
4642
5118
  const localAppData = await windowsLocalAppDataAsync();
4643
- return localAppData ? path3.join(await toWslPathAsync(localAppData), "Roblox", "logs") : void 0;
5119
+ return localAppData ? path5.join(await toWslPathAsync(localAppData), "Roblox", "logs") : void 0;
4644
5120
  })();
4645
5121
  const logsRoot = await studioLogsRoot;
4646
5122
  if (!logsRoot)
@@ -4657,7 +5133,7 @@ async function spawnWindowsStudio(exe, args, processEnvironment, studioWorkingDi
4657
5133
  const windowsWorkingDirectory = studioWorkingDirectory ? await toStudioLaunchArgAsync(studioWorkingDirectory) : void 0;
4658
5134
  const testWorkerJobName = process.env.RSMCP_STUDIO_TEST_WORKER_JOB;
4659
5135
  const script = buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment, windowsWorkingDirectory, testWorkerJobName);
4660
- const launcher = spawn("powershell.exe", ["-NoProfile", "-Command", script], {
5136
+ const launcher = spawn2("powershell.exe", ["-NoProfile", "-Command", script], {
4661
5137
  cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
4662
5138
  stdio: ["pipe", "pipe", "pipe"],
4663
5139
  windowsHide: true
@@ -4877,21 +5353,21 @@ function resolveEntrypointDir() {
4877
5353
  if (!entrypoint)
4878
5354
  return void 0;
4879
5355
  try {
4880
- return path3.dirname(realpathSync(entrypoint));
5356
+ return path5.dirname(realpathSync(entrypoint));
4881
5357
  } catch {
4882
- return path3.dirname(path3.resolve(entrypoint));
5358
+ return path5.dirname(path5.resolve(entrypoint));
4883
5359
  }
4884
5360
  }
4885
5361
  function resolveBaseplateTemplatePath() {
4886
5362
  const entrypointDir = resolveEntrypointDir();
4887
5363
  const candidates = [
4888
5364
  ...entrypointDir ? [
4889
- path3.join(entrypointDir, "assets", BASEPLATE_TEMPLATE_NAME),
4890
- path3.join(entrypointDir, "..", "assets", BASEPLATE_TEMPLATE_NAME)
5365
+ path5.join(entrypointDir, "assets", BASEPLATE_TEMPLATE_NAME),
5366
+ path5.join(entrypointDir, "..", "assets", BASEPLATE_TEMPLATE_NAME)
4891
5367
  ] : [],
4892
- path3.join(process.cwd(), "packages", "core", "assets", BASEPLATE_TEMPLATE_NAME),
4893
- path3.join(process.cwd(), "packages", "robloxstudio-mcp", "dist", "assets", BASEPLATE_TEMPLATE_NAME),
4894
- path3.join(process.cwd(), "assets", BASEPLATE_TEMPLATE_NAME)
5368
+ path5.join(process.cwd(), "packages", "core", "assets", BASEPLATE_TEMPLATE_NAME),
5369
+ path5.join(process.cwd(), "packages", "robloxstudio-mcp", "dist", "assets", BASEPLATE_TEMPLATE_NAME),
5370
+ path5.join(process.cwd(), "assets", BASEPLATE_TEMPLATE_NAME)
4895
5371
  ];
4896
5372
  for (const candidate of candidates) {
4897
5373
  if (existsSync2(candidate))
@@ -4913,7 +5389,7 @@ function isProcessAlive2(pid) {
4913
5389
  function sweepStaleBaseplateFiles() {
4914
5390
  let entries;
4915
5391
  try {
4916
- entries = readdirSync(BASEPLATE_TEMP_DIR);
5392
+ entries = readdirSync2(BASEPLATE_TEMP_DIR);
4917
5393
  } catch {
4918
5394
  return;
4919
5395
  }
@@ -4924,24 +5400,24 @@ function sweepStaleBaseplateFiles() {
4924
5400
  continue;
4925
5401
  if (Number(match[1]) !== process.pid && isProcessAlive2(Number(match[1])))
4926
5402
  continue;
4927
- const file = path3.join(BASEPLATE_TEMP_DIR, entry);
5403
+ const file = path5.join(BASEPLATE_TEMP_DIR, entry);
4928
5404
  try {
4929
- if (statSync(file).mtimeMs < cutoff)
4930
- rmSync(file, { force: true });
5405
+ if (statSync2(file).mtimeMs < cutoff)
5406
+ rmSync2(file, { force: true });
4931
5407
  } catch {
4932
5408
  }
4933
5409
  }
4934
5410
  }
4935
5411
  function createBaseplatePlaceFile() {
4936
- mkdirSync(BASEPLATE_TEMP_DIR, { recursive: true });
5412
+ mkdirSync2(BASEPLATE_TEMP_DIR, { recursive: true });
4937
5413
  sweepStaleBaseplateFiles();
4938
- const file = path3.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
5414
+ const file = path5.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
4939
5415
  copyFileSync(resolveBaseplateTemplatePath(), file);
4940
5416
  return file;
4941
5417
  }
4942
5418
  function isGeneratedBaseplatePlaceFile(file) {
4943
- const resolvedFile = path3.resolve(file);
4944
- return path3.dirname(resolvedFile) === path3.resolve(BASEPLATE_TEMP_DIR) && BASEPLATE_TEMP_NAME.test(path3.basename(resolvedFile));
5419
+ const resolvedFile = path5.resolve(file);
5420
+ return path5.dirname(resolvedFile) === path5.resolve(BASEPLATE_TEMP_DIR) && BASEPLATE_TEMP_NAME.test(path5.basename(resolvedFile));
4945
5421
  }
4946
5422
  function cleanupManagedBaseplateFiles(record) {
4947
5423
  if (record.source !== "baseplate" || !record.localPlaceFile)
@@ -4950,7 +5426,7 @@ function cleanupManagedBaseplateFiles(record) {
4950
5426
  return;
4951
5427
  for (const file of [record.localPlaceFile, `${record.localPlaceFile}.lock`]) {
4952
5428
  try {
4953
- rmSync(file, { force: true });
5429
+ rmSync2(file, { force: true });
4954
5430
  } catch {
4955
5431
  }
4956
5432
  }
@@ -4967,21 +5443,33 @@ function resolveStudioExeFromVersions(root) {
4967
5443
  if (!existsSync2(root)) {
4968
5444
  throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
4969
5445
  }
4970
- const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path3.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
5446
+ const candidates = readdirSync2(root).filter((name) => name.startsWith("version-")).map((name) => path5.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
4971
5447
  if (candidates.length === 0) {
4972
5448
  throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
4973
5449
  }
4974
5450
  const exe = candidates[0];
4975
- const installationDir = path3.dirname(exe);
4976
- const hasIncompleteDownload = readdirSync(installationDir).some((name) => /\.crdownload$/i.test(name));
4977
- const settings = statSync(path3.join(installationDir, "AppSettings.xml"), { throwIfNoEntry: false });
5451
+ const installationDir = path5.dirname(exe);
5452
+ const hasIncompleteDownload = readdirSync2(installationDir).some((name) => /\.crdownload$/i.test(name));
5453
+ const settings = statSync2(path5.join(installationDir, "AppSettings.xml"), { throwIfNoEntry: false });
4978
5454
  if (hasIncompleteDownload || !settings?.isFile() || settings.size === 0) {
4979
5455
  const reason = hasIncompleteDownload ? "an unfinished .crdownload download is present" : "AppSettings.xml is missing, empty, or not a regular file";
4980
5456
  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.`);
4981
5457
  }
4982
5458
  return exe;
4983
5459
  }
5460
+ function resolveNativeLinuxStudioExe() {
5461
+ const exe = process.env.ROBLOX_STUDIO_EXE;
5462
+ if (!exe) {
5463
+ throw new Error("Set ROBLOX_STUDIO_EXE to RobloxStudioBeta.exe inside your Wine prefix to launch Studio on native Linux.");
5464
+ }
5465
+ if (!statSync2(exe, { throwIfNoEntry: false })?.isFile()) {
5466
+ throw new Error(`ROBLOX_STUDIO_EXE does not name an existing file: ${exe}`);
5467
+ }
5468
+ return exe;
5469
+ }
4984
5470
  function resolveStudioExe() {
5471
+ if (process.platform === "linux" && isWineHost())
5472
+ return resolveNativeLinuxStudioExe();
4985
5473
  if (process.env.ROBLOX_STUDIO_EXE)
4986
5474
  return process.env.ROBLOX_STUDIO_EXE;
4987
5475
  if (process.platform === "darwin") {
@@ -4991,10 +5479,12 @@ function resolveStudioExe() {
4991
5479
  throw new Error("Roblox Studio executable auto-discovery is only supported on Windows, WSL, and macOS. Set ROBLOX_STUDIO_EXE.");
4992
5480
  }
4993
5481
  const localAppData = windowsLocalAppData();
4994
- const root = localAppData ? path3.join(toWslPath(localAppData), "Roblox", "Versions") : path3.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
5482
+ const root = localAppData ? path5.join(toWslPath(localAppData), "Roblox", "Versions") : path5.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
4995
5483
  return resolveStudioExeFromVersions(root);
4996
5484
  }
4997
5485
  async function resolveStudioExeAsync() {
5486
+ if (process.platform === "linux" && isWineHost())
5487
+ return resolveNativeLinuxStudioExe();
4998
5488
  if (process.env.ROBLOX_STUDIO_EXE)
4999
5489
  return process.env.ROBLOX_STUDIO_EXE;
5000
5490
  if (process.platform === "darwin") {
@@ -5004,7 +5494,7 @@ async function resolveStudioExeAsync() {
5004
5494
  throw new Error("Roblox Studio executable auto-discovery is only supported on Windows, WSL, and macOS. Set ROBLOX_STUDIO_EXE.");
5005
5495
  }
5006
5496
  const localAppData = await windowsLocalAppDataAsync();
5007
- const root = localAppData ? path3.join(await toWslPathAsync(localAppData), "Roblox", "Versions") : path3.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
5497
+ const root = localAppData ? path5.join(await toWslPathAsync(localAppData), "Roblox", "Versions") : path5.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
5008
5498
  return resolveStudioExeFromVersions(root);
5009
5499
  }
5010
5500
  var WINDOWS_STUDIO_PROCESS_QUERY = [
@@ -5041,6 +5531,9 @@ async function observeStudioProcesses(timeoutMs = 15e3) {
5041
5531
  }).filter((proc) => Number.isFinite(proc.Id));
5042
5532
  return { status: "ok", observedAt, processes };
5043
5533
  }
5534
+ if (process.platform === "linux" && isWineHost()) {
5535
+ return { status: "ok", observedAt, processes: listWineStudioProcesses() };
5536
+ }
5044
5537
  if (process.platform !== "win32" && !isWsl()) {
5045
5538
  return { status: "ok", observedAt, processes: [] };
5046
5539
  }
@@ -5057,7 +5550,7 @@ async function observeStudioProcesses(timeoutMs = 15e3) {
5057
5550
  async function currentBootIdAsync() {
5058
5551
  if (process.platform === "linux") {
5059
5552
  try {
5060
- return readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
5553
+ return readFileSync3("/proc/sys/kernel/random/boot_id", "utf8").trim();
5061
5554
  } catch {
5062
5555
  }
5063
5556
  }
@@ -5108,11 +5601,11 @@ function buildStudioLaunchArgs(options) {
5108
5601
  ];
5109
5602
  }
5110
5603
  }
5111
- function delay3(ms) {
5604
+ function delay4(ms) {
5112
5605
  return new Promise((resolve6) => setTimeout(resolve6, ms));
5113
5606
  }
5114
5607
  function basenameAny(filePath) {
5115
- return path3.basename(filePath.replace(/\\/g, "/"));
5608
+ return path5.basename(filePath.replace(/\\/g, "/"));
5116
5609
  }
5117
5610
  var StudioInstanceManager = class {
5118
5611
  managedByInstanceId = /* @__PURE__ */ new Map();
@@ -5364,8 +5857,15 @@ var StudioInstanceManager = class {
5364
5857
  proc = await this.processAdapter.spawnStudio(exe, args, spawnOptions);
5365
5858
  } else if (lifecycleCapabilities.processIdentity.launcher === "windows-retained" || lifecycleCapabilities.processIdentity.launcher === "wsl-windows-retained") {
5366
5859
  proc = await this.windowsStudioLauncher(exe, args, processEnvironment, studioWorkingDirectory);
5860
+ } else if (lifecycleCapabilities.processIdentity.launcher === "wine-retained") {
5861
+ const wineLauncher = (this.platformCapabilities ?? getStudioPlatformCapabilities()).wineLauncher;
5862
+ if (!wineLauncher)
5863
+ throw new Error("The Wine Studio launcher path is unavailable.");
5864
+ proc = await spawnWineStudio(wineLauncher, exe, args, spawnOptions, {
5865
+ logDir: path5.join(this.registry.dir, "wine-launches")
5866
+ });
5367
5867
  } else {
5368
- const child = spawn(exe, args, spawnOptions);
5868
+ const child = spawn2(exe, args, spawnOptions);
5369
5869
  proc = {
5370
5870
  pid: child.pid,
5371
5871
  nativePid: child.pid,
@@ -5415,7 +5915,7 @@ var StudioInstanceManager = class {
5415
5915
  }
5416
5916
  const launchedAt = Date.now();
5417
5917
  const record = {
5418
- recordId: randomUUID3(),
5918
+ recordId: randomUUID4(),
5419
5919
  source: options.source,
5420
5920
  nativeProcessId: proc.nativePid,
5421
5921
  nativeProcessStartedAt: proc.nativeStartedAt,
@@ -5485,7 +5985,7 @@ var StudioInstanceManager = class {
5485
5985
  await this.persist(record);
5486
5986
  break;
5487
5987
  }
5488
- await delay3(250);
5988
+ await delay4(250);
5489
5989
  }
5490
5990
  if (record.nativeProcessId === void 0 && process.platform !== "win32" && !isWsl()) {
5491
5991
  record.nativeProcessId = proc.pid;
@@ -5667,6 +6167,11 @@ var StudioInstanceManager = class {
5667
6167
  throw new Error("Cannot stop a Windows Studio process without its creation-time identity.");
5668
6168
  }
5669
6169
  await stopWindowsStudio(processId, startedAt, Math.max(1, deadline - Date.now()));
6170
+ } else if (isWineHost()) {
6171
+ if (startedAt === void 0) {
6172
+ throw new Error("Cannot stop a Wine Studio process without its creation-time identity.");
6173
+ }
6174
+ await stopWineStudio(processId, startedAt, Math.max(1, deadline - Date.now()));
5670
6175
  } else {
5671
6176
  try {
5672
6177
  process.kill(processId, "SIGTERM");
@@ -5687,7 +6192,7 @@ var StudioInstanceManager = class {
5687
6192
  if (remaining <= 0) {
5688
6193
  throw new StudioCloseVerificationError(`Timed out closing Studio process ${processId}: the process is still running. Close can be retried.`, observation);
5689
6194
  }
5690
- await delay3(Math.min(100, remaining));
6195
+ await delay4(Math.min(100, remaining));
5691
6196
  if (Date.now() >= deadline) {
5692
6197
  throw new StudioCloseVerificationError(`Timed out closing Studio process ${processId}: the process is still running. Close can be retried.`, observation);
5693
6198
  }
@@ -5784,15 +6289,39 @@ var StudioInstanceManager = class {
5784
6289
  confirmedExitGraceMs: this.confirmedExitGraceMs
5785
6290
  };
5786
6291
  }
6292
+ isOrphanedUnreleasedLaunch(registryRecord, currentBootId) {
6293
+ const ownerPid = registryRecord.ownerPid;
6294
+ return !(registryRecord.processAuthorizationState === "released" || this.launchControls.has(registryRecord.recordId) || registryRecord.bootId !== currentBootId || ownerPid === void 0 || ownerPid !== process.pid && isProcessAlive2(ownerPid));
6295
+ }
6296
+ // A broker that dies between spawn and authorization leaves its Wine launch
6297
+ // gate stopped. The gate is /bin/sh, not Studio, so the orphan cleanup below
6298
+ // never sees it in a Studio snapshot.
6299
+ async reapOrphanedWineLaunchGates(currentBootId) {
6300
+ if (this.getLifecycleCapabilities().processIdentity.launcher !== "wine-retained")
6301
+ return;
6302
+ for (const registryRecord of await this.registry.listOpenUnchecked()) {
6303
+ const processId = registryRecord.nativeProcessId;
6304
+ const startedAt = registryRecord.nativeProcessStartedAt;
6305
+ if (registryRecord.processAuthorizationState !== "pending" || !processId || !startedAt || !this.isOrphanedUnreleasedLaunch(registryRecord, currentBootId)) {
6306
+ continue;
6307
+ }
6308
+ try {
6309
+ if (killOrphanedWineLaunchGate(processId, startedAt)) {
6310
+ console.error(`[studio] Killed Wine launch gate ${processId}, left stopped by a broker that exited before authorizing it.`);
6311
+ }
6312
+ } catch (error) {
6313
+ console.error(`[studio] Could not check orphaned Wine launch gate ${processId}: ${error instanceof Error ? error.message : String(error)}`);
6314
+ }
6315
+ }
6316
+ }
5787
6317
  async sweepRegistry(snapshot) {
5788
6318
  const sweepOptions = await this.registrySweepOptions(snapshot);
6319
+ await this.reapOrphanedWineLaunchGates(sweepOptions.currentBootId);
5789
6320
  await this.registry.sweep(sweepOptions);
5790
6321
  const persisted = await this.registry.listOpenUnchecked();
5791
6322
  for (const registryRecord of persisted) {
5792
- const ownerPid = registryRecord.ownerPid;
5793
- if (registryRecord.processAuthorizationState === "released" || this.launchControls.has(registryRecord.recordId) || registryRecord.bootId !== sweepOptions.currentBootId || ownerPid === void 0 || ownerPid !== process.pid && isProcessAlive2(ownerPid)) {
6323
+ if (!this.isOrphanedUnreleasedLaunch(registryRecord, sweepOptions.currentBootId))
5794
6324
  continue;
5795
- }
5796
6325
  const record = this.fromRegistryRecord(registryRecord);
5797
6326
  if (this.observeRecord(record, snapshot).status !== "running")
5798
6327
  continue;
@@ -5855,13 +6384,13 @@ var StudioInstanceManager = class {
5855
6384
  const processId = record.nativeProcessId ?? record.spawnPid;
5856
6385
  if (record.spawnPid && record.spawnPid === processId && studioProcess.Id === processId)
5857
6386
  return true;
5858
- const processPath = studioProcess.Path ? path3.normalize(studioProcess.Path).toLowerCase() : "";
5859
- const exePath = record.exe ? path3.normalize(record.exe).toLowerCase() : "";
6387
+ const processPath = studioProcess.Path ? path5.normalize(studioProcess.Path).toLowerCase() : "";
6388
+ const exePath = record.exe ? path5.normalize(record.exe).toLowerCase() : "";
5860
6389
  if (processPath && exePath && (processPath === exePath || basenameAny(processPath) === basenameAny(exePath))) {
5861
6390
  return true;
5862
6391
  }
5863
6392
  const commandLine = studioProcess.CommandLine ?? "";
5864
- if (record.localPlaceFile && commandLine.includes(path3.basename(record.localPlaceFile)))
6393
+ if (record.localPlaceFile && commandLine.includes(path5.basename(record.localPlaceFile)))
5865
6394
  return true;
5866
6395
  if (record.placeId !== void 0 && commandLine.includes(String(record.placeId)))
5867
6396
  return true;
@@ -6180,6 +6709,7 @@ var OPEN_WORLD_TOOLS = /* @__PURE__ */ new Set([
6180
6709
  "get_asset_thumbnail",
6181
6710
  "get_roblox_docs",
6182
6711
  "insert_asset",
6712
+ "manage_monetization",
6183
6713
  "preview_asset",
6184
6714
  "search_assets",
6185
6715
  "upload_asset"
@@ -6392,17 +6922,17 @@ function createToolHttpHandler(options) {
6392
6922
 
6393
6923
  // ../core/dist/auth.js
6394
6924
  import { randomBytes, createHash as createHash2, timingSafeEqual } from "crypto";
6395
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync, linkSync, unlinkSync } from "fs";
6396
- import { join as join4, dirname as dirname2 } from "path";
6925
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, linkSync as linkSync2, unlinkSync as unlinkSync2 } from "fs";
6926
+ import { join as join5, dirname as dirname2 } from "path";
6397
6927
  import { homedir as homedir3 } from "os";
6398
6928
  function authTokenFilePath() {
6399
- return join4(homedir3(), ".robloxstudio-mcp", "auth-token");
6929
+ return join5(homedir3(), ".robloxstudio-mcp", "auth-token");
6400
6930
  }
6401
6931
  var EMPTY_TOKEN_RETRY_MS = 25;
6402
6932
  var EMPTY_TOKEN_RETRIES = 20;
6403
6933
  function readTokenFile(filePath) {
6404
6934
  for (let attempt = 0; ; attempt++) {
6405
- const token = readFileSync3(filePath, "utf8").trim();
6935
+ const token = readFileSync4(filePath, "utf8").trim();
6406
6936
  if (token || attempt >= EMPTY_TOKEN_RETRIES)
6407
6937
  return token;
6408
6938
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, EMPTY_TOKEN_RETRY_MS);
@@ -6429,12 +6959,12 @@ function resolveAuthToken() {
6429
6959
  throw error;
6430
6960
  }
6431
6961
  const fresh = randomBytes(32).toString("hex");
6432
- mkdirSync2(dirname2(filePath), { recursive: true, mode: 448 });
6962
+ mkdirSync3(dirname2(filePath), { recursive: true, mode: 448 });
6433
6963
  const stagingPath = `${filePath}.${process.pid}.${randomBytes(16).toString("hex")}.tmp`;
6434
- writeFileSync(stagingPath, fresh + "\n", { encoding: "utf8", mode: 384, flag: "wx" });
6964
+ writeFileSync2(stagingPath, fresh + "\n", { encoding: "utf8", mode: 384, flag: "wx" });
6435
6965
  try {
6436
6966
  try {
6437
- linkSync(stagingPath, filePath);
6967
+ linkSync2(stagingPath, filePath);
6438
6968
  } catch (error) {
6439
6969
  if (error.code !== "EEXIST")
6440
6970
  throw error;
@@ -6445,7 +6975,7 @@ function resolveAuthToken() {
6445
6975
  return { token, source: "file", filePath };
6446
6976
  } finally {
6447
6977
  try {
6448
- unlinkSync(stagingPath);
6978
+ unlinkSync2(stagingPath);
6449
6979
  } catch {
6450
6980
  }
6451
6981
  }
@@ -6800,7 +7330,8 @@ var TOOL_PROXY_ENDPOINTS = {
6800
7330
  get_scene_analysis: ["/api/get-scene-analysis"],
6801
7331
  export_rbxm: ["/api/export-rbxm"],
6802
7332
  import_rbxm: ["/api/import-rbxm"],
6803
- find_and_replace_in_scripts: ["/api/find-and-replace-in-scripts"]
7333
+ find_and_replace_in_scripts: ["/api/find-and-replace-in-scripts"],
7334
+ manage_monetization: ["/api/place-info"]
6804
7335
  };
6805
7336
  function toPassivePeer(peer) {
6806
7337
  return {
@@ -6918,6 +7449,7 @@ var TOOL_HANDLERS = {
6918
7449
  set_device_simulator: (tools, body) => tools.setDeviceSimulator(body.target, body.deviceId, body.orientation, body.resolution, body.pixelDensity, body.scalingMode, body.stopSimulation, body.instance_id),
6919
7450
  capture_device_matrix: (tools, body) => tools.captureDeviceMatrix(body.entries, body.target, body.format, body.quality, body.settleSeconds, body.restoreAfter, body.instance_id),
6920
7451
  manage_instance: (tools, body) => tools.manageInstance(body),
7452
+ manage_monetization: (tools, body) => tools.manageMonetization(body, body.instance_id),
6921
7453
  solo_playtest: (tools, body) => tools.soloPlaytest(body.action, body.mode, body.timeout, body.instance_id),
6922
7454
  multiplayer_playtest: (tools, body) => tools.multiplayerPlaytest(body.action, body.numPlayers, body.target, body.testArgs, body.value, body.timeout, body.instance_id),
6923
7455
  get_runtime_logs: (tools, body, context) => tools.getRuntimeLogs(body.instance_id, body.multiplayer_group_id, body.cursor, body.cursor_by_instance, body.tail, body.filter, context?.signal),
@@ -7135,8 +7667,8 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
7135
7667
  });
7136
7668
  const authToken = security?.authToken;
7137
7669
  const authRequired = (requestPath) => {
7138
- const path9 = requestPath.toLowerCase().replace(/\/+$/, "");
7139
- return path9 === "/mcp" || path9.startsWith("/mcp/") || path9 === "/proxy" || path9 === "/topology" || path9 === "/request-status" || path9 === "/unregister-instance-id" || path9 === "/create-multiplayer-group" || path9 === "/remove-multiplayer-group";
7670
+ const path11 = requestPath.toLowerCase().replace(/\/+$/, "");
7671
+ return path11 === "/mcp" || path11.startsWith("/mcp/") || path11 === "/proxy" || path11 === "/topology" || path11 === "/request-status" || path11 === "/unregister-instance-id" || path11 === "/create-multiplayer-group" || path11 === "/remove-multiplayer-group";
7140
7672
  };
7141
7673
  app.use((req, res, next) => {
7142
7674
  if (!authToken || !authRequired(req.path)) {
@@ -7737,6 +8269,56 @@ var StudioHttpClient = class {
7737
8269
  };
7738
8270
 
7739
8271
  // ../core/dist/opencloud-client.js
8272
+ var MONETIZATION_APIS = {
8273
+ developer_product: {
8274
+ label: "developer product",
8275
+ collectionPath: (universeId) => `/developer-products/v2/universes/${universeId}/developer-products`,
8276
+ listField: "developerProducts",
8277
+ idField: "productId",
8278
+ iconField: "iconImageAssetId"
8279
+ },
8280
+ game_pass: {
8281
+ label: "game pass",
8282
+ collectionPath: (universeId) => `/game-passes/v1/universes/${universeId}/game-passes`,
8283
+ listField: "gamePasses",
8284
+ idField: "gamePassId",
8285
+ iconField: "iconAssetId"
8286
+ }
8287
+ };
8288
+ function readField(source, key) {
8289
+ return typeof source === "object" && source !== null ? Reflect.get(source, key) : void 0;
8290
+ }
8291
+ function positiveSafeInteger(value) {
8292
+ const parsed = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : value;
8293
+ return typeof parsed === "number" && Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
8294
+ }
8295
+ function normalizeMonetizationItem(kind, universeId, value) {
8296
+ const api = MONETIZATION_APIS[kind];
8297
+ const id = positiveSafeInteger(readField(value, api.idField));
8298
+ if (id === void 0) {
8299
+ throw new Error(`Open Cloud returned a ${api.label} without a valid ${api.idField}.`);
8300
+ }
8301
+ const name = readField(value, "name");
8302
+ const description = readField(value, "description");
8303
+ const price = readField(readField(value, "priceInformation"), "defaultPriceInRobux");
8304
+ const createdAt = readField(value, "createdTimestamp");
8305
+ const updatedAt = readField(value, "updatedTimestamp");
8306
+ const immutable = readField(value, "isImmutable");
8307
+ return {
8308
+ kind,
8309
+ id,
8310
+ universeId: positiveSafeInteger(readField(value, "universeId")) ?? universeId,
8311
+ name: typeof name === "string" ? name : "",
8312
+ description: typeof description === "string" ? description : "",
8313
+ forSale: readField(value, "isForSale") === true,
8314
+ price: typeof price === "number" && Number.isSafeInteger(price) && price >= 0 ? price : null,
8315
+ managedPricing: readField(value, "isManagedPricingEnabled") === true,
8316
+ iconAssetId: positiveSafeInteger(readField(value, api.iconField)) ?? null,
8317
+ ...typeof createdAt === "string" ? { createdAt } : {},
8318
+ ...typeof updatedAt === "string" ? { updatedAt } : {},
8319
+ ...kind === "developer_product" && typeof immutable === "boolean" ? { immutable } : {}
8320
+ };
8321
+ }
7740
8322
  function detectAudioMimeType(data) {
7741
8323
  if (data.length >= 4 && data.subarray(0, 4).toString("ascii") === "OggS") {
7742
8324
  return "audio/ogg";
@@ -7752,6 +8334,68 @@ function detectAudioMimeType(data) {
7752
8334
  }
7753
8335
  return void 0;
7754
8336
  }
8337
+ var OpenCloudRequestError = class extends Error {
8338
+ status;
8339
+ details;
8340
+ constructor(message, status, details = {}) {
8341
+ super(message);
8342
+ this.status = status;
8343
+ this.details = details;
8344
+ this.name = "OpenCloudRequestError";
8345
+ }
8346
+ };
8347
+ async function openCloudResponseError(response) {
8348
+ const errorBody = await response.text();
8349
+ let parsed;
8350
+ try {
8351
+ parsed = JSON.parse(errorBody);
8352
+ } catch {
8353
+ parsed = void 0;
8354
+ }
8355
+ const text = (key) => {
8356
+ const value = readField(parsed, key);
8357
+ return typeof value === "string" && value ? value : void 0;
8358
+ };
8359
+ const errorMessage2 = text("detail") ?? text("message") ?? text("errorMessage") ?? errorBody;
8360
+ const errorCode3 = readField(parsed, "errorCode");
8361
+ const field = text("field");
8362
+ const hint = text("hint");
8363
+ const details = {
8364
+ ...typeof errorCode3 === "string" || typeof errorCode3 === "number" ? { errorCode: errorCode3 } : {},
8365
+ ...field ? { field } : {},
8366
+ ...hint ? { hint } : {}
8367
+ };
8368
+ const { status } = response;
8369
+ let message;
8370
+ if (status === 401) {
8371
+ message = "Invalid or expired API key";
8372
+ } else if (status === 403) {
8373
+ message = `API key lacks required permissions: ${errorMessage2}`;
8374
+ } else if (status === 429) {
8375
+ message = "Rate limit exceeded. Please try again later.";
8376
+ } else {
8377
+ message = `Open Cloud API error (${status}): ${errorMessage2}`;
8378
+ }
8379
+ return new OpenCloudRequestError(message, status, details);
8380
+ }
8381
+ function monetizationForm(changes) {
8382
+ const form = new FormData();
8383
+ if (changes.name !== void 0)
8384
+ form.append("name", changes.name);
8385
+ if (changes.description !== void 0)
8386
+ form.append("description", changes.description);
8387
+ if (changes.price !== void 0)
8388
+ form.append("price", String(changes.price));
8389
+ if (changes.forSale !== void 0)
8390
+ form.append("isForSale", String(changes.forSale));
8391
+ if (changes.managedPricing !== void 0) {
8392
+ form.append("isManagedPricingEnabled", String(changes.managedPricing));
8393
+ }
8394
+ if (changes.icon) {
8395
+ form.append("imageFile", new Blob([new Uint8Array(changes.icon.data)], { type: changes.icon.mimeType }), changes.icon.fileName);
8396
+ }
8397
+ return form;
8398
+ }
7755
8399
  var OpenCloudClient = class {
7756
8400
  apiKey;
7757
8401
  baseUrl;
@@ -7792,25 +8436,8 @@ var OpenCloudClient = class {
7792
8436
  body: body ? JSON.stringify(body) : void 0,
7793
8437
  signal: controller.signal
7794
8438
  });
7795
- if (!response.ok) {
7796
- const errorBody = await response.text();
7797
- let errorMessage2;
7798
- try {
7799
- const errorJson = JSON.parse(errorBody);
7800
- errorMessage2 = errorJson.detail || errorJson.message || errorBody;
7801
- } catch {
7802
- errorMessage2 = errorBody;
7803
- }
7804
- if (response.status === 401) {
7805
- throw new Error("Invalid or expired API key");
7806
- } else if (response.status === 403) {
7807
- throw new Error(`API key lacks required permissions: ${errorMessage2}`);
7808
- } else if (response.status === 429) {
7809
- throw new Error("Rate limit exceeded. Please try again later.");
7810
- } else {
7811
- throw new Error(`Open Cloud API error (${response.status}): ${errorMessage2}`);
7812
- }
7813
- }
8439
+ if (!response.ok)
8440
+ throw await openCloudResponseError(response);
7814
8441
  return await response.json();
7815
8442
  } catch (error) {
7816
8443
  if (controller.signal.aborted)
@@ -7855,6 +8482,36 @@ var OpenCloudClient = class {
7855
8482
  }
7856
8483
  });
7857
8484
  }
8485
+ async listMonetizationItems(kind, universeId, pageToken) {
8486
+ const api = MONETIZATION_APIS[kind];
8487
+ const response = await this.request(`${api.collectionPath(universeId)}/creator`, {
8488
+ params: { pageToken }
8489
+ });
8490
+ const rows = readField(response, api.listField);
8491
+ if (!Array.isArray(rows)) {
8492
+ throw new Error(`Open Cloud returned a ${api.label} list without ${api.listField}.`);
8493
+ }
8494
+ const nextPageToken = readField(response, "nextPageToken");
8495
+ return {
8496
+ items: rows.map((row) => normalizeMonetizationItem(kind, universeId, row)),
8497
+ ...typeof nextPageToken === "string" && nextPageToken ? { nextPageToken } : {}
8498
+ };
8499
+ }
8500
+ async getMonetizationItem(kind, universeId, id) {
8501
+ const path11 = `${MONETIZATION_APIS[kind].collectionPath(universeId)}/${id}/creator`;
8502
+ return normalizeMonetizationItem(kind, universeId, await this.request(path11));
8503
+ }
8504
+ async createMonetizationItem(kind, universeId, item) {
8505
+ const response = await this.requestMultipart(MONETIZATION_APIS[kind].collectionPath(universeId), monetizationForm(item));
8506
+ return normalizeMonetizationItem(kind, universeId, response);
8507
+ }
8508
+ /** Changes only the given fields. Roblox answers with an empty 204. */
8509
+ async updateMonetizationItem(kind, universeId, id, changes) {
8510
+ const path11 = `${MONETIZATION_APIS[kind].collectionPath(universeId)}/${id}`;
8511
+ await this.sendMultipart(path11, monetizationForm(changes), "PATCH", async (response) => {
8512
+ await response.arrayBuffer();
8513
+ });
8514
+ }
7858
8515
  async getAssetThumbnail(assetId, size = "420x420") {
7859
8516
  const url = `https://thumbnails.roblox.com/v1/assets?assetIds=${assetId}&size=${size}&format=Png`;
7860
8517
  try {
@@ -7904,7 +8561,7 @@ var OpenCloudClient = class {
7904
8561
  }
7905
8562
  async downloadAudioAssetContent(assetId, maxBytes) {
7906
8563
  if (!this.apiKey) {
7907
- throw new Error("Open Cloud API key not configured. Set ROBLOX_OPEN_CLOUD_API_KEY with asset:read permission to download audio previews.");
8564
+ throw new Error("Open Cloud API key not configured. Set ROBLOX_OPEN_CLOUD_API_KEY with the legacy-asset:manage scope to download audio previews.");
7908
8565
  }
7909
8566
  if (!Number.isSafeInteger(assetId) || assetId <= 0) {
7910
8567
  throw new Error("Audio asset ID must be a positive integer.");
@@ -7922,7 +8579,8 @@ var OpenCloudClient = class {
7922
8579
  signal: controller.signal
7923
8580
  });
7924
8581
  if (!deliveryResponse.ok) {
7925
- throw new Error(`Roblox asset delivery request failed (${deliveryResponse.status}).`);
8582
+ const scopeHint = deliveryResponse.status === 403 ? " The Open Cloud API key lacks the legacy-asset:manage scope required to download audio." : "";
8583
+ throw new Error(`Roblox asset delivery request failed (${deliveryResponse.status}).${scopeHint}`);
7926
8584
  }
7927
8585
  const delivery = await deliveryResponse.json();
7928
8586
  if (!delivery.location) {
@@ -8016,7 +8674,11 @@ var OpenCloudClient = class {
8016
8674
  }
8017
8675
  return mimeTypes[ext];
8018
8676
  }
8019
- async requestMultipart(endpoint, formData) {
8677
+ requestMultipart(endpoint, formData) {
8678
+ return this.sendMultipart(endpoint, formData, "POST", async (response) => await response.json());
8679
+ }
8680
+ // readResponse runs inside the deadline, so a stalled body still times out.
8681
+ async sendMultipart(endpoint, formData, method, readResponse) {
8020
8682
  if (!this.apiKey) {
8021
8683
  throw new Error("Open Cloud API key not configured. Set ROBLOX_OPEN_CLOUD_API_KEY environment variable.");
8022
8684
  }
@@ -8025,31 +8687,14 @@ var OpenCloudClient = class {
8025
8687
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
8026
8688
  try {
8027
8689
  const response = await fetch(url, {
8028
- method: "POST",
8690
+ method,
8029
8691
  headers: { "x-api-key": this.apiKey },
8030
8692
  body: formData,
8031
8693
  signal: controller.signal
8032
8694
  });
8033
- if (!response.ok) {
8034
- const errorBody = await response.text();
8035
- let errorMessage2;
8036
- try {
8037
- const errorJson = JSON.parse(errorBody);
8038
- errorMessage2 = errorJson.detail || errorJson.message || errorBody;
8039
- } catch {
8040
- errorMessage2 = errorBody;
8041
- }
8042
- if (response.status === 401) {
8043
- throw new Error("Invalid or expired API key");
8044
- } else if (response.status === 403) {
8045
- throw new Error(`API key lacks required permissions: ${errorMessage2}`);
8046
- } else if (response.status === 429) {
8047
- throw new Error("Rate limit exceeded. Please try again later.");
8048
- } else {
8049
- throw new Error(`Open Cloud API error (${response.status}): ${errorMessage2}`);
8050
- }
8051
- }
8052
- return await response.json();
8695
+ if (!response.ok)
8696
+ throw await openCloudResponseError(response);
8697
+ return await readResponse(response);
8053
8698
  } catch (error) {
8054
8699
  if (controller.signal.aborted)
8055
8700
  throw new Error("Request timed out");
@@ -8247,9 +8892,54 @@ var RobloxCookieClient = class {
8247
8892
  };
8248
8893
 
8249
8894
  // ../core/dist/image-decode.js
8250
- import * as fs2 from "fs";
8251
- import * as path4 from "path";
8252
8895
  import { inflateSync } from "zlib";
8896
+
8897
+ // ../core/dist/local-file.js
8898
+ import * as fs2 from "fs";
8899
+ import * as path6 from "path";
8900
+ function errorCode(error) {
8901
+ return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
8902
+ }
8903
+ function readRegularFileWithinLimit(filePath, argumentName, validateSize) {
8904
+ let descriptor;
8905
+ try {
8906
+ const flags = fs2.constants.O_RDONLY | (fs2.constants.O_NONBLOCK ?? 0);
8907
+ descriptor = fs2.openSync(path6.resolve(filePath), flags);
8908
+ } catch (error) {
8909
+ const code = errorCode(error);
8910
+ if (code === "ENOENT") {
8911
+ throw new Error(`${argumentName} not found: ${filePath}`);
8912
+ }
8913
+ if (code === "EISDIR") {
8914
+ throw new Error(`${argumentName} must reference a regular file: ${filePath}`);
8915
+ }
8916
+ throw error;
8917
+ }
8918
+ try {
8919
+ const stats = fs2.fstatSync(descriptor);
8920
+ if (!stats.isFile()) {
8921
+ throw new Error(`${argumentName} must reference a regular file: ${filePath}`);
8922
+ }
8923
+ validateSize(stats.size);
8924
+ const data = Buffer.allocUnsafe(stats.size + 1);
8925
+ let offset = 0;
8926
+ while (offset < data.length) {
8927
+ const bytesRead = fs2.readSync(descriptor, data, offset, data.length - offset, offset);
8928
+ if (bytesRead === 0)
8929
+ break;
8930
+ offset += bytesRead;
8931
+ }
8932
+ if (offset !== stats.size) {
8933
+ validateSize(offset);
8934
+ throw new Error(`${argumentName} size changed while reading: ${filePath}`);
8935
+ }
8936
+ return data.subarray(0, offset);
8937
+ } finally {
8938
+ fs2.closeSync(descriptor);
8939
+ }
8940
+ }
8941
+
8942
+ // ../core/dist/image-decode.js
8253
8943
  var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
8254
8944
  var MAX_EDITABLE_IMAGE_DIMENSION = 1024;
8255
8945
  var MAX_PNG_DIMENSION = 16 * 1024;
@@ -8313,40 +9003,6 @@ function getBase64DecodedLength(encoded) {
8313
9003
  }
8314
9004
  return decodedLength;
8315
9005
  }
8316
- function readPngFileWithinLimit(resolved, imagePath) {
8317
- let descriptor;
8318
- try {
8319
- const flags = fs2.constants.O_RDONLY | (fs2.constants.O_NONBLOCK ?? 0);
8320
- descriptor = fs2.openSync(resolved, flags);
8321
- } catch (error) {
8322
- if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
8323
- throw new Error(`image_path not found: ${imagePath}`);
8324
- }
8325
- throw error;
8326
- }
8327
- try {
8328
- const stats = fs2.fstatSync(descriptor);
8329
- if (!stats.isFile()) {
8330
- throw new Error(`image_path must reference a regular file: ${imagePath}`);
8331
- }
8332
- validatePngInputSize(stats.size);
8333
- const data = Buffer.allocUnsafe(stats.size + 1);
8334
- let offset = 0;
8335
- while (offset < data.length) {
8336
- const bytesRead = fs2.readSync(descriptor, data, offset, data.length - offset, offset);
8337
- if (bytesRead === 0)
8338
- break;
8339
- offset += bytesRead;
8340
- }
8341
- if (offset !== stats.size) {
8342
- validatePngInputSize(offset);
8343
- throw new Error(`image_path size changed while reading: ${imagePath}`);
8344
- }
8345
- return data.subarray(0, offset);
8346
- } finally {
8347
- fs2.closeSync(descriptor);
8348
- }
8349
- }
8350
9006
  function validateSourceDimensions(width, height, maxPixels) {
8351
9007
  if (width <= 0 || height <= 0) {
8352
9008
  throw new Error("PNG IHDR dimensions must be positive.");
@@ -8580,14 +9236,13 @@ function decodePngBase64ToRgba(encoded) {
8580
9236
  return decodePngToRgba(data);
8581
9237
  }
8582
9238
  function decodeImagePathToRgba(imagePath) {
8583
- const resolved = path4.resolve(imagePath);
8584
- return decodePngToRgba(readPngFileWithinLimit(resolved, imagePath));
9239
+ return decodePngToRgba(readRegularFileWithinLimit(imagePath, "image_path", validatePngInputSize));
8585
9240
  }
8586
9241
 
8587
9242
  // ../core/dist/studio-skills.js
8588
9243
  import { createHash as createHash3 } from "crypto";
8589
- import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
8590
- import * as path5 from "path";
9244
+ import { existsSync as existsSync3, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
9245
+ import * as path7 from "path";
8591
9246
 
8592
9247
  // ../../node_modules/fzstd/esm/index.mjs
8593
9248
  var ab = ArrayBuffer;
@@ -9480,12 +10135,12 @@ function discoverNamedFile(root, fileName, depth = 0) {
9480
10135
  const matches = [];
9481
10136
  let entries;
9482
10137
  try {
9483
- entries = readdirSync2(root, { withFileTypes: true });
10138
+ entries = readdirSync3(root, { withFileTypes: true });
9484
10139
  } catch {
9485
10140
  return matches;
9486
10141
  }
9487
10142
  for (const entry of entries) {
9488
- const entryPath = path5.join(root, entry.name);
10143
+ const entryPath = path7.join(root, entry.name);
9489
10144
  if (entry.isFile() && entry.name.toLowerCase() === fileName.toLowerCase()) {
9490
10145
  matches.push(entryPath);
9491
10146
  } else if (entry.isDirectory()) {
@@ -9502,13 +10157,13 @@ function resolveAssistantBundlePath(studioExe) {
9502
10157
  }
9503
10158
  return override;
9504
10159
  }
9505
- const exeDirectory = path5.dirname(studioExe ?? resolveStudioExe());
10160
+ const exeDirectory = path7.dirname(studioExe ?? resolveStudioExe());
9506
10161
  const roots = [
9507
- path5.join(exeDirectory, "BuiltInStandalonePlugins"),
9508
- path5.resolve(exeDirectory, "..", "Resources", "BuiltInStandalonePlugins"),
9509
- path5.resolve(exeDirectory, "..", "..", "OTAPlugins")
10162
+ path7.join(exeDirectory, "BuiltInStandalonePlugins"),
10163
+ path7.resolve(exeDirectory, "..", "Resources", "BuiltInStandalonePlugins"),
10164
+ path7.resolve(exeDirectory, "..", "..", "OTAPlugins")
9510
10165
  ];
9511
- const direct = path5.join(exeDirectory, "BuiltInStandalonePlugins", "Optimized_Embedded_Signature", "Assistant.rbxm");
10166
+ const direct = path7.join(exeDirectory, "BuiltInStandalonePlugins", "Optimized_Embedded_Signature", "Assistant.rbxm");
9512
10167
  const candidates = /* @__PURE__ */ new Set();
9513
10168
  if (existsSync3(direct))
9514
10169
  candidates.add(direct);
@@ -9516,7 +10171,7 @@ function resolveAssistantBundlePath(studioExe) {
9516
10171
  for (const candidate of discoverNamedFile(root, "Assistant.rbxm"))
9517
10172
  candidates.add(candidate);
9518
10173
  }
9519
- const newest = [...candidates].map((candidate) => ({ candidate, modifiedAt: statSync2(candidate).mtimeMs })).sort((left, right) => right.modifiedAt - left.modifiedAt)[0]?.candidate;
10174
+ const newest = [...candidates].map((candidate) => ({ candidate, modifiedAt: statSync3(candidate).mtimeMs })).sort((left, right) => right.modifiedAt - left.modifiedAt)[0]?.candidate;
9520
10175
  if (!newest) {
9521
10176
  throw new Error(`Studio Assistant bundle not found for ${studioExe}. Set ROBLOX_STUDIO_ASSISTANT_BUNDLE to the installed Assistant.rbxm path.`);
9522
10177
  }
@@ -9524,16 +10179,16 @@ function resolveAssistantBundlePath(studioExe) {
9524
10179
  }
9525
10180
  var cachedBundle;
9526
10181
  function loadBuiltInStudioSkills(bundlePath = resolveAssistantBundlePath()) {
9527
- const stats = statSync2(bundlePath);
10182
+ const stats = statSync3(bundlePath);
9528
10183
  if (cachedBundle?.path === bundlePath && cachedBundle.modifiedAt === stats.mtimeMs && cachedBundle.size === stats.size) {
9529
10184
  return cachedBundle.value;
9530
10185
  }
9531
- const buffer = readFileSync4(bundlePath);
10186
+ const buffer = readFileSync5(bundlePath);
9532
10187
  const skills = parseBuiltInStudioSkills(buffer);
9533
10188
  if (skills.length === 0) {
9534
10189
  throw new Error(`No built-in skill documents found in ${bundlePath}`);
9535
10190
  }
9536
- const studioVersion = bundlePath.split(path5.sep).find((segment) => /^version-[a-z0-9]+$/i.test(segment));
10191
+ const studioVersion = bundlePath.split(path7.sep).find((segment) => /^version-[a-z0-9]+$/i.test(segment));
9537
10192
  const value = {
9538
10193
  bundlePath,
9539
10194
  bundleModifiedAt: stats.mtime.toISOString(),
@@ -9549,6 +10204,314 @@ function findBuiltInStudioSkill(bundle, requestedName) {
9549
10204
  return bundle.skills.find((skill) => skill.name.toLowerCase() === wanted || skill.sourceName.toLowerCase() === wanted);
9550
10205
  }
9551
10206
 
10207
+ // ../core/dist/tools/monetization.js
10208
+ var MAX_PRICE_ROBUX = 1e9;
10209
+ var MAX_NAME_SCAN_PAGES = 200;
10210
+ var MAX_ICON_BYTES = 10 * 1024 * 1024;
10211
+ var PNG_SIGNATURE2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
10212
+ var SCOPE_PREFIX = {
10213
+ developer_product: "developer-product",
10214
+ game_pass: "game-pass"
10215
+ };
10216
+ var ACTION_FIELDS = {
10217
+ list: ["page_token"],
10218
+ get: ["id"],
10219
+ create: ["name", "description", "price", "for_sale", "managed_pricing", "image_path"],
10220
+ update: ["id", "name", "description", "price", "for_sale", "managed_pricing", "image_path"]
10221
+ };
10222
+ var ACTION_SCOPED_FIELDS = [
10223
+ "id",
10224
+ "page_token",
10225
+ "name",
10226
+ "description",
10227
+ "price",
10228
+ "for_sale",
10229
+ "managed_pricing",
10230
+ "image_path"
10231
+ ];
10232
+ var MonetizationError = class extends Error {
10233
+ code;
10234
+ details;
10235
+ constructor(code, message, details = {}) {
10236
+ super(message);
10237
+ this.code = code;
10238
+ this.details = details;
10239
+ this.name = "MonetizationError";
10240
+ }
10241
+ };
10242
+ function invalidArgument(message) {
10243
+ return new MonetizationError("invalid_argument", message);
10244
+ }
10245
+ function isAction(value) {
10246
+ return value === "list" || value === "get" || value === "create" || value === "update";
10247
+ }
10248
+ function isKind(value) {
10249
+ return value === "developer_product" || value === "game_pass";
10250
+ }
10251
+ function isPresent(value) {
10252
+ return value !== void 0 && value !== null;
10253
+ }
10254
+ function optionalId(value, name) {
10255
+ if (!isPresent(value))
10256
+ return void 0;
10257
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
10258
+ throw invalidArgument(`${name} must be a positive whole number.`);
10259
+ }
10260
+ return value;
10261
+ }
10262
+ function optionalPrice(value) {
10263
+ if (!isPresent(value))
10264
+ return void 0;
10265
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > MAX_PRICE_ROBUX) {
10266
+ throw invalidArgument(`price must be a whole number of Robux from 1 to ${MAX_PRICE_ROBUX}.`);
10267
+ }
10268
+ return value;
10269
+ }
10270
+ function optionalString(value, name) {
10271
+ if (!isPresent(value))
10272
+ return void 0;
10273
+ if (typeof value !== "string")
10274
+ throw invalidArgument(`${name} must be a string.`);
10275
+ return value;
10276
+ }
10277
+ function optionalNonBlankString(value, name) {
10278
+ const text = optionalString(value, name);
10279
+ if (text !== void 0 && text.trim() === "")
10280
+ throw invalidArgument(`${name} must not be empty.`);
10281
+ return text;
10282
+ }
10283
+ function optionalBoolean(value, name) {
10284
+ if (!isPresent(value))
10285
+ return void 0;
10286
+ if (typeof value !== "boolean")
10287
+ throw invalidArgument(`${name} must be true or false.`);
10288
+ return value;
10289
+ }
10290
+ function parseRequest(request) {
10291
+ const { action, kind } = request;
10292
+ if (!isAction(action))
10293
+ throw invalidArgument("action must be list, get, create, or update.");
10294
+ if (!isKind(kind))
10295
+ throw invalidArgument("kind must be developer_product or game_pass.");
10296
+ for (const field of ACTION_SCOPED_FIELDS) {
10297
+ if (isPresent(request[field]) && !ACTION_FIELDS[action].includes(field)) {
10298
+ throw invalidArgument(`${field} is not used by action=${action}.`);
10299
+ }
10300
+ }
10301
+ const parsed = {
10302
+ action,
10303
+ kind,
10304
+ universeId: optionalId(request.universe_id, "universe_id"),
10305
+ id: optionalId(request.id, "id"),
10306
+ pageToken: optionalNonBlankString(request.page_token, "page_token"),
10307
+ name: optionalNonBlankString(request.name, "name")?.trim(),
10308
+ description: optionalString(request.description, "description"),
10309
+ price: optionalPrice(request.price),
10310
+ forSale: optionalBoolean(request.for_sale, "for_sale"),
10311
+ managedPricing: optionalBoolean(request.managed_pricing, "managed_pricing"),
10312
+ imagePath: optionalNonBlankString(request.image_path, "image_path")
10313
+ };
10314
+ if (action === "get" || action === "update")
10315
+ requiredId(parsed);
10316
+ if (action === "create") {
10317
+ requiredName(parsed);
10318
+ if (parsed.forSale === true && parsed.price === void 0) {
10319
+ throw invalidArgument("price is required to create an item with for_sale=true.");
10320
+ }
10321
+ }
10322
+ if (action === "update" && Object.keys(itemChanges(parsed)).length === 0 && parsed.imagePath === void 0) {
10323
+ throw invalidArgument("update needs at least one of name, description, price, for_sale, managed_pricing, or image_path.");
10324
+ }
10325
+ return parsed;
10326
+ }
10327
+ function requiredId(request) {
10328
+ if (request.id === void 0)
10329
+ throw invalidArgument(`id is required for action=${request.action}.`);
10330
+ return request.id;
10331
+ }
10332
+ function requiredName(request) {
10333
+ if (request.name === void 0)
10334
+ throw invalidArgument(`name is required for action=${request.action}.`);
10335
+ return request.name;
10336
+ }
10337
+ function itemChanges(request) {
10338
+ return {
10339
+ ...request.name !== void 0 ? { name: request.name } : {},
10340
+ ...request.description !== void 0 ? { description: request.description } : {},
10341
+ ...request.price !== void 0 ? { price: request.price } : {},
10342
+ ...request.forSale !== void 0 ? { forSale: request.forSale } : {},
10343
+ ...request.managedPricing !== void 0 ? { managedPricing: request.managedPricing } : {}
10344
+ };
10345
+ }
10346
+ function itemLabel(kind) {
10347
+ return kind === "developer_product" ? "developer product" : "game pass";
10348
+ }
10349
+ function comparableName(name) {
10350
+ return name.trim().toLowerCase();
10351
+ }
10352
+ async function findItemNamed(client, kind, universeId, name) {
10353
+ const wanted = comparableName(name);
10354
+ let pageToken;
10355
+ for (let page = 0; page < MAX_NAME_SCAN_PAGES; page++) {
10356
+ const result = await client.listMonetizationItems(kind, universeId, pageToken);
10357
+ const match = result.items.find((candidate) => comparableName(candidate.name) === wanted);
10358
+ if (match)
10359
+ return match;
10360
+ if (!result.nextPageToken)
10361
+ return void 0;
10362
+ pageToken = result.nextPageToken;
10363
+ }
10364
+ throw new MonetizationError("duplicate_check_incomplete", `Stopped looking for an existing ${itemLabel(kind)} named "${name}" after ${MAX_NAME_SCAN_PAGES} pages, so nothing was created.`);
10365
+ }
10366
+ function requiredScopes(request) {
10367
+ const prefix = SCOPE_PREFIX[request.kind];
10368
+ return request.action === "list" || request.action === "get" ? [`${prefix}:read`] : [`${prefix}:read`, `${prefix}:write`];
10369
+ }
10370
+ function publicItem(item) {
10371
+ return {
10372
+ kind: item.kind,
10373
+ id: item.id,
10374
+ universe_id: item.universeId,
10375
+ name: item.name,
10376
+ description: item.description,
10377
+ for_sale: item.forSale,
10378
+ price: item.price,
10379
+ managed_pricing: item.managedPricing,
10380
+ icon_asset_id: item.iconAssetId,
10381
+ ...item.createdAt !== void 0 ? { created_at: item.createdAt } : {},
10382
+ ...item.updatedAt !== void 0 ? { updated_at: item.updatedAt } : {},
10383
+ ...item.immutable !== void 0 ? { immutable: item.immutable } : {}
10384
+ };
10385
+ }
10386
+ async function resolveUniverse(request, context) {
10387
+ if (request.universeId !== void 0)
10388
+ return request.universeId;
10389
+ const universeId = await context.connectedUniverseId();
10390
+ if (!Number.isSafeInteger(universeId) || universeId <= 0) {
10391
+ throw new MonetizationError("unpublished_place", "The connected Studio place is not published, so it has no universe. Publish it or pass universe_id.");
10392
+ }
10393
+ return universeId;
10394
+ }
10395
+ function iconType(data) {
10396
+ if (data.length >= PNG_SIGNATURE2.length && data.subarray(0, PNG_SIGNATURE2.length).equals(PNG_SIGNATURE2)) {
10397
+ return { mimeType: "image/png", extension: "png" };
10398
+ }
10399
+ if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) {
10400
+ return { mimeType: "image/jpeg", extension: "jpg" };
10401
+ }
10402
+ if (data.length >= 2 && data[0] === 66 && data[1] === 77) {
10403
+ return { mimeType: "image/bmp", extension: "bmp" };
10404
+ }
10405
+ return void 0;
10406
+ }
10407
+ function readIcon(imagePath) {
10408
+ let data;
10409
+ try {
10410
+ data = readRegularFileWithinLimit(imagePath, "image_path", (byteLength) => {
10411
+ if (byteLength > MAX_ICON_BYTES) {
10412
+ throw new Error(`image_path is ${byteLength} bytes; icons are limited to ${MAX_ICON_BYTES} bytes.`);
10413
+ }
10414
+ });
10415
+ } catch (error) {
10416
+ throw invalidArgument(error instanceof Error ? error.message : String(error));
10417
+ }
10418
+ const type = iconType(data);
10419
+ if (!type)
10420
+ throw invalidArgument("image_path must be a PNG, JPEG, or BMP file.");
10421
+ return { data, fileName: `icon.${type.extension}`, mimeType: type.mimeType };
10422
+ }
10423
+ async function runAction(request, universeId, client, icon) {
10424
+ const { kind } = request;
10425
+ const changes = { ...itemChanges(request), ...icon ? { icon } : {} };
10426
+ switch (request.action) {
10427
+ case "list": {
10428
+ const page = await client.listMonetizationItems(kind, universeId, request.pageToken);
10429
+ return {
10430
+ kind,
10431
+ universe_id: universeId,
10432
+ items: page.items.map(publicItem),
10433
+ ...page.nextPageToken ? { next_page_token: page.nextPageToken } : {}
10434
+ };
10435
+ }
10436
+ case "get":
10437
+ return { item: publicItem(await client.getMonetizationItem(kind, universeId, requiredId(request))) };
10438
+ case "create": {
10439
+ const name = requiredName(request);
10440
+ const existing = await findItemNamed(client, kind, universeId, name);
10441
+ if (existing) {
10442
+ throw new MonetizationError("duplicate_name", `A ${itemLabel(kind)} named "${existing.name}" already exists in universe ${universeId}. Update it or choose another name.`, { existing: publicItem(existing) });
10443
+ }
10444
+ const created = await client.createMonetizationItem(kind, universeId, {
10445
+ ...changes,
10446
+ name,
10447
+ forSale: request.forSale ?? false
10448
+ });
10449
+ return { created: true, item: publicItem(created) };
10450
+ }
10451
+ case "update": {
10452
+ const id = requiredId(request);
10453
+ await client.updateMonetizationItem(kind, universeId, id, changes);
10454
+ try {
10455
+ return { updated: true, item: publicItem(await client.getMonetizationItem(kind, universeId, id)) };
10456
+ } catch (error) {
10457
+ const reason = error instanceof Error ? error.message : String(error);
10458
+ return {
10459
+ updated: true,
10460
+ item: null,
10461
+ warnings: [`The update succeeded, but reading the item back failed: ${reason}`]
10462
+ };
10463
+ }
10464
+ }
10465
+ }
10466
+ }
10467
+ function openCloudFailure(error, request) {
10468
+ const { message, status } = error;
10469
+ switch (status) {
10470
+ case 401:
10471
+ return { error: "unauthorized", message, hint: "Check ROBLOX_OPEN_CLOUD_API_KEY." };
10472
+ case 403:
10473
+ return {
10474
+ error: "forbidden",
10475
+ message,
10476
+ required_scopes: requiredScopes(request),
10477
+ hint: "Grant these scopes to the API key and add this experience to it."
10478
+ };
10479
+ case 404:
10480
+ return { error: "not_found", message, hint: "Check id, kind, and universe_id." };
10481
+ case 409:
10482
+ return { error: "conflict", message };
10483
+ case 429:
10484
+ return { error: "rate_limited", message, hint: "Wait before retrying." };
10485
+ default: {
10486
+ if (status >= 400 && status < 500) {
10487
+ const { field, hint } = error.details;
10488
+ return { error: "invalid_request", message, ...field ? { field } : {}, ...hint ? { hint } : {} };
10489
+ }
10490
+ return { error: "open_cloud_error", message, status };
10491
+ }
10492
+ }
10493
+ }
10494
+ async function runManageMonetization(request, context) {
10495
+ let parsed;
10496
+ try {
10497
+ parsed = parseRequest(request);
10498
+ if (!context.client.hasApiKey()) {
10499
+ const scopes = requiredScopes(parsed);
10500
+ throw new MonetizationError("missing_api_key", `ROBLOX_OPEN_CLOUD_API_KEY is not set. Create an Open Cloud API key with ${scopes.join(" and ")} for this experience.`, { required_scopes: scopes });
10501
+ }
10502
+ const icon = parsed.imagePath === void 0 ? void 0 : readIcon(parsed.imagePath);
10503
+ const universeId = await resolveUniverse(parsed, context);
10504
+ return await runAction(parsed, universeId, context.client, icon);
10505
+ } catch (error) {
10506
+ if (error instanceof MonetizationError) {
10507
+ return { error: error.code, message: error.message, ...error.details };
10508
+ }
10509
+ if (error instanceof OpenCloudRequestError && parsed)
10510
+ return openCloudFailure(error, parsed);
10511
+ throw error;
10512
+ }
10513
+ }
10514
+
9552
10515
  // ../core/dist/jpeg-encoder.js
9553
10516
  var ZIGZAG = [
9554
10517
  0,
@@ -10502,7 +11465,7 @@ function rgbaToJpeg(rgba, width, height, quality = 80) {
10502
11465
 
10503
11466
  // ../core/dist/png-encoder.js
10504
11467
  import { deflateSync } from "zlib";
10505
- var PNG_SIGNATURE2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
11468
+ var PNG_SIGNATURE3 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
10506
11469
  var CRC_TABLE = new Uint32Array(256);
10507
11470
  for (let n = 0; n < 256; n++) {
10508
11471
  let c = n;
@@ -10546,7 +11509,7 @@ function rgbaToPng(rgba, width, height) {
10546
11509
  ihdr[11] = 0;
10547
11510
  ihdr[12] = 0;
10548
11511
  return Buffer.concat([
10549
- PNG_SIGNATURE2,
11512
+ PNG_SIGNATURE3,
10550
11513
  writeChunk("IHDR", ihdr),
10551
11514
  writeChunk("IDAT", deflateSync(filtered)),
10552
11515
  writeChunk("IEND", Buffer.alloc(0))
@@ -10554,16 +11517,16 @@ function rgbaToPng(rgba, width, height) {
10554
11517
  }
10555
11518
 
10556
11519
  // ../core/dist/host-capture.js
10557
- import { spawn as spawn3 } from "child_process";
11520
+ import { spawn as spawn4 } from "child_process";
10558
11521
  import * as fs4 from "fs";
10559
11522
  import * as os4 from "os";
10560
- import * as path7 from "path";
11523
+ import * as path9 from "path";
10561
11524
 
10562
11525
  // ../core/dist/host-capture-macos.js
10563
- import { spawn as spawn2 } from "child_process";
11526
+ import { spawn as spawn3 } from "child_process";
10564
11527
  import * as fs3 from "fs";
10565
11528
  import * as os3 from "os";
10566
- import * as path6 from "path";
11529
+ import * as path8 from "path";
10567
11530
  var MAX_IMAGE_BYTES = 128 * 1024 * 1024;
10568
11531
  var MAX_OUTPUT_BYTES = 64 * 1024;
10569
11532
  var STUDIO_BUNDLE_ID = "com.Roblox.RobloxStudio";
@@ -10672,7 +11635,7 @@ func capture() async throws {
10672
11635
  function runBoundedCaptureProcess(executable, args, timeoutMs, env = process.env) {
10673
11636
  return new Promise((resolve6, reject) => {
10674
11637
  const processGroup = process.platform !== "win32";
10675
- const child = spawn2(executable, args, { env, stdio: ["ignore", "pipe", "pipe"], detached: processGroup });
11638
+ const child = spawn3(executable, args, { env, stdio: ["ignore", "pipe", "pipe"], detached: processGroup });
10676
11639
  let stdout = "";
10677
11640
  let stderr = "";
10678
11641
  let outputBytes = 0;
@@ -10729,10 +11692,10 @@ async function assertSwiftToolchain(env = process.env, selectedDeveloperDir = ()
10729
11692
  }
10730
11693
  }
10731
11694
  const compilers = [
10732
- path6.join(developerDir, "usr", "bin", "swiftc"),
10733
- path6.join(developerDir, "Toolchains", "XcodeDefault.xctoolchain", "usr", "bin", "swiftc")
11695
+ path8.join(developerDir, "usr", "bin", "swiftc"),
11696
+ path8.join(developerDir, "Toolchains", "XcodeDefault.xctoolchain", "usr", "bin", "swiftc")
10734
11697
  ];
10735
- if (!path6.isAbsolute(developerDir) || !compilers.some((compiler) => exists(compiler))) {
11698
+ if (!path8.isAbsolute(developerDir) || !compilers.some((compiler) => exists(compiler))) {
10736
11699
  throw new Error(MISSING_SWIFT_TOOLCHAIN);
10737
11700
  }
10738
11701
  }
@@ -10741,11 +11704,11 @@ async function getHelper() {
10741
11704
  if (!compiledHelper) {
10742
11705
  compiledHelper = (async () => {
10743
11706
  await assertSwiftToolchain();
10744
- const directory = fs3.mkdtempSync(path6.join(os3.tmpdir(), "robloxstudio-mcp-macos-helper-"));
11707
+ const directory = fs3.mkdtempSync(path8.join(os3.tmpdir(), "robloxstudio-mcp-macos-helper-"));
10745
11708
  const cleanup = () => fs3.rmSync(directory, { recursive: true, force: true });
10746
11709
  try {
10747
- const source = path6.join(directory, "capture.swift");
10748
- const executable = path6.join(directory, "capture");
11710
+ const source = path8.join(directory, "capture.swift");
11711
+ const executable = path8.join(directory, "capture");
10749
11712
  fs3.writeFileSync(source, MACOS_CAPTURE_SOURCE, { mode: 384 });
10750
11713
  await runBoundedCaptureProcess("/usr/bin/xcrun", [
10751
11714
  "swiftc",
@@ -10754,7 +11717,7 @@ async function getHelper() {
10754
11717
  "-o",
10755
11718
  executable,
10756
11719
  "-module-cache-path",
10757
- path6.join(directory, "modules")
11720
+ path8.join(directory, "modules")
10758
11721
  ], 6e4);
10759
11722
  process.once("exit", cleanup);
10760
11723
  return executable;
@@ -10805,9 +11768,9 @@ async function runMacCapture(output, titleHint, expectedIdentity) {
10805
11768
  });
10806
11769
  }
10807
11770
  async function captureMacStudioWindow(titleHint, expectedIdentity, runCapture = runMacCapture) {
10808
- const directory = fs3.mkdtempSync(path6.join(os3.tmpdir(), "robloxstudio-mcp-macos-capture-"));
11771
+ const directory = fs3.mkdtempSync(path8.join(os3.tmpdir(), "robloxstudio-mcp-macos-capture-"));
10809
11772
  try {
10810
- const output = path6.join(directory, "capture.rgba");
11773
+ const output = path8.join(directory, "capture.rgba");
10811
11774
  const stdout = await runCapture(output, titleHint, expectedIdentity);
10812
11775
  const report = parseMacCaptureReport(stdout, expectedIdentity);
10813
11776
  if (!report.ok)
@@ -11073,7 +12036,7 @@ function parseWindowsCaptureReport(value, expectedIdentity) {
11073
12036
  function powershellPath() {
11074
12037
  const systemRoot = process.env.SystemRoot ?? process.env.windir;
11075
12038
  if (systemRoot) {
11076
- const candidate = path7.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
12039
+ const candidate = path9.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
11077
12040
  if (fs4.existsSync(candidate))
11078
12041
  return candidate;
11079
12042
  }
@@ -11082,7 +12045,7 @@ function powershellPath() {
11082
12045
  function runWindowsCapture(titleHint, outFile, expectedIdentity) {
11083
12046
  const { promise, resolve: resolve6, reject } = Promise.withResolvers();
11084
12047
  const encoded = Buffer.from(WINDOWS_CAPTURE_SCRIPT, "utf16le").toString("base64");
11085
- const child = spawn3(powershellPath(), ["-NoProfile", "-NonInteractive", "-NoLogo", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded], {
12048
+ const child = spawn4(powershellPath(), ["-NoProfile", "-NonInteractive", "-NoLogo", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded], {
11086
12049
  env: {
11087
12050
  ...process.env,
11088
12051
  MCP_CAPTURE_TITLE_HINT: titleHint ?? "",
@@ -11145,7 +12108,7 @@ async function captureStudioWindow(titleHint, expectedIdentity) {
11145
12108
  }
11146
12109
  if (process.platform === "darwin")
11147
12110
  return captureMacStudioWindow(titleHint, expectedIdentity);
11148
- const outFile = path7.join(os4.tmpdir(), `robloxstudio-mcp-capture-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.bgra`);
12111
+ const outFile = path9.join(os4.tmpdir(), `robloxstudio-mcp-capture-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.bgra`);
11149
12112
  try {
11150
12113
  const report = parseWindowsCaptureReport(await runWindowsCapture(titleHint, outFile, expectedIdentity), expectedIdentity);
11151
12114
  if (!report.ok)
@@ -11177,7 +12140,7 @@ async function captureStudioWindow(titleHint, expectedIdentity) {
11177
12140
 
11178
12141
  // ../core/dist/tools/index.js
11179
12142
  import * as fs5 from "fs";
11180
- import * as path8 from "path";
12143
+ import * as path10 from "path";
11181
12144
  function sameHostWindowIdentity(actual, expected) {
11182
12145
  return actual?.windowId === expected.windowId && actual.processId === expected.processId && actual.bundleIdentifier === expected.bundleIdentifier;
11183
12146
  }
@@ -11360,9 +12323,9 @@ function compactSoundReference(sound) {
11360
12323
  name: stringField(sound, "name"),
11361
12324
  className: stringField(sound, "className")
11362
12325
  };
11363
- const path9 = stringField(sound, "path");
11364
- if (path9)
11365
- compact.path = path9;
12326
+ const path11 = stringField(sound, "path");
12327
+ if (path11)
12328
+ compact.path = path11;
11366
12329
  const assetId = robloxAssetIdFromContentId(sound.assetId ?? sound.soundId ?? sound.asset);
11367
12330
  if (assetId !== void 0)
11368
12331
  compact.assetId = assetId;
@@ -11425,7 +12388,7 @@ function loadMicroProfilerBaseline(source, sourcePath) {
11425
12388
  if (typeof sourcePath !== "string" || sourcePath === "") {
11426
12389
  throw new Error("baseline_path must be a non-empty string when provided");
11427
12390
  }
11428
- const resolved = path8.resolve(sourcePath);
12391
+ const resolved = path10.resolve(sourcePath);
11429
12392
  const parsed = JSON.parse(fs5.readFileSync(resolved, "utf8"));
11430
12393
  const record = asRecord(parsed);
11431
12394
  if (!record)
@@ -12377,8 +13340,8 @@ var RobloxStudioTools = class {
12377
13340
  timedOut: true
12378
13341
  };
12379
13342
  }
12380
- async getFileTree(path9 = "", instance_id) {
12381
- const response = await this._callSingle("/api/file-tree", { path: path9 }, void 0, instance_id);
13343
+ async getFileTree(path11 = "", instance_id) {
13344
+ const response = await this._callSingle("/api/file-tree", { path: path11 }, void 0, instance_id);
12382
13345
  return {
12383
13346
  content: [
12384
13347
  {
@@ -12410,6 +13373,28 @@ var RobloxStudioTools = class {
12410
13373
  ]
12411
13374
  };
12412
13375
  }
13376
+ async manageMonetization(request, instance_id) {
13377
+ return this._textResult(await runManageMonetization(request, {
13378
+ client: this.openCloudClient,
13379
+ connectedUniverseId: () => this._connectedUniverseId(instance_id)
13380
+ }));
13381
+ }
13382
+ // Monetization APIs address a universe, which Studio reports as game.GameId.
13383
+ async _connectedUniverseId(instance_id) {
13384
+ let info;
13385
+ try {
13386
+ info = await this._callSingle("/api/place-info", {}, void 0, instance_id);
13387
+ } catch (error) {
13388
+ if (error instanceof RoutingFailure) {
13389
+ throw new RoutingFailure({
13390
+ ...error.routingError,
13391
+ message: `${error.routingError.message} Pass universe_id to skip Studio.`
13392
+ });
13393
+ }
13394
+ throw error;
13395
+ }
13396
+ return typeof info.gameId === "number" ? info.gameId : 0;
13397
+ }
12413
13398
  async searchObjects(query, searchType = "name", propertyName, instance_id) {
12414
13399
  const response = await this._callSingle("/api/search-objects", {
12415
13400
  query,
@@ -12470,9 +13455,9 @@ var RobloxStudioTools = class {
12470
13455
  ]
12471
13456
  };
12472
13457
  }
12473
- async getProjectStructure(path9, maxDepth, scriptsOnly, instance_id) {
13458
+ async getProjectStructure(path11, maxDepth, scriptsOnly, instance_id) {
12474
13459
  const response = await this._callSingle("/api/project-structure", {
12475
- path: path9,
13460
+ path: path11,
12476
13461
  maxDepth,
12477
13462
  scriptsOnly
12478
13463
  }, void 0, instance_id);
@@ -12633,7 +13618,7 @@ var RobloxStudioTools = class {
12633
13618
  };
12634
13619
  }
12635
13620
  async setSelection(paths, mode, instance_id) {
12636
- if (!Array.isArray(paths) || paths.some((path9) => typeof path9 !== "string" || path9.length === 0)) {
13621
+ if (!Array.isArray(paths) || paths.some((path11) => typeof path11 !== "string" || path11.length === 0)) {
12637
13622
  throw new Error("selection paths must contain only non-empty instance paths");
12638
13623
  }
12639
13624
  const selectionMode = mode ?? "set";
@@ -13370,8 +14355,8 @@ var RobloxStudioTools = class {
13370
14355
  const rawJson = mutable.raw_json;
13371
14356
  if (typeof rawJson === "string") {
13372
14357
  if (typeof outputPath === "string" && outputPath !== "") {
13373
- const resolvedOutputPath = path8.resolve(outputPath);
13374
- fs5.mkdirSync(path8.dirname(resolvedOutputPath), { recursive: true });
14358
+ const resolvedOutputPath = path10.resolve(outputPath);
14359
+ fs5.mkdirSync(path10.dirname(resolvedOutputPath), { recursive: true });
13375
14360
  fs5.writeFileSync(resolvedOutputPath, rawJson, "utf8");
13376
14361
  mutable.output_path = resolvedOutputPath;
13377
14362
  }
@@ -13433,8 +14418,8 @@ var RobloxStudioTools = class {
13433
14418
  const rawSnapshotBase64 = mutable.raw_snapshot_base64;
13434
14419
  if (typeof rawSnapshotBase64 === "string") {
13435
14420
  if (typeof outputPath === "string" && outputPath !== "") {
13436
- const resolvedOutputPath = path8.resolve(outputPath);
13437
- fs5.mkdirSync(path8.dirname(resolvedOutputPath), { recursive: true });
14421
+ const resolvedOutputPath = path10.resolve(outputPath);
14422
+ fs5.mkdirSync(path10.dirname(resolvedOutputPath), { recursive: true });
13438
14423
  fs5.writeFileSync(resolvedOutputPath, Buffer.from(rawSnapshotBase64, "base64"));
13439
14424
  mutable.output_path = resolvedOutputPath;
13440
14425
  }
@@ -13449,8 +14434,8 @@ var RobloxStudioTools = class {
13449
14434
  });
13450
14435
  }
13451
14436
  if (typeof summaryOutputPath === "string" && summaryOutputPath !== "") {
13452
- const resolvedSummaryPath = path8.resolve(summaryOutputPath);
13453
- fs5.mkdirSync(path8.dirname(resolvedSummaryPath), { recursive: true });
14437
+ const resolvedSummaryPath = path10.resolve(summaryOutputPath);
14438
+ fs5.mkdirSync(path10.dirname(resolvedSummaryPath), { recursive: true });
13454
14439
  fs5.writeFileSync(resolvedSummaryPath, JSON.stringify(mutable, null, 2), "utf8");
13455
14440
  mutable.summary_output_path = resolvedSummaryPath;
13456
14441
  }
@@ -13513,7 +14498,7 @@ var RobloxStudioTools = class {
13513
14498
  return record.placeId !== void 0 && instance.placeId === record.placeId;
13514
14499
  }
13515
14500
  if ((record.source === "baseplate" || record.source === "local_file") && record.localPlaceFile) {
13516
- const expectedName = path8.basename(record.localPlaceFile);
14501
+ const expectedName = path10.basename(record.localPlaceFile);
13517
14502
  return instance.placeName === expectedName || instance.dataModelName === expectedName;
13518
14503
  }
13519
14504
  return true;
@@ -14848,7 +15833,7 @@ var RobloxStudioTools = class {
14848
15833
  throw new Error(`File not found: ${filePath}`);
14849
15834
  }
14850
15835
  const fileContent = fs5.readFileSync(filePath);
14851
- const fileName = path8.basename(filePath);
15836
+ const fileName = path10.basename(filePath);
14852
15837
  const resolvedGroupId = groupId || process.env.ROBLOX_CREATOR_GROUP_ID;
14853
15838
  const resolvedUserId = userId || process.env.ROBLOX_CREATOR_USER_ID;
14854
15839
  if (assetType === "Decal" && this.cookieClient.hasCookie()) {
@@ -15064,9 +16049,9 @@ var RobloxStudioTools = class {
15064
16049
  return { content: [{ type: "text", text: JSON.stringify({ error: "plugin returned no base64 payload" }) }] };
15065
16050
  }
15066
16051
  const bytes = Buffer.from(response.base64, "base64");
15067
- const resolved = path8.resolve(outputPath);
16052
+ const resolved = path10.resolve(outputPath);
15068
16053
  try {
15069
- fs5.mkdirSync(path8.dirname(resolved), { recursive: true });
16054
+ fs5.mkdirSync(path10.dirname(resolved), { recursive: true });
15070
16055
  fs5.writeFileSync(resolved, bytes);
15071
16056
  } catch (err2) {
15072
16057
  return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err2.message}` }) }] };
@@ -15427,7 +16412,7 @@ var RobloxStudioTools = class {
15427
16412
  };
15428
16413
 
15429
16414
  // ../core/dist/proxy-bridge-service.js
15430
- import { randomUUID as randomUUID4 } from "crypto";
16415
+ import { randomUUID as randomUUID5 } from "crypto";
15431
16416
  var PROXY_RESPONSE_GRACE_MS = 5e3;
15432
16417
  function parseRequestStatus(value) {
15433
16418
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -15486,7 +16471,7 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
15486
16471
  super();
15487
16472
  this.primaryBaseUrl = primaryBaseUrl;
15488
16473
  this.authToken = authToken;
15489
- this.proxyInstanceId = randomUUID4();
16474
+ this.proxyInstanceId = randomUUID5();
15490
16475
  this.initialRefresh = this.refreshTopology();
15491
16476
  this.refreshTimer = setInterval(() => this.refreshTopology(), _ProxyBridgeService.REFRESH_INTERVAL_MS);
15492
16477
  }
@@ -15672,7 +16657,7 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
15672
16657
  return status;
15673
16658
  }
15674
16659
  async sendRequest(endpoint, data, targetPeerId, timeoutMs = this.proxyRequestTimeout, signal, operationId) {
15675
- const requestId = operationId ?? randomUUID4();
16660
+ const requestId = operationId ?? randomUUID5();
15676
16661
  const details = { requestId, targetPeerId, stage: "queued", outcome: "not_executed", executionOutcome: "not_executed" };
15677
16662
  if (typeof requestId !== "string" || requestId.trim().length === 0 || requestId.length > 128) {
15678
16663
  throw new RequestFailure("operationId must be a nonempty string of at most 128 characters", "invalid_operation_id", details);
@@ -17570,7 +18555,7 @@ var TOOL_DEFINITIONS = [
17570
18555
  includeAudio: {
17571
18556
  type: "boolean",
17572
18557
  default: true,
17573
- description: "Return inline audio; needs asset:read and never writes files."
18558
+ description: "Return inline audio; needs legacy-asset:manage and never writes files."
17574
18559
  },
17575
18560
  maxAudioPreviews: {
17576
18561
  type: "number",
@@ -17646,6 +18631,72 @@ var TOOL_DEFINITIONS = [
17646
18631
  }
17647
18632
  }
17648
18633
  },
18634
+ // === Monetization ===
18635
+ {
18636
+ name: "manage_monetization",
18637
+ category: "write",
18638
+ description: "Use to list, inspect, create, or update developer products and game passes.",
18639
+ inputSchema: {
18640
+ type: "object",
18641
+ properties: {
18642
+ action: {
18643
+ type: "string",
18644
+ enum: ["list", "get", "create", "update"],
18645
+ description: "Operation; create and update change live Roblox items."
18646
+ },
18647
+ kind: {
18648
+ type: "string",
18649
+ enum: ["developer_product", "game_pass"],
18650
+ description: "Item type."
18651
+ },
18652
+ universe_id: {
18653
+ type: "integer",
18654
+ minimum: 1,
18655
+ description: "Universe ID; defaults to the connected Studio place."
18656
+ },
18657
+ id: {
18658
+ type: "integer",
18659
+ minimum: 1,
18660
+ description: "Product or pass ID; required for get and update."
18661
+ },
18662
+ name: {
18663
+ type: "string",
18664
+ description: "Item name; required for create."
18665
+ },
18666
+ description: {
18667
+ type: "string",
18668
+ description: "Item description shown to buyers."
18669
+ },
18670
+ price: {
18671
+ type: "integer",
18672
+ minimum: 1,
18673
+ maximum: 1e9,
18674
+ description: "Default price in Robux."
18675
+ },
18676
+ for_sale: {
18677
+ type: "boolean",
18678
+ description: "Purchasable; create defaults to false."
18679
+ },
18680
+ managed_pricing: {
18681
+ type: "boolean",
18682
+ description: "Let Roblox price optimization set the price."
18683
+ },
18684
+ image_path: {
18685
+ type: "string",
18686
+ description: "Local PNG, JPEG, or BMP icon file."
18687
+ },
18688
+ page_token: {
18689
+ type: "string",
18690
+ description: "Prior list page token."
18691
+ },
18692
+ instance_id: {
18693
+ type: "string",
18694
+ description: "Studio process ID when ambiguous."
18695
+ }
18696
+ },
18697
+ required: ["action", "kind"]
18698
+ }
18699
+ },
17649
18700
  // === Input Simulation ===
17650
18701
  {
17651
18702
  name: "simulate_mouse_input",
@@ -17944,10 +18995,10 @@ var getAllTools = () => [...TOOL_DEFINITIONS];
17944
18995
 
17945
18996
  // ../core/dist/install-plugin-helpers.js
17946
18997
  var import_saxes = __toESM(require_saxes(), 1);
17947
- import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync, rmSync as rmSync4, rmdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
18998
+ import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync9, renameSync, rmSync as rmSync5, rmdirSync, statSync as statSync4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
17948
18999
  import { execSync } from "child_process";
17949
- import { randomUUID as randomUUID5 } from "crypto";
17950
- import { basename as basename3, join as join8 } from "path";
19000
+ import { randomUUID as randomUUID6 } from "crypto";
19001
+ import { basename as basename3, join as join9 } from "path";
17951
19002
  import { homedir as homedir4 } from "os";
17952
19003
  import { isUtf8 } from "buffer";
17953
19004
  var DEFAULT_PLUGIN_PORT = 58741;
@@ -17999,15 +19050,15 @@ function getPluginsFolder() {
17999
19050
  if (process.env.MCP_PLUGINS_DIR)
18000
19051
  return process.env.MCP_PLUGINS_DIR;
18001
19052
  if (process.platform === "win32") {
18002
- return join8(process.env.LOCALAPPDATA || join8(homedir4(), "AppData", "Local"), "Roblox", "Plugins");
19053
+ return join9(process.env.LOCALAPPDATA || join9(homedir4(), "AppData", "Local"), "Roblox", "Plugins");
18003
19054
  }
18004
19055
  if (isWSL()) {
18005
19056
  const localAppData = getWindowsUserLocalAppData();
18006
19057
  if (localAppData)
18007
- return join8(localAppData.linuxPath, "Roblox", "Plugins");
19058
+ return join9(localAppData.linuxPath, "Roblox", "Plugins");
18008
19059
  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.");
18009
19060
  }
18010
- return join8(homedir4(), "Documents", "Roblox", "Plugins");
19061
+ return join9(homedir4(), "Documents", "Roblox", "Plugins");
18011
19062
  }
18012
19063
  var STALE_TEST_PLUGINS_DIRECTORY = "RsmcpIsolatedPlugins";
18013
19064
  function pluginDirectorySettingPattern() {
@@ -18022,7 +19073,7 @@ function repairStaleStudioPluginDirectorySetting({ settingsPath, studioPluginsDi
18022
19073
  if (studioPluginsDirectory === "") {
18023
19074
  throw new Error("studioPluginsDirectory must be a non-empty path.");
18024
19075
  }
18025
- const contents = readFileSync8(settingsPath, "utf8");
19076
+ const contents = readFileSync9(settingsPath, "utf8");
18026
19077
  const matches = [...contents.matchAll(pluginDirectorySettingPattern())];
18027
19078
  if (matches.length !== 1) {
18028
19079
  throw new Error(`Expected exactly one Studio.PluginsDir QDir in ${settingsPath}; found ${matches.length}.`);
@@ -18031,15 +19082,15 @@ function repairStaleStudioPluginDirectorySetting({ settingsPath, studioPluginsDi
18031
19082
  return false;
18032
19083
  const studioPath = escapeXmlText(studioPluginsDirectory);
18033
19084
  const updated = contents.replace(pluginDirectorySettingPattern(), (_match, open4, _value, close) => `${open4}${studioPath}${close}`);
18034
- const temporaryPath = `${settingsPath}.rsmcp-install-${process.pid}-${randomUUID5()}.tmp`;
19085
+ const temporaryPath = `${settingsPath}.rsmcp-install-${process.pid}-${randomUUID6()}.tmp`;
18035
19086
  try {
18036
- writeFileSync4(temporaryPath, updated, {
19087
+ writeFileSync5(temporaryPath, updated, {
18037
19088
  encoding: "utf8",
18038
- mode: statSync3(settingsPath).mode
19089
+ mode: statSync4(settingsPath).mode
18039
19090
  });
18040
19091
  renameSync(temporaryPath, settingsPath);
18041
19092
  } finally {
18042
- rmSync4(temporaryPath, { force: true });
19093
+ rmSync5(temporaryPath, { force: true });
18043
19094
  }
18044
19095
  return true;
18045
19096
  }
@@ -18050,16 +19101,16 @@ function repairDefaultStudioPluginDirectorySetting(pluginsFolder, log) {
18050
19101
  let defaultPluginsFolder;
18051
19102
  let studioPluginsDirectory;
18052
19103
  if (process.platform === "win32") {
18053
- const localAppData = process.env.LOCALAPPDATA || join8(homedir4(), "AppData", "Local");
18054
- defaultPluginsFolder = join8(localAppData, "Roblox", "Plugins");
18055
- settingsPath = join8(localAppData, "Roblox", "GlobalSettings_13.xml");
19104
+ const localAppData = process.env.LOCALAPPDATA || join9(homedir4(), "AppData", "Local");
19105
+ defaultPluginsFolder = join9(localAppData, "Roblox", "Plugins");
19106
+ settingsPath = join9(localAppData, "Roblox", "GlobalSettings_13.xml");
18056
19107
  studioPluginsDirectory = defaultPluginsFolder.replaceAll("\\", "/");
18057
19108
  } else if (isWSL()) {
18058
19109
  const localAppData = getWindowsUserLocalAppData();
18059
19110
  if (!localAppData)
18060
19111
  return;
18061
- defaultPluginsFolder = join8(localAppData.linuxPath, "Roblox", "Plugins");
18062
- settingsPath = join8(localAppData.linuxPath, "Roblox", "GlobalSettings_13.xml");
19112
+ defaultPluginsFolder = join9(localAppData.linuxPath, "Roblox", "Plugins");
19113
+ settingsPath = join9(localAppData.linuxPath, "Roblox", "GlobalSettings_13.xml");
18063
19114
  studioPluginsDirectory = [
18064
19115
  localAppData.windowsPath.replace(/[\\/]+$/u, ""),
18065
19116
  "Roblox",
@@ -18075,12 +19126,12 @@ function repairDefaultStudioPluginDirectorySetting(pluginsFolder, log) {
18075
19126
  }
18076
19127
  }
18077
19128
  function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
18078
- const otherDest = join8(pluginsFolder, otherAssetName);
19129
+ const otherDest = join9(pluginsFolder, otherAssetName);
18079
19130
  if (!existsSync7(otherDest))
18080
19131
  return;
18081
19132
  if (replace) {
18082
19133
  try {
18083
- unlinkSync2(otherDest);
19134
+ unlinkSync3(otherDest);
18084
19135
  log(`Removed conflicting ${otherAssetName}.`);
18085
19136
  } catch (err2) {
18086
19137
  warn(`[install-plugin] Could not remove ${otherDest}: ${err2}. Continuing.`);
@@ -18094,18 +19145,18 @@ Delete ${otherAssetName} manually or use the default CLI installer behavior to r
18094
19145
  `);
18095
19146
  }
18096
19147
  var PLUGIN_INSTALL_LOCK_NAME = ".robloxstudio-mcp-plugin-install.lock";
18097
- function errorCode(error) {
19148
+ function errorCode2(error) {
18098
19149
  return error !== null && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
18099
19150
  }
18100
19151
  function pluginInstallLockError(lockPath, reason) {
18101
19152
  return new Error(`${reason}: ${lockPath}. If no installer or lock recovery is running, remove that lock directory and retry.`);
18102
19153
  }
18103
19154
  function readPluginInstallLockOwner(lockPath) {
18104
- const ownerPath = join8(lockPath, "owner.json");
19155
+ const ownerPath = join9(lockPath, "owner.json");
18105
19156
  if (!lstatSync2(lockPath).isDirectory() || !lstatSync2(ownerPath).isFile()) {
18106
19157
  throw new Error("Lock and owner metadata must be a directory and regular file");
18107
19158
  }
18108
- const owner = JSON.parse(readFileSync8(ownerPath, "utf8"));
19159
+ const owner = JSON.parse(readFileSync9(ownerPath, "utf8"));
18109
19160
  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) {
18110
19161
  throw new Error("Invalid lock owner metadata");
18111
19162
  }
@@ -18116,9 +19167,9 @@ function assertPluginInstallOwnerExited(lockPath, owner) {
18116
19167
  try {
18117
19168
  process.kill(owner.pid, 0);
18118
19169
  } catch (error) {
18119
- if (errorCode(error) === "ESRCH")
19170
+ if (errorCode2(error) === "ESRCH")
18120
19171
  return;
18121
- throw pluginInstallLockError(lockPath, `Cannot confirm that plugin installation owner PID ${owner.pid} has exited (${errorCode(error) ?? error})`);
19172
+ throw pluginInstallLockError(lockPath, `Cannot confirm that plugin installation owner PID ${owner.pid} has exited (${errorCode2(error) ?? error})`);
18122
19173
  }
18123
19174
  }
18124
19175
  throw pluginInstallLockError(lockPath, `Another Studio plugin installation is already in progress (owner PID ${owner.pid})`);
@@ -18131,13 +19182,13 @@ function recoverPluginInstallLock(lockPath, warn) {
18131
19182
  throw pluginInstallLockError(lockPath, `Cannot safely read plugin installation lock owner (${error})`);
18132
19183
  }
18133
19184
  assertPluginInstallOwnerExited(lockPath, owner);
18134
- const claimPath = join8(lockPath, `recovery-${owner.token}`);
19185
+ const claimPath = join9(lockPath, `recovery-${owner.token}`);
18135
19186
  try {
18136
- mkdirSync4(claimPath);
19187
+ mkdirSync5(claimPath);
18137
19188
  } catch (error) {
18138
- throw pluginInstallLockError(lockPath, errorCode(error) === "EEXIST" ? "Plugin installation lock recovery is already in progress or was interrupted" : `Could not claim plugin installation lock recovery (${error})`);
19189
+ throw pluginInstallLockError(lockPath, errorCode2(error) === "EEXIST" ? "Plugin installation lock recovery is already in progress or was interrupted" : `Could not claim plugin installation lock recovery (${error})`);
18139
19190
  }
18140
- const abandonedPath = `${lockPath}.${randomUUID5()}.abandoned`;
19191
+ const abandonedPath = `${lockPath}.${randomUUID6()}.abandoned`;
18141
19192
  let moved = false;
18142
19193
  try {
18143
19194
  let currentOwner;
@@ -18157,49 +19208,49 @@ function recoverPluginInstallLock(lockPath, warn) {
18157
19208
  try {
18158
19209
  rmdirSync(claimPath);
18159
19210
  } catch (error) {
18160
- if (errorCode(error) !== "ENOENT") {
19211
+ if (errorCode2(error) !== "ENOENT") {
18161
19212
  warn(`[install-plugin] Could not release lock recovery claim ${claimPath}: ${error}`);
18162
19213
  }
18163
19214
  }
18164
19215
  }
18165
19216
  }
18166
19217
  try {
18167
- rmSync4(abandonedPath, { recursive: true, force: true });
19218
+ rmSync5(abandonedPath, { recursive: true, force: true });
18168
19219
  } catch (error) {
18169
19220
  warn(`[install-plugin] Could not clean abandoned install lock ${abandonedPath}: ${error}`);
18170
19221
  }
18171
19222
  }
18172
19223
  function acquirePluginInstallLock(pluginsFolder, warn) {
18173
- const lockPath = join8(pluginsFolder, PLUGIN_INSTALL_LOCK_NAME);
18174
- const ownerPath = join8(lockPath, "owner.json");
19224
+ const lockPath = join9(pluginsFolder, PLUGIN_INSTALL_LOCK_NAME);
19225
+ const ownerPath = join9(lockPath, "owner.json");
18175
19226
  const owner = {
18176
19227
  pid: process.pid,
18177
- token: randomUUID5(),
19228
+ token: randomUUID6(),
18178
19229
  createdAt: Date.now()
18179
19230
  };
18180
19231
  try {
18181
- mkdirSync4(lockPath);
19232
+ mkdirSync5(lockPath);
18182
19233
  } catch (error) {
18183
- if (errorCode(error) !== "EEXIST")
19234
+ if (errorCode2(error) !== "EEXIST")
18184
19235
  throw error;
18185
19236
  recoverPluginInstallLock(lockPath, warn);
18186
19237
  try {
18187
- mkdirSync4(lockPath);
19238
+ mkdirSync5(lockPath);
18188
19239
  } catch (retryError) {
18189
- if (errorCode(retryError) !== "EEXIST")
19240
+ if (errorCode2(retryError) !== "EEXIST")
18190
19241
  throw retryError;
18191
19242
  throw pluginInstallLockError(lockPath, "Another Studio plugin installation is already in progress");
18192
19243
  }
18193
19244
  }
18194
19245
  try {
18195
- writeFileSync4(ownerPath, `${JSON.stringify(owner)}
19246
+ writeFileSync5(ownerPath, `${JSON.stringify(owner)}
18196
19247
  `, {
18197
19248
  encoding: "utf8",
18198
19249
  flag: "wx"
18199
19250
  });
18200
19251
  } catch (error) {
18201
19252
  try {
18202
- rmSync4(lockPath, { recursive: true, force: true });
19253
+ rmSync5(lockPath, { recursive: true, force: true });
18203
19254
  } catch (cleanupError) {
18204
19255
  warn(`[install-plugin] Could not clean failed install lock ${lockPath}: ${cleanupError}`);
18205
19256
  }
@@ -18209,10 +19260,10 @@ function acquirePluginInstallLock(pluginsFolder, warn) {
18209
19260
  try {
18210
19261
  const currentOwner = readPluginInstallLockOwner(lockPath);
18211
19262
  if (currentOwner.pid === owner.pid && currentOwner.token === owner.token) {
18212
- rmSync4(lockPath, { recursive: true, force: true });
19263
+ rmSync5(lockPath, { recursive: true, force: true });
18213
19264
  }
18214
19265
  } catch (error) {
18215
- if (errorCode(error) !== "ENOENT") {
19266
+ if (errorCode2(error) !== "ENOENT") {
18216
19267
  warn(`[install-plugin] Could not release install lock ${lockPath}: ${error}`);
18217
19268
  }
18218
19269
  }
@@ -18305,7 +19356,7 @@ function installedFileMatches(source, destination) {
18305
19356
  return false;
18306
19357
  if (!lstatSync2(destination).isFile())
18307
19358
  return false;
18308
- const installed = readFileSync8(destination);
19359
+ const installed = readFileSync9(destination);
18309
19360
  return installed.length === source.length && installed.equals(source);
18310
19361
  }
18311
19362
  function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source, expectedVersion, expectedVariant, rawPort, replaceVariant = true, log = console.log, warn = console.warn }) {
@@ -18316,22 +19367,22 @@ function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source,
18316
19367
  }
18317
19368
  const configured = configurePluginAssetForPort(source, rawPort);
18318
19369
  assertPluginAssetIdentity(configured, assetName, expectedVersion, expectedVariant);
18319
- mkdirSync4(pluginsFolder, { recursive: true });
18320
- const destination = join8(pluginsFolder, assetName);
19370
+ mkdirSync5(pluginsFolder, { recursive: true });
19371
+ const destination = join9(pluginsFolder, assetName);
18321
19372
  const releaseLock = acquirePluginInstallLock(pluginsFolder, warn);
18322
19373
  try {
18323
19374
  let installed = false;
18324
19375
  if (!installedFileMatches(configured, destination)) {
18325
- const staged = join8(pluginsFolder, `.${assetName}.${process.pid}.${randomUUID5()}.tmp`);
19376
+ const staged = join9(pluginsFolder, `.${assetName}.${process.pid}.${randomUUID6()}.tmp`);
18326
19377
  try {
18327
- writeFileSync4(staged, configured, { flag: "wx" });
19378
+ writeFileSync5(staged, configured, { flag: "wx" });
18328
19379
  renameSync(staged, destination);
18329
19380
  installed = true;
18330
19381
  } finally {
18331
19382
  try {
18332
- unlinkSync2(staged);
19383
+ unlinkSync3(staged);
18333
19384
  } catch (error) {
18334
- if (errorCode(error) !== "ENOENT") {
19385
+ if (errorCode2(error) !== "ENOENT") {
18335
19386
  warn(`[install-plugin] Could not remove staging file ${staged}: ${error}`);
18336
19387
  }
18337
19388
  }
@@ -18352,8 +19403,8 @@ function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source,
18352
19403
  }
18353
19404
 
18354
19405
  // ../core/dist/plugin-installer.js
18355
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
18356
- import { dirname as dirname5, join as join9 } from "path";
19406
+ import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
19407
+ import { dirname as dirname5, join as join10 } from "path";
18357
19408
  import { fileURLToPath } from "url";
18358
19409
  import { get } from "https";
18359
19410
  var REPO = "chrrxs/robloxstudio-mcp";
@@ -18417,8 +19468,8 @@ function createPluginInstaller(moduleUrl, variant) {
18417
19468
  }
18418
19469
  function bundledAssetPath() {
18419
19470
  const candidates = [
18420
- join9(currentDir, "..", "studio-plugin", ASSET_NAME),
18421
- join9(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
19471
+ join10(currentDir, "..", "studio-plugin", ASSET_NAME),
19472
+ join10(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
18422
19473
  ];
18423
19474
  return candidates.find((candidate) => existsSync8(candidate)) ?? null;
18424
19475
  }
@@ -18428,7 +19479,7 @@ function createPluginInstaller(moduleUrl, variant) {
18428
19479
  return existsSync8(sourcePath) ? sourcePath : null;
18429
19480
  }
18430
19481
  function packageVersion() {
18431
- const pkg = JSON.parse(readFileSync9(join9(currentDir, "..", "package.json"), "utf8"));
19482
+ const pkg = JSON.parse(readFileSync10(join10(currentDir, "..", "package.json"), "utf8"));
18432
19483
  if (!pkg.version) {
18433
19484
  throw new Error("Package version not found");
18434
19485
  }
@@ -18453,7 +19504,7 @@ function createPluginInstaller(moduleUrl, variant) {
18453
19504
  if (!sourcePath) {
18454
19505
  throw new Error(`Bundled ${ASSET_NAME} not found. Run npm run ${buildCommand} in this worktree first.`);
18455
19506
  }
18456
- const result = installSource(readFileSync9(sourcePath), options);
19507
+ const result = installSource(readFileSync10(sourcePath), options);
18457
19508
  if (result.installed) {
18458
19509
  log(`Installed ${ASSET_NAME} to ${result.destination}`);
18459
19510
  }
@@ -18465,7 +19516,7 @@ function createPluginInstaller(moduleUrl, variant) {
18465
19516
  throw new Error(`Plugin asset not found at explicit path ${options.sourcePath}.`);
18466
19517
  }
18467
19518
  if (bundled) {
18468
- const result2 = installSource(readFileSync9(bundled), options);
19519
+ const result2 = installSource(readFileSync10(bundled), options);
18469
19520
  if (result2.installed) {
18470
19521
  log(`Installed bundled ${ASSET_NAME} to ${result2.destination}`);
18471
19522
  } else {