@chrrxs/robloxstudio-mcp 3.1.6 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3296,7 +3296,7 @@ Tool descriptions explain selection. Input schemas explain arguments. This guide
3296
3296
  ## Connection and paths
3297
3297
 
3298
3298
  - Use canonical DataModel paths returned by the tools. Paths usually start with game.
3299
- - Call get_connected_instances when more than one Studio process may be connected. Pass either a top-level Instance ID or a multiplayer group's role-suffixed Instance ID as instance_id on later calls.
3299
+ - Call get_connected_instances when more than one Studio process may be connected. Pass either a top-level Instance ID or a multiplayer group's role-suffixed Instance ID as instance_id on later calls. A role-suffixed ID selects that server or client as the default target; a conflicting single-peer target is rejected.
3300
3300
  - Use get_place_info for the active place identity and settings.
3301
3301
 
3302
3302
  ## Discovery and edit work
@@ -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
  }
@@ -3856,6 +3879,9 @@ var ManagedInstanceRegistry = class {
3856
3879
  if (!options.observeProcess || !(parsed.nativeProcessId || parsed.spawnPid))
3857
3880
  continue;
3858
3881
  const observation = await options.observeProcess(parsed);
3882
+ const previousObservationAt = parsed.lastProcessObservationAt;
3883
+ if (previousObservationAt !== void 0 && observation.observedAt < previousObservationAt)
3884
+ continue;
3859
3885
  if (observation.status === "unknown") {
3860
3886
  parsed.processObservationStatus = "unknown";
3861
3887
  parsed.lastProcessObservationAt = observation.observedAt;
@@ -3865,7 +3891,6 @@ var ManagedInstanceRegistry = class {
3865
3891
  await this.writeRecordUnlocked(parsed);
3866
3892
  continue;
3867
3893
  }
3868
- const previousObservationAt = parsed.lastProcessObservationAt;
3869
3894
  parsed.lastSuccessfulProcessObservationAt = observation.observedAt;
3870
3895
  parsed.lastProcessObservationAt = observation.observedAt;
3871
3896
  parsed.lastProcessObservationError = void 0;
@@ -3909,7 +3934,8 @@ var ManagedInstanceRegistry = class {
3909
3934
 
3910
3935
  // ../core/dist/studio-platform.js
3911
3936
  import { execFileSync } from "child_process";
3912
- import { existsSync, readFileSync } from "fs";
3937
+ import { accessSync, constants, existsSync, readFileSync, statSync } from "fs";
3938
+ import * as path3 from "path";
3913
3939
  var WSL_KERNEL_PATTERN = /microsoft|wsl/i;
3914
3940
  var WINDOWS_INTEROP_PROBE_TOKEN = "ROBLOXSTUDIO_MCP_WINDOWS_INTEROP_OK";
3915
3941
  var WINDOWS_POWERSHELL = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe";
@@ -3950,7 +3976,20 @@ function detectStudioPlatform(evidence) {
3950
3976
  }
3951
3977
  };
3952
3978
  }
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.";
3979
+ if (!hasWslKernel && evidence.wineLauncher && evidence.wineLauncherExecutable === true) {
3980
+ return {
3981
+ hostPlatform: "linux",
3982
+ isWsl: false,
3983
+ windowsInteropAvailable: false,
3984
+ processIdentity: {
3985
+ supported: true,
3986
+ launcher: "wine-retained"
3987
+ },
3988
+ // Wine hosts are always Linux; keep POSIX semantics when tests run elsewhere.
3989
+ wineLauncher: path3.posix.resolve(evidence.wineLauncher)
3990
+ };
3991
+ }
3992
+ 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
3993
  return {
3955
3994
  hostPlatform: "linux",
3956
3995
  isWsl: false,
@@ -4000,11 +4039,22 @@ function probeWindowsInterop(kernelVersion) {
4000
4039
  }
4001
4040
  return false;
4002
4041
  }
4042
+ function isExecutableFile(file) {
4043
+ try {
4044
+ if (!statSync(file).isFile())
4045
+ return false;
4046
+ accessSync(file, constants.X_OK);
4047
+ return true;
4048
+ } catch {
4049
+ return false;
4050
+ }
4051
+ }
4003
4052
  var cachedStudioPlatformCapabilities;
4004
4053
  function getStudioPlatformCapabilities() {
4005
4054
  if (cachedStudioPlatformCapabilities)
4006
4055
  return cachedStudioPlatformCapabilities;
4007
4056
  const kernelVersion = readKernelVersion();
4057
+ const wineLauncher = process.env.ROBLOX_STUDIO_WINE_LAUNCHER || void 0;
4008
4058
  cachedStudioPlatformCapabilities = detectStudioPlatform({
4009
4059
  platform: process.platform,
4010
4060
  kernelVersion,
@@ -4012,13 +4062,435 @@ function getStudioPlatformCapabilities() {
4012
4062
  wslDistroName: process.env.WSL_DISTRO_NAME,
4013
4063
  windowsRootPresent: existsSync("/mnt/c/Windows"),
4014
4064
  wslPathPresent: existsSync("/usr/bin/wslpath") || existsSync("/bin/wslpath"),
4015
- windowsInteropAvailable: probeWindowsInterop(kernelVersion)
4065
+ windowsInteropAvailable: probeWindowsInterop(kernelVersion),
4066
+ wineLauncher,
4067
+ wineLauncherExecutable: process.platform === "linux" && wineLauncher !== void 0 && isExecutableFile(wineLauncher)
4016
4068
  });
4017
4069
  return cachedStudioPlatformCapabilities;
4018
4070
  }
4019
4071
 
4072
+ // ../core/dist/studio-wine.js
4073
+ import { spawn } from "child_process";
4074
+ import { randomUUID as randomUUID3 } from "crypto";
4075
+ import { closeSync, fstatSync, linkSync, mkdirSync, openSync, readdirSync, readFileSync as readFileSync2, readSync, rmSync, unlinkSync, writeFileSync } from "fs";
4076
+ import * as path4 from "path";
4077
+ var DEFAULT_PROC_ROOT = "/proc";
4078
+ var STUDIO_EXECUTABLE_NAME = "robloxstudiobeta.exe";
4079
+ var GATE_SCRIPT = 'kill -STOP $$ && exec "$0" "$@"';
4080
+ var GATE_TIMEOUT_MS = 5e3;
4081
+ var STUDIO_EXEC_TIMEOUT_MS = 6e4;
4082
+ var MAX_STUDIO_EXEC_TIMEOUT_MS = 15e4;
4083
+ var STUDIO_EXEC_TIMEOUT_ENV = "ROBLOX_STUDIO_WINE_START_TIMEOUT_MS";
4084
+ var ABORT_TIMEOUT_MS = 1e4;
4085
+ var TERMINATE_GRACE_MS = 5e3;
4086
+ var POLL_INTERVAL_MS = 50;
4087
+ var LAUNCHER_LOGS_KEPT = 10;
4088
+ var LAUNCHER_LOG_TAIL_BYTES = 4096;
4089
+ var LAUNCHER_LOG_TAIL_LINES = 20;
4090
+ var BOOT_ANCHOR_PREFIX = "wine-boot-time-";
4091
+ var FILETIME_UNIX_EPOCH2 = 116444736000000000n;
4092
+ var FILETIME_TICKS_PER_SECOND = 10000000n;
4093
+ var USER_HZ = 100n;
4094
+ var bootTimeByProcRoot = /* @__PURE__ */ new Map();
4095
+ function delay3(ms) {
4096
+ const { promise, resolve: resolve6 } = Promise.withResolvers();
4097
+ setTimeout(resolve6, ms);
4098
+ return promise;
4099
+ }
4100
+ function isMissingProcessError(error) {
4101
+ const code = error?.code;
4102
+ return code === "ENOENT" || code === "ESRCH";
4103
+ }
4104
+ function procStartTimeToFileTime(bootTimeSeconds2, startTimeTicks) {
4105
+ return (FILETIME_UNIX_EPOCH2 + bootTimeSeconds2 * FILETIME_TICKS_PER_SECOND + startTimeTicks * (FILETIME_TICKS_PER_SECOND / USER_HZ)).toString();
4106
+ }
4107
+ function toWineWindowsPath(unixPath) {
4108
+ return `Z:${unixPath.replace(/\//g, "\\")}`;
4109
+ }
4110
+ function isWineStudioCommand(argv0) {
4111
+ return argv0 !== void 0 && argv0.split(/[\\/]/u).pop()?.toLowerCase() === STUDIO_EXECUTABLE_NAME;
4112
+ }
4113
+ function parseProcStat(content) {
4114
+ const commEnd = content.lastIndexOf(")");
4115
+ const fields = commEnd < 0 ? [] : content.slice(commEnd + 1).trim().split(/\s+/u);
4116
+ const state = fields[0];
4117
+ const startTime = fields[19];
4118
+ if (!state || !startTime || !/^\d+$/u.test(startTime)) {
4119
+ throw new Error("Malformed /proc/<pid>/stat.");
4120
+ }
4121
+ return { state, startTimeTicks: BigInt(startTime) };
4122
+ }
4123
+ function readProcStat(procRoot, pid) {
4124
+ let content;
4125
+ try {
4126
+ content = readFileSync2(`${procRoot}/${pid}/stat`, "utf8");
4127
+ } catch (error) {
4128
+ if (isMissingProcessError(error))
4129
+ return void 0;
4130
+ throw error;
4131
+ }
4132
+ return parseProcStat(content);
4133
+ }
4134
+ function readCmdline(procRoot, pid) {
4135
+ let content;
4136
+ try {
4137
+ content = readFileSync2(`${procRoot}/${pid}/cmdline`, "utf8");
4138
+ } catch (error) {
4139
+ if (isMissingProcessError(error))
4140
+ return void 0;
4141
+ throw error;
4142
+ }
4143
+ const args = content.split("\0");
4144
+ if (args[args.length - 1] === "")
4145
+ args.pop();
4146
+ return args;
4147
+ }
4148
+ function readArgv0(procRoot, pid) {
4149
+ return readCmdline(procRoot, pid)?.[0] || void 0;
4150
+ }
4151
+ function readRealUid(procRoot, pid) {
4152
+ let content;
4153
+ try {
4154
+ content = readFileSync2(`${procRoot}/${pid}/status`, "utf8");
4155
+ } catch (error) {
4156
+ if (isMissingProcessError(error))
4157
+ return void 0;
4158
+ throw error;
4159
+ }
4160
+ const match = /^Uid:\s+(\d+)/mu.exec(content);
4161
+ if (!match)
4162
+ throw new Error(`Malformed ${procRoot}/${pid}/status.`);
4163
+ return Number(match[1]);
4164
+ }
4165
+ function readLiveBootTime(procRoot) {
4166
+ const match = /^btime\s+(\d+)\s*$/mu.exec(readFileSync2(`${procRoot}/stat`, "utf8"));
4167
+ if (!match)
4168
+ throw new Error(`${procRoot}/stat has no btime.`);
4169
+ return BigInt(match[1]);
4170
+ }
4171
+ function readBootId(procRoot) {
4172
+ try {
4173
+ const bootId = readFileSync2(`${procRoot}/sys/kernel/random/boot_id`, "utf8").trim();
4174
+ return /^[0-9a-f-]+$/iu.test(bootId) ? bootId : void 0;
4175
+ } catch {
4176
+ return void 0;
4177
+ }
4178
+ }
4179
+ function readBootAnchor(file) {
4180
+ let content;
4181
+ try {
4182
+ content = readFileSync2(file, "utf8");
4183
+ } catch (error) {
4184
+ if (error.code === "ENOENT")
4185
+ return void 0;
4186
+ throw error;
4187
+ }
4188
+ const match = /^(\d+)\n?$/u.exec(content);
4189
+ if (!match)
4190
+ throw new Error(`${file} is not a boot time.`);
4191
+ return BigInt(match[1]);
4192
+ }
4193
+ function removeStaleBootAnchors(anchorDir, currentName) {
4194
+ try {
4195
+ for (const name of readdirSync(anchorDir)) {
4196
+ if (!name.startsWith(BOOT_ANCHOR_PREFIX) || name.startsWith(currentName))
4197
+ continue;
4198
+ rmSync(path4.join(anchorDir, name), { force: true });
4199
+ }
4200
+ } catch {
4201
+ }
4202
+ }
4203
+ function anchoredBootTime(procRoot, liveBootTime) {
4204
+ const bootId = readBootId(procRoot);
4205
+ if (bootId === void 0)
4206
+ return liveBootTime;
4207
+ const anchorDir = defaultManagedInstanceRegistryDir();
4208
+ const anchorName = `${BOOT_ANCHOR_PREFIX}${bootId}`;
4209
+ const anchorFile = path4.join(anchorDir, anchorName);
4210
+ try {
4211
+ const existing = readBootAnchor(anchorFile);
4212
+ if (existing !== void 0)
4213
+ return existing;
4214
+ mkdirSync(anchorDir, { recursive: true });
4215
+ const tmpFile = `${anchorFile}.${process.pid}.${randomUUID3()}.tmp`;
4216
+ writeFileSync(tmpFile, `${liveBootTime}
4217
+ `);
4218
+ try {
4219
+ linkSync(tmpFile, anchorFile);
4220
+ } catch (error) {
4221
+ if (error.code !== "EEXIST")
4222
+ throw error;
4223
+ } finally {
4224
+ unlinkSync(tmpFile);
4225
+ }
4226
+ removeStaleBootAnchors(anchorDir, anchorName);
4227
+ return readBootAnchor(anchorFile) ?? liveBootTime;
4228
+ } catch (error) {
4229
+ 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.`);
4230
+ return liveBootTime;
4231
+ }
4232
+ }
4233
+ function bootTimeSeconds(procRoot) {
4234
+ const cached = bootTimeByProcRoot.get(procRoot);
4235
+ if (cached !== void 0)
4236
+ return cached;
4237
+ const bootTime = anchoredBootTime(procRoot, readLiveBootTime(procRoot));
4238
+ bootTimeByProcRoot.set(procRoot, bootTime);
4239
+ return bootTime;
4240
+ }
4241
+ function readWineProcessStartTime(pid, procRoot = DEFAULT_PROC_ROOT) {
4242
+ const stat2 = readProcStat(procRoot, pid);
4243
+ if (!stat2 || stat2.state === "Z" || stat2.state === "X")
4244
+ return void 0;
4245
+ return procStartTimeToFileTime(bootTimeSeconds(procRoot), stat2.startTimeTicks);
4246
+ }
4247
+ function listWineStudioProcesses(procRoot = DEFAULT_PROC_ROOT, uid = process.getuid?.()) {
4248
+ const processes = [];
4249
+ for (const entry of readdirSync(procRoot)) {
4250
+ if (!/^\d+$/u.test(entry))
4251
+ continue;
4252
+ const pid = Number(entry);
4253
+ let argv0;
4254
+ let startedAt;
4255
+ try {
4256
+ if (uid !== void 0 && readRealUid(procRoot, pid) !== uid)
4257
+ continue;
4258
+ argv0 = readArgv0(procRoot, pid);
4259
+ if (!isWineStudioCommand(argv0))
4260
+ continue;
4261
+ startedAt = readWineProcessStartTime(pid, procRoot);
4262
+ } catch (error) {
4263
+ if (error.code === "EACCES" || isMissingProcessError(error))
4264
+ continue;
4265
+ throw error;
4266
+ }
4267
+ if (startedAt === void 0)
4268
+ continue;
4269
+ processes.push({
4270
+ Id: pid,
4271
+ Name: "RobloxStudioBeta",
4272
+ Path: argv0,
4273
+ MainWindowTitle: "",
4274
+ StartTimeUtcFileTime: startedAt
4275
+ });
4276
+ }
4277
+ return processes;
4278
+ }
4279
+ function signalProcess(pid, signal) {
4280
+ try {
4281
+ process.kill(pid, signal);
4282
+ } catch (error) {
4283
+ if (!isMissingProcessError(error))
4284
+ throw error;
4285
+ }
4286
+ }
4287
+ async function stopWineStudio(pid, startedAt, timeoutMs, procRoot = DEFAULT_PROC_ROOT) {
4288
+ if (!Number.isSafeInteger(pid) || pid <= 0)
4289
+ throw new Error("processId must be a positive integer.");
4290
+ if (!/^[1-9]\d*$/u.test(startedAt))
4291
+ throw new Error("startedAt must be a positive FILETIME string.");
4292
+ const isTarget = () => readWineProcessStartTime(pid, procRoot) === startedAt;
4293
+ if (!isTarget())
4294
+ return;
4295
+ signalProcess(pid, "SIGTERM");
4296
+ const graceDeadline = Date.now() + Math.min(TERMINATE_GRACE_MS, timeoutMs / 2);
4297
+ while (Date.now() < graceDeadline) {
4298
+ await delay3(POLL_INTERVAL_MS);
4299
+ if (!isTarget())
4300
+ return;
4301
+ }
4302
+ if (isTarget())
4303
+ signalProcess(pid, "SIGKILL");
4304
+ }
4305
+ function killOrphanedWineLaunchGate(pid, startedAt, procRoot = DEFAULT_PROC_ROOT) {
4306
+ if (!Number.isSafeInteger(pid) || pid <= 0)
4307
+ return false;
4308
+ const stat2 = readProcStat(procRoot, pid);
4309
+ if (stat2?.state !== "T")
4310
+ return false;
4311
+ if (procStartTimeToFileTime(bootTimeSeconds(procRoot), stat2.startTimeTicks) !== startedAt)
4312
+ return false;
4313
+ const cmdline = readCmdline(procRoot, pid);
4314
+ if (cmdline?.[1] !== "-c" || cmdline[2] !== GATE_SCRIPT)
4315
+ return false;
4316
+ signalProcess(pid, "SIGKILL");
4317
+ return true;
4318
+ }
4319
+ function studioExecTimeoutMs(explicit) {
4320
+ if (explicit !== void 0)
4321
+ return explicit;
4322
+ const configured = process.env[STUDIO_EXEC_TIMEOUT_ENV];
4323
+ if (configured === void 0 || configured === "")
4324
+ return STUDIO_EXEC_TIMEOUT_MS;
4325
+ const value = /^\d+$/u.test(configured) ? Number(configured) : NaN;
4326
+ if (!Number.isSafeInteger(value) || value < 1 || value > MAX_STUDIO_EXEC_TIMEOUT_MS) {
4327
+ throw new Error(`${STUDIO_EXEC_TIMEOUT_ENV} must be a whole number of milliseconds from 1 to ${MAX_STUDIO_EXEC_TIMEOUT_MS}; got "${configured}".`);
4328
+ }
4329
+ return value;
4330
+ }
4331
+ function openLauncherLog(logDir) {
4332
+ try {
4333
+ mkdirSync(logDir, { recursive: true });
4334
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/gu, "-");
4335
+ const file = path4.join(logDir, `${stamp}-${randomUUID3().slice(0, 8)}.log`);
4336
+ const fd = openSync(file, "a", 384);
4337
+ const logs = readdirSync(logDir).filter((name) => name.endsWith(".log")).sort();
4338
+ for (const name of logs.slice(0, Math.max(0, logs.length - LAUNCHER_LOGS_KEPT))) {
4339
+ rmSync(path4.join(logDir, name), { force: true });
4340
+ }
4341
+ return { file, fd };
4342
+ } catch (error) {
4343
+ console.error(`[studio] Could not open a Wine launcher log in ${logDir}; launcher output is discarded: ${error instanceof Error ? error.message : String(error)}`);
4344
+ return void 0;
4345
+ }
4346
+ }
4347
+ function launcherLogTail(file) {
4348
+ if (file === void 0)
4349
+ return "";
4350
+ let tail;
4351
+ try {
4352
+ const fd = openSync(file, "r");
4353
+ try {
4354
+ const size = fstatSync(fd).size;
4355
+ const length = Math.min(size, LAUNCHER_LOG_TAIL_BYTES);
4356
+ const buffer = Buffer.alloc(length);
4357
+ readSync(fd, buffer, 0, length, size - length);
4358
+ tail = buffer.toString("utf8");
4359
+ } finally {
4360
+ closeSync(fd);
4361
+ }
4362
+ } catch {
4363
+ return ` Launcher log: ${file}.`;
4364
+ }
4365
+ const lines = tail.trimEnd().split("\n").slice(-LAUNCHER_LOG_TAIL_LINES).join("\n");
4366
+ return lines ? ` Launcher output (${file}):
4367
+ ${lines}` : ` The launcher wrote no output (log: ${file}).`;
4368
+ }
4369
+ async function spawnWineStudio(launcher, exe, args, spawnOptions, options = {}) {
4370
+ const procRoot = options.procRoot ?? DEFAULT_PROC_ROOT;
4371
+ const execTimeoutMs = studioExecTimeoutMs(options.studioExecTimeoutMs);
4372
+ const log = options.logDir === void 0 ? void 0 : openLauncherLog(options.logDir);
4373
+ let child;
4374
+ try {
4375
+ child = spawn("/bin/sh", ["-c", GATE_SCRIPT, launcher, exe, ...args], {
4376
+ cwd: spawnOptions.cwd,
4377
+ env: spawnOptions.env,
4378
+ detached: true,
4379
+ stdio: log ? ["ignore", log.fd, log.fd] : "ignore"
4380
+ });
4381
+ } finally {
4382
+ if (log)
4383
+ closeSync(log.fd);
4384
+ }
4385
+ const logTail = () => launcherLogTail(log?.file);
4386
+ let exitDetail;
4387
+ const { promise: exited, resolve: markExited } = Promise.withResolvers();
4388
+ child.once("exit", (code, signal) => {
4389
+ exitDetail ??= signal ? `signal ${signal}` : `code ${code}`;
4390
+ markExited();
4391
+ });
4392
+ child.once("error", (error) => {
4393
+ exitDetail ??= error.message;
4394
+ });
4395
+ const pid = child.pid;
4396
+ if (pid === void 0) {
4397
+ await delay3(0);
4398
+ throw new Error(`Failed to start the Wine Studio launch gate: ${exitDetail ?? "no PID was assigned"}.`);
4399
+ }
4400
+ const gateDeadline = Date.now() + (options.gateTimeoutMs ?? GATE_TIMEOUT_MS);
4401
+ let gated;
4402
+ try {
4403
+ while (exitDetail === void 0 && Date.now() < gateDeadline) {
4404
+ const stat2 = readProcStat(procRoot, pid);
4405
+ if (stat2?.state === "T") {
4406
+ gated = stat2;
4407
+ break;
4408
+ }
4409
+ await delay3(10);
4410
+ }
4411
+ } catch (error) {
4412
+ child.kill("SIGKILL");
4413
+ throw error;
4414
+ }
4415
+ if (!gated) {
4416
+ child.kill("SIGKILL");
4417
+ 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()}`);
4418
+ }
4419
+ const nativeStartedAt = procStartTimeToFileTime(bootTimeSeconds(procRoot), gated.startTimeTicks);
4420
+ let controlState = "pending";
4421
+ const authorize = async () => {
4422
+ if (controlState === "resumed")
4423
+ return;
4424
+ if (controlState !== "pending") {
4425
+ throw new Error(`Studio launch cannot be authorized from ${controlState}.`);
4426
+ }
4427
+ controlState = "authorizing";
4428
+ child.kill("SIGCONT");
4429
+ const deadline = Date.now() + execTimeoutMs;
4430
+ try {
4431
+ while (exitDetail === void 0 && Date.now() < deadline) {
4432
+ if (readWineProcessStartTime(pid, procRoot) === nativeStartedAt && isWineStudioCommand(readArgv0(procRoot, pid))) {
4433
+ controlState = "resumed";
4434
+ return;
4435
+ }
4436
+ await delay3(POLL_INTERVAL_MS);
4437
+ }
4438
+ } catch (error) {
4439
+ controlState = "aborted";
4440
+ child.kill("SIGKILL");
4441
+ throw error;
4442
+ }
4443
+ controlState = "aborted";
4444
+ if (exitDetail !== void 0) {
4445
+ throw new Error(`Wine Studio process ${pid} exited (${exitDetail}) before RobloxStudioBeta.exe started.${logTail()}`);
4446
+ }
4447
+ child.kill("SIGKILL");
4448
+ 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()}`);
4449
+ };
4450
+ const release = async () => {
4451
+ if (controlState === "released")
4452
+ return;
4453
+ if (controlState !== "resumed") {
4454
+ throw new Error(`Studio launch cannot be released from ${controlState}.`);
4455
+ }
4456
+ controlState = "released";
4457
+ };
4458
+ const abort = async () => {
4459
+ if (controlState === "released")
4460
+ return;
4461
+ controlState = "aborted";
4462
+ if (exitDetail !== void 0)
4463
+ return;
4464
+ child.kill("SIGKILL");
4465
+ const { promise: timedOut, resolve: markTimedOut } = Promise.withResolvers();
4466
+ const timer = setTimeout(() => markTimedOut(true), ABORT_TIMEOUT_MS);
4467
+ try {
4468
+ if (await Promise.race([exited.then(() => false), timedOut])) {
4469
+ throw new Error(`Timed out aborting Wine Studio process ${pid}.`);
4470
+ }
4471
+ } finally {
4472
+ clearTimeout(timer);
4473
+ }
4474
+ };
4475
+ return {
4476
+ pid,
4477
+ nativePid: pid,
4478
+ nativeStartedAt,
4479
+ unref: () => child.unref(),
4480
+ authorize,
4481
+ release,
4482
+ abort,
4483
+ onExit: (listener) => {
4484
+ child.once("exit", listener);
4485
+ },
4486
+ onError: (listener) => {
4487
+ child.once("error", listener);
4488
+ }
4489
+ };
4490
+ }
4491
+
4020
4492
  // ../core/dist/studio-instance-manager.js
4021
- var BASEPLATE_TEMP_DIR = path3.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
4493
+ var BASEPLATE_TEMP_DIR = path5.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
4022
4494
  var BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
4023
4495
  var BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
4024
4496
  var retainedLaunchControls = /* @__PURE__ */ new Map();
@@ -4101,6 +4573,9 @@ async function runAsync(command, args, options = {}) {
4101
4573
  function isWsl() {
4102
4574
  return getStudioPlatformCapabilities().isWsl;
4103
4575
  }
4576
+ function isWineHost() {
4577
+ return getStudioPlatformCapabilities().processIdentity.launcher === "wine-retained";
4578
+ }
4104
4579
  async function powershellAsync(script, timeoutMs = 15e3) {
4105
4580
  return runAsync("powershell.exe", ["-NoProfile", "-Command", script], {
4106
4581
  cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
@@ -4144,7 +4619,9 @@ async function toWslPathAsync(windowsPath) {
4144
4619
  return runAsync("wslpath", ["-u", windowsPath]);
4145
4620
  }
4146
4621
  async function toStudioLaunchArgAsync(arg) {
4147
- if (!isWsl() || !path3.isAbsolute(arg) || !existsSync2(arg))
4622
+ if (isWineHost() && path5.isAbsolute(arg) && existsSync2(arg))
4623
+ return toWineWindowsPath(arg);
4624
+ if (!isWsl() || !path5.isAbsolute(arg) || !existsSync2(arg))
4148
4625
  return arg;
4149
4626
  return runAsync("wslpath", ["-w", arg]);
4150
4627
  }
@@ -4464,7 +4941,8 @@ public sealed class McpSuspendedStudio : IDisposable
4464
4941
 
4465
4942
  private static IntPtr OpenTestWorkerJob(string name)
4466
4943
  {
4467
- if (name == null) return IntPtr.Zero;
4944
+ // PowerShell passes $null to a .NET string parameter as "", so both mean "no worker job".
4945
+ if (String.IsNullOrEmpty(name)) return IntPtr.Zero;
4468
4946
  if (!System.Text.RegularExpressions.Regex.IsMatch(name, @"\\ALocal\\\\RsmcpStudioWorker-[a-f0-9]{32}\\z"))
4469
4947
  throw new ArgumentException("Invalid Studio test worker job name");
4470
4948
  IntPtr handle = OpenJobObjectW(5, false, name);
@@ -4640,7 +5118,7 @@ var studioLogsRoot;
4640
5118
  async function settleWindowsStudioSignIn(startedAt) {
4641
5119
  studioLogsRoot ??= (async () => {
4642
5120
  const localAppData = await windowsLocalAppDataAsync();
4643
- return localAppData ? path3.join(await toWslPathAsync(localAppData), "Roblox", "logs") : void 0;
5121
+ return localAppData ? path5.join(await toWslPathAsync(localAppData), "Roblox", "logs") : void 0;
4644
5122
  })();
4645
5123
  const logsRoot = await studioLogsRoot;
4646
5124
  if (!logsRoot)
@@ -4657,7 +5135,7 @@ async function spawnWindowsStudio(exe, args, processEnvironment, studioWorkingDi
4657
5135
  const windowsWorkingDirectory = studioWorkingDirectory ? await toStudioLaunchArgAsync(studioWorkingDirectory) : void 0;
4658
5136
  const testWorkerJobName = process.env.RSMCP_STUDIO_TEST_WORKER_JOB;
4659
5137
  const script = buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment, windowsWorkingDirectory, testWorkerJobName);
4660
- const launcher = spawn("powershell.exe", ["-NoProfile", "-Command", script], {
5138
+ const launcher = spawn2("powershell.exe", ["-NoProfile", "-Command", script], {
4661
5139
  cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
4662
5140
  stdio: ["pipe", "pipe", "pipe"],
4663
5141
  windowsHide: true
@@ -4877,21 +5355,21 @@ function resolveEntrypointDir() {
4877
5355
  if (!entrypoint)
4878
5356
  return void 0;
4879
5357
  try {
4880
- return path3.dirname(realpathSync(entrypoint));
5358
+ return path5.dirname(realpathSync(entrypoint));
4881
5359
  } catch {
4882
- return path3.dirname(path3.resolve(entrypoint));
5360
+ return path5.dirname(path5.resolve(entrypoint));
4883
5361
  }
4884
5362
  }
4885
5363
  function resolveBaseplateTemplatePath() {
4886
5364
  const entrypointDir = resolveEntrypointDir();
4887
5365
  const candidates = [
4888
5366
  ...entrypointDir ? [
4889
- path3.join(entrypointDir, "assets", BASEPLATE_TEMPLATE_NAME),
4890
- path3.join(entrypointDir, "..", "assets", BASEPLATE_TEMPLATE_NAME)
5367
+ path5.join(entrypointDir, "assets", BASEPLATE_TEMPLATE_NAME),
5368
+ path5.join(entrypointDir, "..", "assets", BASEPLATE_TEMPLATE_NAME)
4891
5369
  ] : [],
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)
5370
+ path5.join(process.cwd(), "packages", "core", "assets", BASEPLATE_TEMPLATE_NAME),
5371
+ path5.join(process.cwd(), "packages", "robloxstudio-mcp", "dist", "assets", BASEPLATE_TEMPLATE_NAME),
5372
+ path5.join(process.cwd(), "assets", BASEPLATE_TEMPLATE_NAME)
4895
5373
  ];
4896
5374
  for (const candidate of candidates) {
4897
5375
  if (existsSync2(candidate))
@@ -4913,7 +5391,7 @@ function isProcessAlive2(pid) {
4913
5391
  function sweepStaleBaseplateFiles() {
4914
5392
  let entries;
4915
5393
  try {
4916
- entries = readdirSync(BASEPLATE_TEMP_DIR);
5394
+ entries = readdirSync2(BASEPLATE_TEMP_DIR);
4917
5395
  } catch {
4918
5396
  return;
4919
5397
  }
@@ -4924,24 +5402,24 @@ function sweepStaleBaseplateFiles() {
4924
5402
  continue;
4925
5403
  if (Number(match[1]) !== process.pid && isProcessAlive2(Number(match[1])))
4926
5404
  continue;
4927
- const file = path3.join(BASEPLATE_TEMP_DIR, entry);
5405
+ const file = path5.join(BASEPLATE_TEMP_DIR, entry);
4928
5406
  try {
4929
- if (statSync(file).mtimeMs < cutoff)
4930
- rmSync(file, { force: true });
5407
+ if (statSync2(file).mtimeMs < cutoff)
5408
+ rmSync2(file, { force: true });
4931
5409
  } catch {
4932
5410
  }
4933
5411
  }
4934
5412
  }
4935
5413
  function createBaseplatePlaceFile() {
4936
- mkdirSync(BASEPLATE_TEMP_DIR, { recursive: true });
5414
+ mkdirSync2(BASEPLATE_TEMP_DIR, { recursive: true });
4937
5415
  sweepStaleBaseplateFiles();
4938
- const file = path3.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
5416
+ const file = path5.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
4939
5417
  copyFileSync(resolveBaseplateTemplatePath(), file);
4940
5418
  return file;
4941
5419
  }
4942
5420
  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));
5421
+ const resolvedFile = path5.resolve(file);
5422
+ return path5.dirname(resolvedFile) === path5.resolve(BASEPLATE_TEMP_DIR) && BASEPLATE_TEMP_NAME.test(path5.basename(resolvedFile));
4945
5423
  }
4946
5424
  function cleanupManagedBaseplateFiles(record) {
4947
5425
  if (record.source !== "baseplate" || !record.localPlaceFile)
@@ -4950,7 +5428,7 @@ function cleanupManagedBaseplateFiles(record) {
4950
5428
  return;
4951
5429
  for (const file of [record.localPlaceFile, `${record.localPlaceFile}.lock`]) {
4952
5430
  try {
4953
- rmSync(file, { force: true });
5431
+ rmSync2(file, { force: true });
4954
5432
  } catch {
4955
5433
  }
4956
5434
  }
@@ -4967,21 +5445,33 @@ function resolveStudioExeFromVersions(root) {
4967
5445
  if (!existsSync2(root)) {
4968
5446
  throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
4969
5447
  }
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);
5448
+ 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
5449
  if (candidates.length === 0) {
4972
5450
  throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
4973
5451
  }
4974
5452
  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 });
5453
+ const installationDir = path5.dirname(exe);
5454
+ const hasIncompleteDownload = readdirSync2(installationDir).some((name) => /\.crdownload$/i.test(name));
5455
+ const settings = statSync2(path5.join(installationDir, "AppSettings.xml"), { throwIfNoEntry: false });
4978
5456
  if (hasIncompleteDownload || !settings?.isFile() || settings.size === 0) {
4979
5457
  const reason = hasIncompleteDownload ? "an unfinished .crdownload download is present" : "AppSettings.xml is missing, empty, or not a regular file";
4980
5458
  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
5459
  }
4982
5460
  return exe;
4983
5461
  }
5462
+ function resolveNativeLinuxStudioExe() {
5463
+ const exe = process.env.ROBLOX_STUDIO_EXE;
5464
+ if (!exe) {
5465
+ throw new Error("Set ROBLOX_STUDIO_EXE to RobloxStudioBeta.exe inside your Wine prefix to launch Studio on native Linux.");
5466
+ }
5467
+ if (!statSync2(exe, { throwIfNoEntry: false })?.isFile()) {
5468
+ throw new Error(`ROBLOX_STUDIO_EXE does not name an existing file: ${exe}`);
5469
+ }
5470
+ return exe;
5471
+ }
4984
5472
  function resolveStudioExe() {
5473
+ if (process.platform === "linux" && isWineHost())
5474
+ return resolveNativeLinuxStudioExe();
4985
5475
  if (process.env.ROBLOX_STUDIO_EXE)
4986
5476
  return process.env.ROBLOX_STUDIO_EXE;
4987
5477
  if (process.platform === "darwin") {
@@ -4991,10 +5481,12 @@ function resolveStudioExe() {
4991
5481
  throw new Error("Roblox Studio executable auto-discovery is only supported on Windows, WSL, and macOS. Set ROBLOX_STUDIO_EXE.");
4992
5482
  }
4993
5483
  const localAppData = windowsLocalAppData();
4994
- const root = localAppData ? path3.join(toWslPath(localAppData), "Roblox", "Versions") : path3.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
5484
+ const root = localAppData ? path5.join(toWslPath(localAppData), "Roblox", "Versions") : path5.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
4995
5485
  return resolveStudioExeFromVersions(root);
4996
5486
  }
4997
5487
  async function resolveStudioExeAsync() {
5488
+ if (process.platform === "linux" && isWineHost())
5489
+ return resolveNativeLinuxStudioExe();
4998
5490
  if (process.env.ROBLOX_STUDIO_EXE)
4999
5491
  return process.env.ROBLOX_STUDIO_EXE;
5000
5492
  if (process.platform === "darwin") {
@@ -5004,7 +5496,7 @@ async function resolveStudioExeAsync() {
5004
5496
  throw new Error("Roblox Studio executable auto-discovery is only supported on Windows, WSL, and macOS. Set ROBLOX_STUDIO_EXE.");
5005
5497
  }
5006
5498
  const localAppData = await windowsLocalAppDataAsync();
5007
- const root = localAppData ? path3.join(await toWslPathAsync(localAppData), "Roblox", "Versions") : path3.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
5499
+ const root = localAppData ? path5.join(await toWslPathAsync(localAppData), "Roblox", "Versions") : path5.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
5008
5500
  return resolveStudioExeFromVersions(root);
5009
5501
  }
5010
5502
  var WINDOWS_STUDIO_PROCESS_QUERY = [
@@ -5017,10 +5509,10 @@ var WINDOWS_STUDIO_PROCESS_QUERY = [
5017
5509
  function parseWindowsStudioProcesses(output) {
5018
5510
  const parsed = JSON.parse(output);
5019
5511
  const processes = Array.isArray(parsed) ? parsed : [parsed];
5020
- if (!processes.every((value) => value !== null && typeof value === "object" && "Id" in value && typeof value.Id === "number" && Number.isSafeInteger(value.Id) && value.Id > 0 && "Name" in value && typeof value.Name === "string" && "Path" in value && typeof value.Path === "string" && "MainWindowTitle" in value && typeof value.MainWindowTitle === "string" && "StartTimeUtcFileTime" in value && typeof value.StartTimeUtcFileTime === "string" && /^[1-9]\d*$/u.test(value.StartTimeUtcFileTime))) {
5512
+ if (!processes.every((value) => value !== null && typeof value === "object" && "Id" in value && typeof value.Id === "number" && Number.isSafeInteger(value.Id) && value.Id > 0 && "Name" in value && typeof value.Name === "string" && "Path" in value && (typeof value.Path === "string" || value.Path === null) && "MainWindowTitle" in value && typeof value.MainWindowTitle === "string" && "StartTimeUtcFileTime" in value && typeof value.StartTimeUtcFileTime === "string" && /^[1-9]\d*$/u.test(value.StartTimeUtcFileTime))) {
5021
5513
  throw new Error("Malformed Roblox Studio process enumeration result.");
5022
5514
  }
5023
- return processes;
5515
+ return processes.map(({ Path, ...processInfo }) => Path === null ? processInfo : { ...processInfo, Path });
5024
5516
  }
5025
5517
  async function observeStudioProcesses(timeoutMs = 15e3) {
5026
5518
  const observedAt = Date.now();
@@ -5041,6 +5533,9 @@ async function observeStudioProcesses(timeoutMs = 15e3) {
5041
5533
  }).filter((proc) => Number.isFinite(proc.Id));
5042
5534
  return { status: "ok", observedAt, processes };
5043
5535
  }
5536
+ if (process.platform === "linux" && isWineHost()) {
5537
+ return { status: "ok", observedAt, processes: listWineStudioProcesses() };
5538
+ }
5044
5539
  if (process.platform !== "win32" && !isWsl()) {
5045
5540
  return { status: "ok", observedAt, processes: [] };
5046
5541
  }
@@ -5057,7 +5552,7 @@ async function observeStudioProcesses(timeoutMs = 15e3) {
5057
5552
  async function currentBootIdAsync() {
5058
5553
  if (process.platform === "linux") {
5059
5554
  try {
5060
- return readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
5555
+ return readFileSync3("/proc/sys/kernel/random/boot_id", "utf8").trim();
5061
5556
  } catch {
5062
5557
  }
5063
5558
  }
@@ -5108,11 +5603,11 @@ function buildStudioLaunchArgs(options) {
5108
5603
  ];
5109
5604
  }
5110
5605
  }
5111
- function delay3(ms) {
5606
+ function delay4(ms) {
5112
5607
  return new Promise((resolve6) => setTimeout(resolve6, ms));
5113
5608
  }
5114
5609
  function basenameAny(filePath) {
5115
- return path3.basename(filePath.replace(/\\/g, "/"));
5610
+ return path5.basename(filePath.replace(/\\/g, "/"));
5116
5611
  }
5117
5612
  var StudioInstanceManager = class {
5118
5613
  managedByInstanceId = /* @__PURE__ */ new Map();
@@ -5364,8 +5859,15 @@ var StudioInstanceManager = class {
5364
5859
  proc = await this.processAdapter.spawnStudio(exe, args, spawnOptions);
5365
5860
  } else if (lifecycleCapabilities.processIdentity.launcher === "windows-retained" || lifecycleCapabilities.processIdentity.launcher === "wsl-windows-retained") {
5366
5861
  proc = await this.windowsStudioLauncher(exe, args, processEnvironment, studioWorkingDirectory);
5862
+ } else if (lifecycleCapabilities.processIdentity.launcher === "wine-retained") {
5863
+ const wineLauncher = (this.platformCapabilities ?? getStudioPlatformCapabilities()).wineLauncher;
5864
+ if (!wineLauncher)
5865
+ throw new Error("The Wine Studio launcher path is unavailable.");
5866
+ proc = await spawnWineStudio(wineLauncher, exe, args, spawnOptions, {
5867
+ logDir: path5.join(this.registry.dir, "wine-launches")
5868
+ });
5367
5869
  } else {
5368
- const child = spawn(exe, args, spawnOptions);
5870
+ const child = spawn2(exe, args, spawnOptions);
5369
5871
  proc = {
5370
5872
  pid: child.pid,
5371
5873
  nativePid: child.pid,
@@ -5415,7 +5917,7 @@ var StudioInstanceManager = class {
5415
5917
  }
5416
5918
  const launchedAt = Date.now();
5417
5919
  const record = {
5418
- recordId: randomUUID3(),
5920
+ recordId: randomUUID4(),
5419
5921
  source: options.source,
5420
5922
  nativeProcessId: proc.nativePid,
5421
5923
  nativeProcessStartedAt: proc.nativeStartedAt,
@@ -5485,7 +5987,7 @@ var StudioInstanceManager = class {
5485
5987
  await this.persist(record);
5486
5988
  break;
5487
5989
  }
5488
- await delay3(250);
5990
+ await delay4(250);
5489
5991
  }
5490
5992
  if (record.nativeProcessId === void 0 && process.platform !== "win32" && !isWsl()) {
5491
5993
  record.nativeProcessId = proc.pid;
@@ -5667,6 +6169,11 @@ var StudioInstanceManager = class {
5667
6169
  throw new Error("Cannot stop a Windows Studio process without its creation-time identity.");
5668
6170
  }
5669
6171
  await stopWindowsStudio(processId, startedAt, Math.max(1, deadline - Date.now()));
6172
+ } else if (isWineHost()) {
6173
+ if (startedAt === void 0) {
6174
+ throw new Error("Cannot stop a Wine Studio process without its creation-time identity.");
6175
+ }
6176
+ await stopWineStudio(processId, startedAt, Math.max(1, deadline - Date.now()));
5670
6177
  } else {
5671
6178
  try {
5672
6179
  process.kill(processId, "SIGTERM");
@@ -5687,7 +6194,7 @@ var StudioInstanceManager = class {
5687
6194
  if (remaining <= 0) {
5688
6195
  throw new StudioCloseVerificationError(`Timed out closing Studio process ${processId}: the process is still running. Close can be retried.`, observation);
5689
6196
  }
5690
- await delay3(Math.min(100, remaining));
6197
+ await delay4(Math.min(100, remaining));
5691
6198
  if (Date.now() >= deadline) {
5692
6199
  throw new StudioCloseVerificationError(`Timed out closing Studio process ${processId}: the process is still running. Close can be retried.`, observation);
5693
6200
  }
@@ -5784,15 +6291,39 @@ var StudioInstanceManager = class {
5784
6291
  confirmedExitGraceMs: this.confirmedExitGraceMs
5785
6292
  };
5786
6293
  }
6294
+ isOrphanedUnreleasedLaunch(registryRecord, currentBootId) {
6295
+ const ownerPid = registryRecord.ownerPid;
6296
+ return !(registryRecord.processAuthorizationState === "released" || this.launchControls.has(registryRecord.recordId) || registryRecord.bootId !== currentBootId || ownerPid === void 0 || ownerPid !== process.pid && isProcessAlive2(ownerPid));
6297
+ }
6298
+ // A broker that dies between spawn and authorization leaves its Wine launch
6299
+ // gate stopped. The gate is /bin/sh, not Studio, so the orphan cleanup below
6300
+ // never sees it in a Studio snapshot.
6301
+ async reapOrphanedWineLaunchGates(currentBootId) {
6302
+ if (this.getLifecycleCapabilities().processIdentity.launcher !== "wine-retained")
6303
+ return;
6304
+ for (const registryRecord of await this.registry.listOpenUnchecked()) {
6305
+ const processId = registryRecord.nativeProcessId;
6306
+ const startedAt = registryRecord.nativeProcessStartedAt;
6307
+ if (registryRecord.processAuthorizationState !== "pending" || !processId || !startedAt || !this.isOrphanedUnreleasedLaunch(registryRecord, currentBootId)) {
6308
+ continue;
6309
+ }
6310
+ try {
6311
+ if (killOrphanedWineLaunchGate(processId, startedAt)) {
6312
+ console.error(`[studio] Killed Wine launch gate ${processId}, left stopped by a broker that exited before authorizing it.`);
6313
+ }
6314
+ } catch (error) {
6315
+ console.error(`[studio] Could not check orphaned Wine launch gate ${processId}: ${error instanceof Error ? error.message : String(error)}`);
6316
+ }
6317
+ }
6318
+ }
5787
6319
  async sweepRegistry(snapshot) {
5788
6320
  const sweepOptions = await this.registrySweepOptions(snapshot);
6321
+ await this.reapOrphanedWineLaunchGates(sweepOptions.currentBootId);
5789
6322
  await this.registry.sweep(sweepOptions);
5790
6323
  const persisted = await this.registry.listOpenUnchecked();
5791
6324
  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)) {
6325
+ if (!this.isOrphanedUnreleasedLaunch(registryRecord, sweepOptions.currentBootId))
5794
6326
  continue;
5795
- }
5796
6327
  const record = this.fromRegistryRecord(registryRecord);
5797
6328
  if (this.observeRecord(record, snapshot).status !== "running")
5798
6329
  continue;
@@ -5855,13 +6386,13 @@ var StudioInstanceManager = class {
5855
6386
  const processId = record.nativeProcessId ?? record.spawnPid;
5856
6387
  if (record.spawnPid && record.spawnPid === processId && studioProcess.Id === processId)
5857
6388
  return true;
5858
- const processPath = studioProcess.Path ? path3.normalize(studioProcess.Path).toLowerCase() : "";
5859
- const exePath = record.exe ? path3.normalize(record.exe).toLowerCase() : "";
6389
+ const processPath = studioProcess.Path ? path5.normalize(studioProcess.Path).toLowerCase() : "";
6390
+ const exePath = record.exe ? path5.normalize(record.exe).toLowerCase() : "";
5860
6391
  if (processPath && exePath && (processPath === exePath || basenameAny(processPath) === basenameAny(exePath))) {
5861
6392
  return true;
5862
6393
  }
5863
6394
  const commandLine = studioProcess.CommandLine ?? "";
5864
- if (record.localPlaceFile && commandLine.includes(path3.basename(record.localPlaceFile)))
6395
+ if (record.localPlaceFile && commandLine.includes(path5.basename(record.localPlaceFile)))
5865
6396
  return true;
5866
6397
  if (record.placeId !== void 0 && commandLine.includes(String(record.placeId)))
5867
6398
  return true;
@@ -5992,6 +6523,8 @@ var StudioInstanceManager = class {
5992
6523
  if (record.closedAt !== void 0)
5993
6524
  return;
5994
6525
  const previousObservationAt = record.lastProcessObservationAt;
6526
+ if (previousObservationAt !== void 0 && observation.observedAt < previousObservationAt)
6527
+ return;
5995
6528
  record.lastProcessObservationAt = observation.observedAt;
5996
6529
  if (observation.status === "unknown") {
5997
6530
  record.processObservationStatus = "unknown";
@@ -6180,6 +6713,7 @@ var OPEN_WORLD_TOOLS = /* @__PURE__ */ new Set([
6180
6713
  "get_asset_thumbnail",
6181
6714
  "get_roblox_docs",
6182
6715
  "insert_asset",
6716
+ "manage_monetization",
6183
6717
  "preview_asset",
6184
6718
  "search_assets",
6185
6719
  "upload_asset"
@@ -6324,7 +6858,7 @@ function serverInstructions(definitions) {
6324
6858
  const has = (...toolNames) => toolNames.every((name) => names.has(name));
6325
6859
  const instructions = ["Use canonical DataModel paths returned by tools."];
6326
6860
  if (has("get_connected_instances")) {
6327
- instructions.push("When more than one Studio process scope is connected, call get_connected_instances and pass either a top-level instance id or a multiplayer group's role-suffixed instance id as instance_id.");
6861
+ instructions.push("When more than one Studio process scope is connected, call get_connected_instances and pass either a top-level instance id or a multiplayer group's role-suffixed instance id as instance_id. A role-suffixed id selects that server or client as the default target; a conflicting single-peer target is rejected.");
6328
6862
  }
6329
6863
  if (has("get_request_status", "execute_luau", "set_properties")) {
6330
6864
  instructions.push("Supply a unique operation_id to execute_luau or set_properties when retry safety matters. After a timeout, query get_request_status with that ID before retrying. Identical arguments reuse a retained outcome; changed arguments are rejected. Recovery and deduplication are bounded to the current server session and five-minute retention window; result payloads may be evicted earlier. Unknown status does not mean unexecuted, and cancellation cannot roll back mutations.", "Request stages are queued, dispatched, executing (plugin handler entered), and response_delivery (handler returned or admission rejected). executionOutcome is separate from waiter state and delivery outcome; handler observations do not prove user Luau instructions ran. A waiter timeout is not an execution deadline or rollback. Neither missing progress nor connection loss proves completion.");
@@ -6392,17 +6926,17 @@ function createToolHttpHandler(options) {
6392
6926
 
6393
6927
  // ../core/dist/auth.js
6394
6928
  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";
6929
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, linkSync as linkSync2, unlinkSync as unlinkSync2 } from "fs";
6930
+ import { join as join5, dirname as dirname2 } from "path";
6397
6931
  import { homedir as homedir3 } from "os";
6398
6932
  function authTokenFilePath() {
6399
- return join4(homedir3(), ".robloxstudio-mcp", "auth-token");
6933
+ return join5(homedir3(), ".robloxstudio-mcp", "auth-token");
6400
6934
  }
6401
6935
  var EMPTY_TOKEN_RETRY_MS = 25;
6402
6936
  var EMPTY_TOKEN_RETRIES = 20;
6403
6937
  function readTokenFile(filePath) {
6404
6938
  for (let attempt = 0; ; attempt++) {
6405
- const token = readFileSync3(filePath, "utf8").trim();
6939
+ const token = readFileSync4(filePath, "utf8").trim();
6406
6940
  if (token || attempt >= EMPTY_TOKEN_RETRIES)
6407
6941
  return token;
6408
6942
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, EMPTY_TOKEN_RETRY_MS);
@@ -6429,12 +6963,12 @@ function resolveAuthToken() {
6429
6963
  throw error;
6430
6964
  }
6431
6965
  const fresh = randomBytes(32).toString("hex");
6432
- mkdirSync2(dirname2(filePath), { recursive: true, mode: 448 });
6966
+ mkdirSync3(dirname2(filePath), { recursive: true, mode: 448 });
6433
6967
  const stagingPath = `${filePath}.${process.pid}.${randomBytes(16).toString("hex")}.tmp`;
6434
- writeFileSync(stagingPath, fresh + "\n", { encoding: "utf8", mode: 384, flag: "wx" });
6968
+ writeFileSync2(stagingPath, fresh + "\n", { encoding: "utf8", mode: 384, flag: "wx" });
6435
6969
  try {
6436
6970
  try {
6437
- linkSync(stagingPath, filePath);
6971
+ linkSync2(stagingPath, filePath);
6438
6972
  } catch (error) {
6439
6973
  if (error.code !== "EEXIST")
6440
6974
  throw error;
@@ -6445,7 +6979,7 @@ function resolveAuthToken() {
6445
6979
  return { token, source: "file", filePath };
6446
6980
  } finally {
6447
6981
  try {
6448
- unlinkSync(stagingPath);
6982
+ unlinkSync2(stagingPath);
6449
6983
  } catch {
6450
6984
  }
6451
6985
  }
@@ -6465,6 +6999,7 @@ var HTTP_BODY_LIMIT_BYTES = 50 * 1024 * 1024;
6465
6999
 
6466
7000
  // ../core/dist/studio-transport.js
6467
7001
  var HEARTBEAT_INTERVAL_MS = 1e4;
7002
+ var PONG_TIMEOUT_MS = 3e4;
6468
7003
  var MAX_PENDING_ACKS = 128;
6469
7004
  var STUDIO_PROTOCOL_VERSION = 1;
6470
7005
  var MAX_ACTIVE_STUDIO_SOCKETS = 64;
@@ -6512,13 +7047,20 @@ var WebSocketStudioTransport = class {
6512
7047
  settling: false,
6513
7048
  statusPending: true,
6514
7049
  heartbeatPending: false,
7050
+ lastSeenAt: Date.now(),
6515
7051
  acknowledgements: /* @__PURE__ */ new Map(),
6516
7052
  onClose: () => {
6517
7053
  this.closeSocket(connection);
6518
7054
  this.finishClose(connection);
6519
7055
  },
6520
7056
  onError: () => this.closeSocket(connection, 1011, "socket_error"),
6521
- onMessage: (data, isBinary) => this.receive(connection, data, isBinary)
7057
+ onMessage: (data, isBinary) => this.receive(connection, data, isBinary),
7058
+ onPong: () => {
7059
+ if (!this.isCurrent(connection))
7060
+ return;
7061
+ connection.lastSeenAt = Date.now();
7062
+ this.queue.updatePeerActivity(transportPeerId);
7063
+ }
6522
7064
  };
6523
7065
  this.queue.setDeliveryActive(transportPeerId, claimOwner, true);
6524
7066
  const replaced = this.sockets.get(transportPeerId);
@@ -6529,10 +7071,20 @@ var WebSocketStudioTransport = class {
6529
7071
  socket.on("close", connection.onClose);
6530
7072
  socket.on("error", connection.onError);
6531
7073
  socket.on("message", connection.onMessage);
7074
+ socket.on("pong", connection.onPong);
6532
7075
  connection.heartbeatTimer = setInterval(() => {
6533
7076
  if (!this.isCurrent(connection))
6534
7077
  return;
6535
- this.queue.updatePeerActivity(transportPeerId);
7078
+ if (Date.now() - connection.lastSeenAt >= PONG_TIMEOUT_MS) {
7079
+ this.closeSocket(connection, 1001, "pong_timeout");
7080
+ return;
7081
+ }
7082
+ try {
7083
+ connection.socket.ping();
7084
+ } catch {
7085
+ this.closeSocket(connection, 1011, "ping_failed");
7086
+ return;
7087
+ }
6536
7088
  connection.statusPending = true;
6537
7089
  connection.heartbeatPending = true;
6538
7090
  this.pump(connection);
@@ -6590,6 +7142,11 @@ var WebSocketStudioTransport = class {
6590
7142
  this.closeSocket(connection, 1007, "invalid_json");
6591
7143
  return;
6592
7144
  }
7145
+ if (message !== null && typeof message === "object" && !Array.isArray(message) && "kind" in message && message.kind === "disconnect") {
7146
+ this.queue.unregisterPeer(connection.transportPeerId);
7147
+ this.closeSocket(connection, 1e3, "peer_unregistered");
7148
+ return;
7149
+ }
6593
7150
  if (message === null || typeof message !== "object" || Array.isArray(message) || !("kind" in message) || message.kind !== "response" && message.kind !== "progress" || !("requestId" in message) || typeof message.requestId !== "string" || message.requestId.length === 0 || message.requestId.length > 1024) {
6594
7151
  this.closeSocket(connection, 1008, "invalid_response");
6595
7152
  return;
@@ -6730,6 +7287,7 @@ var WebSocketStudioTransport = class {
6730
7287
  connection.closed = true;
6731
7288
  clearInterval(connection.heartbeatTimer);
6732
7289
  connection.socket.removeListener("message", connection.onMessage);
7290
+ connection.socket.removeListener("pong", connection.onPong);
6733
7291
  connection.acknowledgements.clear();
6734
7292
  if (this.sockets.get(connection.transportPeerId) === connection)
6735
7293
  this.sockets.delete(connection.transportPeerId);
@@ -6800,7 +7358,8 @@ var TOOL_PROXY_ENDPOINTS = {
6800
7358
  get_scene_analysis: ["/api/get-scene-analysis"],
6801
7359
  export_rbxm: ["/api/export-rbxm"],
6802
7360
  import_rbxm: ["/api/import-rbxm"],
6803
- find_and_replace_in_scripts: ["/api/find-and-replace-in-scripts"]
7361
+ find_and_replace_in_scripts: ["/api/find-and-replace-in-scripts"],
7362
+ manage_monetization: ["/api/place-info"]
6804
7363
  };
6805
7364
  function toPassivePeer(peer) {
6806
7365
  return {
@@ -6918,6 +7477,7 @@ var TOOL_HANDLERS = {
6918
7477
  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
7478
  capture_device_matrix: (tools, body) => tools.captureDeviceMatrix(body.entries, body.target, body.format, body.quality, body.settleSeconds, body.restoreAfter, body.instance_id),
6920
7479
  manage_instance: (tools, body) => tools.manageInstance(body),
7480
+ manage_monetization: (tools, body) => tools.manageMonetization(body, body.instance_id),
6921
7481
  solo_playtest: (tools, body) => tools.soloPlaytest(body.action, body.mode, body.timeout, body.instance_id),
6922
7482
  multiplayer_playtest: (tools, body) => tools.multiplayerPlaytest(body.action, body.numPlayers, body.target, body.testArgs, body.value, body.timeout, body.instance_id),
6923
7483
  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 +7695,8 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
7135
7695
  });
7136
7696
  const authToken = security?.authToken;
7137
7697
  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";
7698
+ const path11 = requestPath.toLowerCase().replace(/\/+$/, "");
7699
+ 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
7700
  };
7141
7701
  app.use((req, res, next) => {
7142
7702
  if (!authToken || !authRequired(req.path)) {
@@ -7290,9 +7850,9 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
7290
7850
  });
7291
7851
  return;
7292
7852
  }
7293
- const isClientRole = role === "client" || /^client-[1-9]\d*$/.test(role);
7853
+ const isClientRole2 = role === "client" || /^client-[1-9]\d*$/.test(role);
7294
7854
  const isProxiedPeer = transportPeerId !== peerId;
7295
- if (isProxiedPeer && !isClientRole || !isProxiedPeer && isClientRole || !isClientRole && role !== "edit" && role !== "server") {
7855
+ if (isProxiedPeer && !isClientRole2 || !isProxiedPeer && isClientRole2 || !isClientRole2 && role !== "edit" && role !== "server") {
7296
7856
  res.status(400).json({
7297
7857
  success: false,
7298
7858
  error: "invalid_peer_topology",
@@ -7737,6 +8297,56 @@ var StudioHttpClient = class {
7737
8297
  };
7738
8298
 
7739
8299
  // ../core/dist/opencloud-client.js
8300
+ var MONETIZATION_APIS = {
8301
+ developer_product: {
8302
+ label: "developer product",
8303
+ collectionPath: (universeId) => `/developer-products/v2/universes/${universeId}/developer-products`,
8304
+ listField: "developerProducts",
8305
+ idField: "productId",
8306
+ iconField: "iconImageAssetId"
8307
+ },
8308
+ game_pass: {
8309
+ label: "game pass",
8310
+ collectionPath: (universeId) => `/game-passes/v1/universes/${universeId}/game-passes`,
8311
+ listField: "gamePasses",
8312
+ idField: "gamePassId",
8313
+ iconField: "iconAssetId"
8314
+ }
8315
+ };
8316
+ function readField(source, key) {
8317
+ return typeof source === "object" && source !== null ? Reflect.get(source, key) : void 0;
8318
+ }
8319
+ function positiveSafeInteger(value) {
8320
+ const parsed = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : value;
8321
+ return typeof parsed === "number" && Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
8322
+ }
8323
+ function normalizeMonetizationItem(kind, universeId, value) {
8324
+ const api = MONETIZATION_APIS[kind];
8325
+ const id = positiveSafeInteger(readField(value, api.idField));
8326
+ if (id === void 0) {
8327
+ throw new Error(`Open Cloud returned a ${api.label} without a valid ${api.idField}.`);
8328
+ }
8329
+ const name = readField(value, "name");
8330
+ const description = readField(value, "description");
8331
+ const price = readField(readField(value, "priceInformation"), "defaultPriceInRobux");
8332
+ const createdAt = readField(value, "createdTimestamp");
8333
+ const updatedAt = readField(value, "updatedTimestamp");
8334
+ const immutable = readField(value, "isImmutable");
8335
+ return {
8336
+ kind,
8337
+ id,
8338
+ universeId: positiveSafeInteger(readField(value, "universeId")) ?? universeId,
8339
+ name: typeof name === "string" ? name : "",
8340
+ description: typeof description === "string" ? description : "",
8341
+ forSale: readField(value, "isForSale") === true,
8342
+ price: typeof price === "number" && Number.isSafeInteger(price) && price >= 0 ? price : null,
8343
+ managedPricing: readField(value, "isManagedPricingEnabled") === true,
8344
+ iconAssetId: positiveSafeInteger(readField(value, api.iconField)) ?? null,
8345
+ ...typeof createdAt === "string" ? { createdAt } : {},
8346
+ ...typeof updatedAt === "string" ? { updatedAt } : {},
8347
+ ...kind === "developer_product" && typeof immutable === "boolean" ? { immutable } : {}
8348
+ };
8349
+ }
7740
8350
  function detectAudioMimeType(data) {
7741
8351
  if (data.length >= 4 && data.subarray(0, 4).toString("ascii") === "OggS") {
7742
8352
  return "audio/ogg";
@@ -7752,6 +8362,68 @@ function detectAudioMimeType(data) {
7752
8362
  }
7753
8363
  return void 0;
7754
8364
  }
8365
+ var OpenCloudRequestError = class extends Error {
8366
+ status;
8367
+ details;
8368
+ constructor(message, status, details = {}) {
8369
+ super(message);
8370
+ this.status = status;
8371
+ this.details = details;
8372
+ this.name = "OpenCloudRequestError";
8373
+ }
8374
+ };
8375
+ async function openCloudResponseError(response) {
8376
+ const errorBody = await response.text();
8377
+ let parsed;
8378
+ try {
8379
+ parsed = JSON.parse(errorBody);
8380
+ } catch {
8381
+ parsed = void 0;
8382
+ }
8383
+ const text = (key) => {
8384
+ const value = readField(parsed, key);
8385
+ return typeof value === "string" && value ? value : void 0;
8386
+ };
8387
+ const errorMessage2 = text("detail") ?? text("message") ?? text("errorMessage") ?? errorBody;
8388
+ const errorCode3 = readField(parsed, "errorCode");
8389
+ const field = text("field");
8390
+ const hint = text("hint");
8391
+ const details = {
8392
+ ...typeof errorCode3 === "string" || typeof errorCode3 === "number" ? { errorCode: errorCode3 } : {},
8393
+ ...field ? { field } : {},
8394
+ ...hint ? { hint } : {}
8395
+ };
8396
+ const { status } = response;
8397
+ let message;
8398
+ if (status === 401) {
8399
+ message = "Invalid or expired API key";
8400
+ } else if (status === 403) {
8401
+ message = `API key lacks required permissions: ${errorMessage2}`;
8402
+ } else if (status === 429) {
8403
+ message = "Rate limit exceeded. Please try again later.";
8404
+ } else {
8405
+ message = `Open Cloud API error (${status}): ${errorMessage2}`;
8406
+ }
8407
+ return new OpenCloudRequestError(message, status, details);
8408
+ }
8409
+ function monetizationForm(changes) {
8410
+ const form = new FormData();
8411
+ if (changes.name !== void 0)
8412
+ form.append("name", changes.name);
8413
+ if (changes.description !== void 0)
8414
+ form.append("description", changes.description);
8415
+ if (changes.price !== void 0)
8416
+ form.append("price", String(changes.price));
8417
+ if (changes.forSale !== void 0)
8418
+ form.append("isForSale", String(changes.forSale));
8419
+ if (changes.managedPricing !== void 0) {
8420
+ form.append("isManagedPricingEnabled", String(changes.managedPricing));
8421
+ }
8422
+ if (changes.icon) {
8423
+ form.append("imageFile", new Blob([new Uint8Array(changes.icon.data)], { type: changes.icon.mimeType }), changes.icon.fileName);
8424
+ }
8425
+ return form;
8426
+ }
7755
8427
  var OpenCloudClient = class {
7756
8428
  apiKey;
7757
8429
  baseUrl;
@@ -7792,25 +8464,8 @@ var OpenCloudClient = class {
7792
8464
  body: body ? JSON.stringify(body) : void 0,
7793
8465
  signal: controller.signal
7794
8466
  });
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
- }
8467
+ if (!response.ok)
8468
+ throw await openCloudResponseError(response);
7814
8469
  return await response.json();
7815
8470
  } catch (error) {
7816
8471
  if (controller.signal.aborted)
@@ -7855,6 +8510,36 @@ var OpenCloudClient = class {
7855
8510
  }
7856
8511
  });
7857
8512
  }
8513
+ async listMonetizationItems(kind, universeId, pageToken) {
8514
+ const api = MONETIZATION_APIS[kind];
8515
+ const response = await this.request(`${api.collectionPath(universeId)}/creator`, {
8516
+ params: { pageToken }
8517
+ });
8518
+ const rows = readField(response, api.listField);
8519
+ if (!Array.isArray(rows)) {
8520
+ throw new Error(`Open Cloud returned a ${api.label} list without ${api.listField}.`);
8521
+ }
8522
+ const nextPageToken = readField(response, "nextPageToken");
8523
+ return {
8524
+ items: rows.map((row) => normalizeMonetizationItem(kind, universeId, row)),
8525
+ ...typeof nextPageToken === "string" && nextPageToken ? { nextPageToken } : {}
8526
+ };
8527
+ }
8528
+ async getMonetizationItem(kind, universeId, id) {
8529
+ const path11 = `${MONETIZATION_APIS[kind].collectionPath(universeId)}/${id}/creator`;
8530
+ return normalizeMonetizationItem(kind, universeId, await this.request(path11));
8531
+ }
8532
+ async createMonetizationItem(kind, universeId, item) {
8533
+ const response = await this.requestMultipart(MONETIZATION_APIS[kind].collectionPath(universeId), monetizationForm(item));
8534
+ return normalizeMonetizationItem(kind, universeId, response);
8535
+ }
8536
+ /** Changes only the given fields. Roblox answers with an empty 204. */
8537
+ async updateMonetizationItem(kind, universeId, id, changes) {
8538
+ const path11 = `${MONETIZATION_APIS[kind].collectionPath(universeId)}/${id}`;
8539
+ await this.sendMultipart(path11, monetizationForm(changes), "PATCH", async (response) => {
8540
+ await response.arrayBuffer();
8541
+ });
8542
+ }
7858
8543
  async getAssetThumbnail(assetId, size = "420x420") {
7859
8544
  const url = `https://thumbnails.roblox.com/v1/assets?assetIds=${assetId}&size=${size}&format=Png`;
7860
8545
  try {
@@ -7904,7 +8589,7 @@ var OpenCloudClient = class {
7904
8589
  }
7905
8590
  async downloadAudioAssetContent(assetId, maxBytes) {
7906
8591
  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.");
8592
+ 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
8593
  }
7909
8594
  if (!Number.isSafeInteger(assetId) || assetId <= 0) {
7910
8595
  throw new Error("Audio asset ID must be a positive integer.");
@@ -7922,7 +8607,8 @@ var OpenCloudClient = class {
7922
8607
  signal: controller.signal
7923
8608
  });
7924
8609
  if (!deliveryResponse.ok) {
7925
- throw new Error(`Roblox asset delivery request failed (${deliveryResponse.status}).`);
8610
+ const scopeHint = deliveryResponse.status === 403 ? " The Open Cloud API key lacks the legacy-asset:manage scope required to download audio." : "";
8611
+ throw new Error(`Roblox asset delivery request failed (${deliveryResponse.status}).${scopeHint}`);
7926
8612
  }
7927
8613
  const delivery = await deliveryResponse.json();
7928
8614
  if (!delivery.location) {
@@ -8016,7 +8702,11 @@ var OpenCloudClient = class {
8016
8702
  }
8017
8703
  return mimeTypes[ext];
8018
8704
  }
8019
- async requestMultipart(endpoint, formData) {
8705
+ requestMultipart(endpoint, formData) {
8706
+ return this.sendMultipart(endpoint, formData, "POST", async (response) => await response.json());
8707
+ }
8708
+ // readResponse runs inside the deadline, so a stalled body still times out.
8709
+ async sendMultipart(endpoint, formData, method, readResponse) {
8020
8710
  if (!this.apiKey) {
8021
8711
  throw new Error("Open Cloud API key not configured. Set ROBLOX_OPEN_CLOUD_API_KEY environment variable.");
8022
8712
  }
@@ -8025,31 +8715,14 @@ var OpenCloudClient = class {
8025
8715
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
8026
8716
  try {
8027
8717
  const response = await fetch(url, {
8028
- method: "POST",
8718
+ method,
8029
8719
  headers: { "x-api-key": this.apiKey },
8030
8720
  body: formData,
8031
8721
  signal: controller.signal
8032
8722
  });
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();
8723
+ if (!response.ok)
8724
+ throw await openCloudResponseError(response);
8725
+ return await readResponse(response);
8053
8726
  } catch (error) {
8054
8727
  if (controller.signal.aborted)
8055
8728
  throw new Error("Request timed out");
@@ -8247,9 +8920,54 @@ var RobloxCookieClient = class {
8247
8920
  };
8248
8921
 
8249
8922
  // ../core/dist/image-decode.js
8250
- import * as fs2 from "fs";
8251
- import * as path4 from "path";
8252
8923
  import { inflateSync } from "zlib";
8924
+
8925
+ // ../core/dist/local-file.js
8926
+ import * as fs2 from "fs";
8927
+ import * as path6 from "path";
8928
+ function errorCode(error) {
8929
+ return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
8930
+ }
8931
+ function readRegularFileWithinLimit(filePath, argumentName, validateSize) {
8932
+ let descriptor;
8933
+ try {
8934
+ const flags = fs2.constants.O_RDONLY | (fs2.constants.O_NONBLOCK ?? 0);
8935
+ descriptor = fs2.openSync(path6.resolve(filePath), flags);
8936
+ } catch (error) {
8937
+ const code = errorCode(error);
8938
+ if (code === "ENOENT") {
8939
+ throw new Error(`${argumentName} not found: ${filePath}`);
8940
+ }
8941
+ if (code === "EISDIR") {
8942
+ throw new Error(`${argumentName} must reference a regular file: ${filePath}`);
8943
+ }
8944
+ throw error;
8945
+ }
8946
+ try {
8947
+ const stats = fs2.fstatSync(descriptor);
8948
+ if (!stats.isFile()) {
8949
+ throw new Error(`${argumentName} must reference a regular file: ${filePath}`);
8950
+ }
8951
+ validateSize(stats.size);
8952
+ const data = Buffer.allocUnsafe(stats.size + 1);
8953
+ let offset = 0;
8954
+ while (offset < data.length) {
8955
+ const bytesRead = fs2.readSync(descriptor, data, offset, data.length - offset, offset);
8956
+ if (bytesRead === 0)
8957
+ break;
8958
+ offset += bytesRead;
8959
+ }
8960
+ if (offset !== stats.size) {
8961
+ validateSize(offset);
8962
+ throw new Error(`${argumentName} size changed while reading: ${filePath}`);
8963
+ }
8964
+ return data.subarray(0, offset);
8965
+ } finally {
8966
+ fs2.closeSync(descriptor);
8967
+ }
8968
+ }
8969
+
8970
+ // ../core/dist/image-decode.js
8253
8971
  var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
8254
8972
  var MAX_EDITABLE_IMAGE_DIMENSION = 1024;
8255
8973
  var MAX_PNG_DIMENSION = 16 * 1024;
@@ -8313,40 +9031,6 @@ function getBase64DecodedLength(encoded) {
8313
9031
  }
8314
9032
  return decodedLength;
8315
9033
  }
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
9034
  function validateSourceDimensions(width, height, maxPixels) {
8351
9035
  if (width <= 0 || height <= 0) {
8352
9036
  throw new Error("PNG IHDR dimensions must be positive.");
@@ -8580,14 +9264,13 @@ function decodePngBase64ToRgba(encoded) {
8580
9264
  return decodePngToRgba(data);
8581
9265
  }
8582
9266
  function decodeImagePathToRgba(imagePath) {
8583
- const resolved = path4.resolve(imagePath);
8584
- return decodePngToRgba(readPngFileWithinLimit(resolved, imagePath));
9267
+ return decodePngToRgba(readRegularFileWithinLimit(imagePath, "image_path", validatePngInputSize));
8585
9268
  }
8586
9269
 
8587
9270
  // ../core/dist/studio-skills.js
8588
9271
  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";
9272
+ import { existsSync as existsSync3, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
9273
+ import * as path7 from "path";
8591
9274
 
8592
9275
  // ../../node_modules/fzstd/esm/index.mjs
8593
9276
  var ab = ArrayBuffer;
@@ -9480,12 +10163,12 @@ function discoverNamedFile(root, fileName, depth = 0) {
9480
10163
  const matches = [];
9481
10164
  let entries;
9482
10165
  try {
9483
- entries = readdirSync2(root, { withFileTypes: true });
10166
+ entries = readdirSync3(root, { withFileTypes: true });
9484
10167
  } catch {
9485
10168
  return matches;
9486
10169
  }
9487
10170
  for (const entry of entries) {
9488
- const entryPath = path5.join(root, entry.name);
10171
+ const entryPath = path7.join(root, entry.name);
9489
10172
  if (entry.isFile() && entry.name.toLowerCase() === fileName.toLowerCase()) {
9490
10173
  matches.push(entryPath);
9491
10174
  } else if (entry.isDirectory()) {
@@ -9502,13 +10185,13 @@ function resolveAssistantBundlePath(studioExe) {
9502
10185
  }
9503
10186
  return override;
9504
10187
  }
9505
- const exeDirectory = path5.dirname(studioExe ?? resolveStudioExe());
10188
+ const exeDirectory = path7.dirname(studioExe ?? resolveStudioExe());
9506
10189
  const roots = [
9507
- path5.join(exeDirectory, "BuiltInStandalonePlugins"),
9508
- path5.resolve(exeDirectory, "..", "Resources", "BuiltInStandalonePlugins"),
9509
- path5.resolve(exeDirectory, "..", "..", "OTAPlugins")
10190
+ path7.join(exeDirectory, "BuiltInStandalonePlugins"),
10191
+ path7.resolve(exeDirectory, "..", "Resources", "BuiltInStandalonePlugins"),
10192
+ path7.resolve(exeDirectory, "..", "..", "OTAPlugins")
9510
10193
  ];
9511
- const direct = path5.join(exeDirectory, "BuiltInStandalonePlugins", "Optimized_Embedded_Signature", "Assistant.rbxm");
10194
+ const direct = path7.join(exeDirectory, "BuiltInStandalonePlugins", "Optimized_Embedded_Signature", "Assistant.rbxm");
9512
10195
  const candidates = /* @__PURE__ */ new Set();
9513
10196
  if (existsSync3(direct))
9514
10197
  candidates.add(direct);
@@ -9516,7 +10199,7 @@ function resolveAssistantBundlePath(studioExe) {
9516
10199
  for (const candidate of discoverNamedFile(root, "Assistant.rbxm"))
9517
10200
  candidates.add(candidate);
9518
10201
  }
9519
- const newest = [...candidates].map((candidate) => ({ candidate, modifiedAt: statSync2(candidate).mtimeMs })).sort((left, right) => right.modifiedAt - left.modifiedAt)[0]?.candidate;
10202
+ const newest = [...candidates].map((candidate) => ({ candidate, modifiedAt: statSync3(candidate).mtimeMs })).sort((left, right) => right.modifiedAt - left.modifiedAt)[0]?.candidate;
9520
10203
  if (!newest) {
9521
10204
  throw new Error(`Studio Assistant bundle not found for ${studioExe}. Set ROBLOX_STUDIO_ASSISTANT_BUNDLE to the installed Assistant.rbxm path.`);
9522
10205
  }
@@ -9524,16 +10207,16 @@ function resolveAssistantBundlePath(studioExe) {
9524
10207
  }
9525
10208
  var cachedBundle;
9526
10209
  function loadBuiltInStudioSkills(bundlePath = resolveAssistantBundlePath()) {
9527
- const stats = statSync2(bundlePath);
10210
+ const stats = statSync3(bundlePath);
9528
10211
  if (cachedBundle?.path === bundlePath && cachedBundle.modifiedAt === stats.mtimeMs && cachedBundle.size === stats.size) {
9529
10212
  return cachedBundle.value;
9530
10213
  }
9531
- const buffer = readFileSync4(bundlePath);
10214
+ const buffer = readFileSync5(bundlePath);
9532
10215
  const skills = parseBuiltInStudioSkills(buffer);
9533
10216
  if (skills.length === 0) {
9534
10217
  throw new Error(`No built-in skill documents found in ${bundlePath}`);
9535
10218
  }
9536
- const studioVersion = bundlePath.split(path5.sep).find((segment) => /^version-[a-z0-9]+$/i.test(segment));
10219
+ const studioVersion = bundlePath.split(path7.sep).find((segment) => /^version-[a-z0-9]+$/i.test(segment));
9537
10220
  const value = {
9538
10221
  bundlePath,
9539
10222
  bundleModifiedAt: stats.mtime.toISOString(),
@@ -9549,6 +10232,314 @@ function findBuiltInStudioSkill(bundle, requestedName) {
9549
10232
  return bundle.skills.find((skill) => skill.name.toLowerCase() === wanted || skill.sourceName.toLowerCase() === wanted);
9550
10233
  }
9551
10234
 
10235
+ // ../core/dist/tools/monetization.js
10236
+ var MAX_PRICE_ROBUX = 1e9;
10237
+ var MAX_NAME_SCAN_PAGES = 200;
10238
+ var MAX_ICON_BYTES = 10 * 1024 * 1024;
10239
+ var PNG_SIGNATURE2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
10240
+ var SCOPE_PREFIX = {
10241
+ developer_product: "developer-product",
10242
+ game_pass: "game-pass"
10243
+ };
10244
+ var ACTION_FIELDS = {
10245
+ list: ["page_token"],
10246
+ get: ["id"],
10247
+ create: ["name", "description", "price", "for_sale", "managed_pricing", "image_path"],
10248
+ update: ["id", "name", "description", "price", "for_sale", "managed_pricing", "image_path"]
10249
+ };
10250
+ var ACTION_SCOPED_FIELDS = [
10251
+ "id",
10252
+ "page_token",
10253
+ "name",
10254
+ "description",
10255
+ "price",
10256
+ "for_sale",
10257
+ "managed_pricing",
10258
+ "image_path"
10259
+ ];
10260
+ var MonetizationError = class extends Error {
10261
+ code;
10262
+ details;
10263
+ constructor(code, message, details = {}) {
10264
+ super(message);
10265
+ this.code = code;
10266
+ this.details = details;
10267
+ this.name = "MonetizationError";
10268
+ }
10269
+ };
10270
+ function invalidArgument(message) {
10271
+ return new MonetizationError("invalid_argument", message);
10272
+ }
10273
+ function isAction(value) {
10274
+ return value === "list" || value === "get" || value === "create" || value === "update";
10275
+ }
10276
+ function isKind(value) {
10277
+ return value === "developer_product" || value === "game_pass";
10278
+ }
10279
+ function isPresent(value) {
10280
+ return value !== void 0 && value !== null;
10281
+ }
10282
+ function optionalId(value, name) {
10283
+ if (!isPresent(value))
10284
+ return void 0;
10285
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
10286
+ throw invalidArgument(`${name} must be a positive whole number.`);
10287
+ }
10288
+ return value;
10289
+ }
10290
+ function optionalPrice(value) {
10291
+ if (!isPresent(value))
10292
+ return void 0;
10293
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > MAX_PRICE_ROBUX) {
10294
+ throw invalidArgument(`price must be a whole number of Robux from 1 to ${MAX_PRICE_ROBUX}.`);
10295
+ }
10296
+ return value;
10297
+ }
10298
+ function optionalString(value, name) {
10299
+ if (!isPresent(value))
10300
+ return void 0;
10301
+ if (typeof value !== "string")
10302
+ throw invalidArgument(`${name} must be a string.`);
10303
+ return value;
10304
+ }
10305
+ function optionalNonBlankString(value, name) {
10306
+ const text = optionalString(value, name);
10307
+ if (text !== void 0 && text.trim() === "")
10308
+ throw invalidArgument(`${name} must not be empty.`);
10309
+ return text;
10310
+ }
10311
+ function optionalBoolean(value, name) {
10312
+ if (!isPresent(value))
10313
+ return void 0;
10314
+ if (typeof value !== "boolean")
10315
+ throw invalidArgument(`${name} must be true or false.`);
10316
+ return value;
10317
+ }
10318
+ function parseRequest(request) {
10319
+ const { action, kind } = request;
10320
+ if (!isAction(action))
10321
+ throw invalidArgument("action must be list, get, create, or update.");
10322
+ if (!isKind(kind))
10323
+ throw invalidArgument("kind must be developer_product or game_pass.");
10324
+ for (const field of ACTION_SCOPED_FIELDS) {
10325
+ if (isPresent(request[field]) && !ACTION_FIELDS[action].includes(field)) {
10326
+ throw invalidArgument(`${field} is not used by action=${action}.`);
10327
+ }
10328
+ }
10329
+ const parsed = {
10330
+ action,
10331
+ kind,
10332
+ universeId: optionalId(request.universe_id, "universe_id"),
10333
+ id: optionalId(request.id, "id"),
10334
+ pageToken: optionalNonBlankString(request.page_token, "page_token"),
10335
+ name: optionalNonBlankString(request.name, "name")?.trim(),
10336
+ description: optionalString(request.description, "description"),
10337
+ price: optionalPrice(request.price),
10338
+ forSale: optionalBoolean(request.for_sale, "for_sale"),
10339
+ managedPricing: optionalBoolean(request.managed_pricing, "managed_pricing"),
10340
+ imagePath: optionalNonBlankString(request.image_path, "image_path")
10341
+ };
10342
+ if (action === "get" || action === "update")
10343
+ requiredId(parsed);
10344
+ if (action === "create") {
10345
+ requiredName(parsed);
10346
+ if (parsed.forSale === true && parsed.price === void 0) {
10347
+ throw invalidArgument("price is required to create an item with for_sale=true.");
10348
+ }
10349
+ }
10350
+ if (action === "update" && Object.keys(itemChanges(parsed)).length === 0 && parsed.imagePath === void 0) {
10351
+ throw invalidArgument("update needs at least one of name, description, price, for_sale, managed_pricing, or image_path.");
10352
+ }
10353
+ return parsed;
10354
+ }
10355
+ function requiredId(request) {
10356
+ if (request.id === void 0)
10357
+ throw invalidArgument(`id is required for action=${request.action}.`);
10358
+ return request.id;
10359
+ }
10360
+ function requiredName(request) {
10361
+ if (request.name === void 0)
10362
+ throw invalidArgument(`name is required for action=${request.action}.`);
10363
+ return request.name;
10364
+ }
10365
+ function itemChanges(request) {
10366
+ return {
10367
+ ...request.name !== void 0 ? { name: request.name } : {},
10368
+ ...request.description !== void 0 ? { description: request.description } : {},
10369
+ ...request.price !== void 0 ? { price: request.price } : {},
10370
+ ...request.forSale !== void 0 ? { forSale: request.forSale } : {},
10371
+ ...request.managedPricing !== void 0 ? { managedPricing: request.managedPricing } : {}
10372
+ };
10373
+ }
10374
+ function itemLabel(kind) {
10375
+ return kind === "developer_product" ? "developer product" : "game pass";
10376
+ }
10377
+ function comparableName(name) {
10378
+ return name.trim().toLowerCase();
10379
+ }
10380
+ async function findItemNamed(client, kind, universeId, name) {
10381
+ const wanted = comparableName(name);
10382
+ let pageToken;
10383
+ for (let page = 0; page < MAX_NAME_SCAN_PAGES; page++) {
10384
+ const result = await client.listMonetizationItems(kind, universeId, pageToken);
10385
+ const match = result.items.find((candidate) => comparableName(candidate.name) === wanted);
10386
+ if (match)
10387
+ return match;
10388
+ if (!result.nextPageToken)
10389
+ return void 0;
10390
+ pageToken = result.nextPageToken;
10391
+ }
10392
+ 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.`);
10393
+ }
10394
+ function requiredScopes(request) {
10395
+ const prefix = SCOPE_PREFIX[request.kind];
10396
+ return request.action === "list" || request.action === "get" ? [`${prefix}:read`] : [`${prefix}:read`, `${prefix}:write`];
10397
+ }
10398
+ function publicItem(item) {
10399
+ return {
10400
+ kind: item.kind,
10401
+ id: item.id,
10402
+ universe_id: item.universeId,
10403
+ name: item.name,
10404
+ description: item.description,
10405
+ for_sale: item.forSale,
10406
+ price: item.price,
10407
+ managed_pricing: item.managedPricing,
10408
+ icon_asset_id: item.iconAssetId,
10409
+ ...item.createdAt !== void 0 ? { created_at: item.createdAt } : {},
10410
+ ...item.updatedAt !== void 0 ? { updated_at: item.updatedAt } : {},
10411
+ ...item.immutable !== void 0 ? { immutable: item.immutable } : {}
10412
+ };
10413
+ }
10414
+ async function resolveUniverse(request, context) {
10415
+ if (request.universeId !== void 0)
10416
+ return request.universeId;
10417
+ const universeId = await context.connectedUniverseId();
10418
+ if (!Number.isSafeInteger(universeId) || universeId <= 0) {
10419
+ throw new MonetizationError("unpublished_place", "The connected Studio place is not published, so it has no universe. Publish it or pass universe_id.");
10420
+ }
10421
+ return universeId;
10422
+ }
10423
+ function iconType(data) {
10424
+ if (data.length >= PNG_SIGNATURE2.length && data.subarray(0, PNG_SIGNATURE2.length).equals(PNG_SIGNATURE2)) {
10425
+ return { mimeType: "image/png", extension: "png" };
10426
+ }
10427
+ if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) {
10428
+ return { mimeType: "image/jpeg", extension: "jpg" };
10429
+ }
10430
+ if (data.length >= 2 && data[0] === 66 && data[1] === 77) {
10431
+ return { mimeType: "image/bmp", extension: "bmp" };
10432
+ }
10433
+ return void 0;
10434
+ }
10435
+ function readIcon(imagePath) {
10436
+ let data;
10437
+ try {
10438
+ data = readRegularFileWithinLimit(imagePath, "image_path", (byteLength) => {
10439
+ if (byteLength > MAX_ICON_BYTES) {
10440
+ throw new Error(`image_path is ${byteLength} bytes; icons are limited to ${MAX_ICON_BYTES} bytes.`);
10441
+ }
10442
+ });
10443
+ } catch (error) {
10444
+ throw invalidArgument(error instanceof Error ? error.message : String(error));
10445
+ }
10446
+ const type = iconType(data);
10447
+ if (!type)
10448
+ throw invalidArgument("image_path must be a PNG, JPEG, or BMP file.");
10449
+ return { data, fileName: `icon.${type.extension}`, mimeType: type.mimeType };
10450
+ }
10451
+ async function runAction(request, universeId, client, icon) {
10452
+ const { kind } = request;
10453
+ const changes = { ...itemChanges(request), ...icon ? { icon } : {} };
10454
+ switch (request.action) {
10455
+ case "list": {
10456
+ const page = await client.listMonetizationItems(kind, universeId, request.pageToken);
10457
+ return {
10458
+ kind,
10459
+ universe_id: universeId,
10460
+ items: page.items.map(publicItem),
10461
+ ...page.nextPageToken ? { next_page_token: page.nextPageToken } : {}
10462
+ };
10463
+ }
10464
+ case "get":
10465
+ return { item: publicItem(await client.getMonetizationItem(kind, universeId, requiredId(request))) };
10466
+ case "create": {
10467
+ const name = requiredName(request);
10468
+ const existing = await findItemNamed(client, kind, universeId, name);
10469
+ if (existing) {
10470
+ 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) });
10471
+ }
10472
+ const created = await client.createMonetizationItem(kind, universeId, {
10473
+ ...changes,
10474
+ name,
10475
+ forSale: request.forSale ?? false
10476
+ });
10477
+ return { created: true, item: publicItem(created) };
10478
+ }
10479
+ case "update": {
10480
+ const id = requiredId(request);
10481
+ await client.updateMonetizationItem(kind, universeId, id, changes);
10482
+ try {
10483
+ return { updated: true, item: publicItem(await client.getMonetizationItem(kind, universeId, id)) };
10484
+ } catch (error) {
10485
+ const reason = error instanceof Error ? error.message : String(error);
10486
+ return {
10487
+ updated: true,
10488
+ item: null,
10489
+ warnings: [`The update succeeded, but reading the item back failed: ${reason}`]
10490
+ };
10491
+ }
10492
+ }
10493
+ }
10494
+ }
10495
+ function openCloudFailure(error, request) {
10496
+ const { message, status } = error;
10497
+ switch (status) {
10498
+ case 401:
10499
+ return { error: "unauthorized", message, hint: "Check ROBLOX_OPEN_CLOUD_API_KEY." };
10500
+ case 403:
10501
+ return {
10502
+ error: "forbidden",
10503
+ message,
10504
+ required_scopes: requiredScopes(request),
10505
+ hint: "Grant these scopes to the API key and add this experience to it."
10506
+ };
10507
+ case 404:
10508
+ return { error: "not_found", message, hint: "Check id, kind, and universe_id." };
10509
+ case 409:
10510
+ return { error: "conflict", message };
10511
+ case 429:
10512
+ return { error: "rate_limited", message, hint: "Wait before retrying." };
10513
+ default: {
10514
+ if (status >= 400 && status < 500) {
10515
+ const { field, hint } = error.details;
10516
+ return { error: "invalid_request", message, ...field ? { field } : {}, ...hint ? { hint } : {} };
10517
+ }
10518
+ return { error: "open_cloud_error", message, status };
10519
+ }
10520
+ }
10521
+ }
10522
+ async function runManageMonetization(request, context) {
10523
+ let parsed;
10524
+ try {
10525
+ parsed = parseRequest(request);
10526
+ if (!context.client.hasApiKey()) {
10527
+ const scopes = requiredScopes(parsed);
10528
+ 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 });
10529
+ }
10530
+ const icon = parsed.imagePath === void 0 ? void 0 : readIcon(parsed.imagePath);
10531
+ const universeId = await resolveUniverse(parsed, context);
10532
+ return await runAction(parsed, universeId, context.client, icon);
10533
+ } catch (error) {
10534
+ if (error instanceof MonetizationError) {
10535
+ return { error: error.code, message: error.message, ...error.details };
10536
+ }
10537
+ if (error instanceof OpenCloudRequestError && parsed)
10538
+ return openCloudFailure(error, parsed);
10539
+ throw error;
10540
+ }
10541
+ }
10542
+
9552
10543
  // ../core/dist/jpeg-encoder.js
9553
10544
  var ZIGZAG = [
9554
10545
  0,
@@ -10502,7 +11493,7 @@ function rgbaToJpeg(rgba, width, height, quality = 80) {
10502
11493
 
10503
11494
  // ../core/dist/png-encoder.js
10504
11495
  import { deflateSync } from "zlib";
10505
- var PNG_SIGNATURE2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
11496
+ var PNG_SIGNATURE3 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
10506
11497
  var CRC_TABLE = new Uint32Array(256);
10507
11498
  for (let n = 0; n < 256; n++) {
10508
11499
  let c = n;
@@ -10546,7 +11537,7 @@ function rgbaToPng(rgba, width, height) {
10546
11537
  ihdr[11] = 0;
10547
11538
  ihdr[12] = 0;
10548
11539
  return Buffer.concat([
10549
- PNG_SIGNATURE2,
11540
+ PNG_SIGNATURE3,
10550
11541
  writeChunk("IHDR", ihdr),
10551
11542
  writeChunk("IDAT", deflateSync(filtered)),
10552
11543
  writeChunk("IEND", Buffer.alloc(0))
@@ -10554,16 +11545,16 @@ function rgbaToPng(rgba, width, height) {
10554
11545
  }
10555
11546
 
10556
11547
  // ../core/dist/host-capture.js
10557
- import { spawn as spawn3 } from "child_process";
11548
+ import { spawn as spawn4 } from "child_process";
10558
11549
  import * as fs4 from "fs";
10559
11550
  import * as os4 from "os";
10560
- import * as path7 from "path";
11551
+ import * as path9 from "path";
10561
11552
 
10562
11553
  // ../core/dist/host-capture-macos.js
10563
- import { spawn as spawn2 } from "child_process";
11554
+ import { spawn as spawn3 } from "child_process";
10564
11555
  import * as fs3 from "fs";
10565
11556
  import * as os3 from "os";
10566
- import * as path6 from "path";
11557
+ import * as path8 from "path";
10567
11558
  var MAX_IMAGE_BYTES = 128 * 1024 * 1024;
10568
11559
  var MAX_OUTPUT_BYTES = 64 * 1024;
10569
11560
  var STUDIO_BUNDLE_ID = "com.Roblox.RobloxStudio";
@@ -10672,7 +11663,7 @@ func capture() async throws {
10672
11663
  function runBoundedCaptureProcess(executable, args, timeoutMs, env = process.env) {
10673
11664
  return new Promise((resolve6, reject) => {
10674
11665
  const processGroup = process.platform !== "win32";
10675
- const child = spawn2(executable, args, { env, stdio: ["ignore", "pipe", "pipe"], detached: processGroup });
11666
+ const child = spawn3(executable, args, { env, stdio: ["ignore", "pipe", "pipe"], detached: processGroup });
10676
11667
  let stdout = "";
10677
11668
  let stderr = "";
10678
11669
  let outputBytes = 0;
@@ -10729,10 +11720,10 @@ async function assertSwiftToolchain(env = process.env, selectedDeveloperDir = ()
10729
11720
  }
10730
11721
  }
10731
11722
  const compilers = [
10732
- path6.join(developerDir, "usr", "bin", "swiftc"),
10733
- path6.join(developerDir, "Toolchains", "XcodeDefault.xctoolchain", "usr", "bin", "swiftc")
11723
+ path8.join(developerDir, "usr", "bin", "swiftc"),
11724
+ path8.join(developerDir, "Toolchains", "XcodeDefault.xctoolchain", "usr", "bin", "swiftc")
10734
11725
  ];
10735
- if (!path6.isAbsolute(developerDir) || !compilers.some((compiler) => exists(compiler))) {
11726
+ if (!path8.isAbsolute(developerDir) || !compilers.some((compiler) => exists(compiler))) {
10736
11727
  throw new Error(MISSING_SWIFT_TOOLCHAIN);
10737
11728
  }
10738
11729
  }
@@ -10741,11 +11732,11 @@ async function getHelper() {
10741
11732
  if (!compiledHelper) {
10742
11733
  compiledHelper = (async () => {
10743
11734
  await assertSwiftToolchain();
10744
- const directory = fs3.mkdtempSync(path6.join(os3.tmpdir(), "robloxstudio-mcp-macos-helper-"));
11735
+ const directory = fs3.mkdtempSync(path8.join(os3.tmpdir(), "robloxstudio-mcp-macos-helper-"));
10745
11736
  const cleanup = () => fs3.rmSync(directory, { recursive: true, force: true });
10746
11737
  try {
10747
- const source = path6.join(directory, "capture.swift");
10748
- const executable = path6.join(directory, "capture");
11738
+ const source = path8.join(directory, "capture.swift");
11739
+ const executable = path8.join(directory, "capture");
10749
11740
  fs3.writeFileSync(source, MACOS_CAPTURE_SOURCE, { mode: 384 });
10750
11741
  await runBoundedCaptureProcess("/usr/bin/xcrun", [
10751
11742
  "swiftc",
@@ -10754,7 +11745,7 @@ async function getHelper() {
10754
11745
  "-o",
10755
11746
  executable,
10756
11747
  "-module-cache-path",
10757
- path6.join(directory, "modules")
11748
+ path8.join(directory, "modules")
10758
11749
  ], 6e4);
10759
11750
  process.once("exit", cleanup);
10760
11751
  return executable;
@@ -10805,9 +11796,9 @@ async function runMacCapture(output, titleHint, expectedIdentity) {
10805
11796
  });
10806
11797
  }
10807
11798
  async function captureMacStudioWindow(titleHint, expectedIdentity, runCapture = runMacCapture) {
10808
- const directory = fs3.mkdtempSync(path6.join(os3.tmpdir(), "robloxstudio-mcp-macos-capture-"));
11799
+ const directory = fs3.mkdtempSync(path8.join(os3.tmpdir(), "robloxstudio-mcp-macos-capture-"));
10809
11800
  try {
10810
- const output = path6.join(directory, "capture.rgba");
11801
+ const output = path8.join(directory, "capture.rgba");
10811
11802
  const stdout = await runCapture(output, titleHint, expectedIdentity);
10812
11803
  const report = parseMacCaptureReport(stdout, expectedIdentity);
10813
11804
  if (!report.ok)
@@ -11073,7 +12064,7 @@ function parseWindowsCaptureReport(value, expectedIdentity) {
11073
12064
  function powershellPath() {
11074
12065
  const systemRoot = process.env.SystemRoot ?? process.env.windir;
11075
12066
  if (systemRoot) {
11076
- const candidate = path7.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
12067
+ const candidate = path9.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
11077
12068
  if (fs4.existsSync(candidate))
11078
12069
  return candidate;
11079
12070
  }
@@ -11082,7 +12073,7 @@ function powershellPath() {
11082
12073
  function runWindowsCapture(titleHint, outFile, expectedIdentity) {
11083
12074
  const { promise, resolve: resolve6, reject } = Promise.withResolvers();
11084
12075
  const encoded = Buffer.from(WINDOWS_CAPTURE_SCRIPT, "utf16le").toString("base64");
11085
- const child = spawn3(powershellPath(), ["-NoProfile", "-NonInteractive", "-NoLogo", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded], {
12076
+ const child = spawn4(powershellPath(), ["-NoProfile", "-NonInteractive", "-NoLogo", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded], {
11086
12077
  env: {
11087
12078
  ...process.env,
11088
12079
  MCP_CAPTURE_TITLE_HINT: titleHint ?? "",
@@ -11145,7 +12136,7 @@ async function captureStudioWindow(titleHint, expectedIdentity) {
11145
12136
  }
11146
12137
  if (process.platform === "darwin")
11147
12138
  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`);
12139
+ const outFile = path9.join(os4.tmpdir(), `robloxstudio-mcp-capture-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.bgra`);
11149
12140
  try {
11150
12141
  const report = parseWindowsCaptureReport(await runWindowsCapture(titleHint, outFile, expectedIdentity), expectedIdentity);
11151
12142
  if (!report.ok)
@@ -11177,7 +12168,7 @@ async function captureStudioWindow(titleHint, expectedIdentity) {
11177
12168
 
11178
12169
  // ../core/dist/tools/index.js
11179
12170
  import * as fs5 from "fs";
11180
- import * as path8 from "path";
12171
+ import * as path10 from "path";
11181
12172
  function sameHostWindowIdentity(actual, expected) {
11182
12173
  return actual?.windowId === expected.windowId && actual.processId === expected.processId && actual.bundleIdentifier === expected.bundleIdentifier;
11183
12174
  }
@@ -11360,9 +12351,9 @@ function compactSoundReference(sound) {
11360
12351
  name: stringField(sound, "name"),
11361
12352
  className: stringField(sound, "className")
11362
12353
  };
11363
- const path9 = stringField(sound, "path");
11364
- if (path9)
11365
- compact.path = path9;
12354
+ const path11 = stringField(sound, "path");
12355
+ if (path11)
12356
+ compact.path = path11;
11366
12357
  const assetId = robloxAssetIdFromContentId(sound.assetId ?? sound.soundId ?? sound.asset);
11367
12358
  if (assetId !== void 0)
11368
12359
  compact.assetId = assetId;
@@ -11425,7 +12416,7 @@ function loadMicroProfilerBaseline(source, sourcePath) {
11425
12416
  if (typeof sourcePath !== "string" || sourcePath === "") {
11426
12417
  throw new Error("baseline_path must be a non-empty string when provided");
11427
12418
  }
11428
- const resolved = path8.resolve(sourcePath);
12419
+ const resolved = path10.resolve(sourcePath);
11429
12420
  const parsed = JSON.parse(fs5.readFileSync(resolved, "utf8"));
11430
12421
  const record = asRecord(parsed);
11431
12422
  if (!record)
@@ -11921,6 +12912,10 @@ end
11921
12912
  error("Unsupported device simulator operation: " .. tostring(opts.operation), 0)
11922
12913
  `.trim();
11923
12914
  }
12915
+ var isClientRole = (role) => /^client-\d+$/.test(role);
12916
+ var isRuntimeRole2 = (role) => role === "server" || isClientRole(role);
12917
+ var anyRole = () => true;
12918
+ var SCOPE_WIDE_TARGETS = /* @__PURE__ */ new Set(["all", "all-clients", "edit-and-clients"]);
11924
12919
  var RobloxStudioTools = class {
11925
12920
  client;
11926
12921
  bridge;
@@ -12064,7 +13059,8 @@ var RobloxStudioTools = class {
12064
13059
  const refresh = this.bridge.refreshTopologyForRouting(signal);
12065
13060
  if (refresh)
12066
13061
  await refresh;
12067
- const resolved = this.bridge.resolveTarget({ instance_id, target });
13062
+ const role = typeof target === "object" ? this._targetForInstanceId(instance_id, target) : target;
13063
+ const resolved = this.bridge.resolveTarget({ instance_id, target: role });
12068
13064
  if (!resolved.ok)
12069
13065
  throw new RoutingFailure(resolved.error);
12070
13066
  if (resolved.mode !== "single") {
@@ -12076,26 +13072,66 @@ var RobloxStudioTools = class {
12076
13072
  }
12077
13073
  return this._requestPeer(endpoint, data, resolved.targetPeerId, timeoutMs, signal, operationId);
12078
13074
  }
13075
+ // A grouped runtime process ID, or its role-suffixed Instance ID, names exactly
13076
+ // one Peer. Solo playtest Peers share one Instance ID, so it names none of them.
13077
+ _peerSelectedByInstanceId(instance_id) {
13078
+ if (instance_id === void 0)
13079
+ return void 0;
13080
+ const selected = this.bridge.getPeersInScope(instance_id).filter((peer) => isRuntimeRole2(peer.role) && (instance_id === peer.instanceId || instance_id === `${peer.instanceId}-${peer.role}`));
13081
+ return selected.length === 1 ? selected[0] : void 0;
13082
+ }
13083
+ // Apply a Peer-naming instance_id to a tool's target after topology refresh: it
13084
+ // replaces the tool default, and a different single-Peer target is rejected
13085
+ // instead of silently acting on another Peer.
13086
+ _targetForInstanceId(instance_id, choice) {
13087
+ const requested = choice.requested || void 0;
13088
+ const selected = this._peerSelectedByInstanceId(instance_id);
13089
+ if (!selected || !choice.accepts(selected.role))
13090
+ return requested ?? choice.fallback;
13091
+ if (requested === void 0)
13092
+ return selected.role;
13093
+ if (requested !== selected.role && !SCOPE_WIDE_TARGETS.has(requested)) {
13094
+ throw new RoutingFailure({
13095
+ code: "ambiguous_target",
13096
+ message: `instance_id "${instance_id}" selects ${selected.role}, but target=${requested}. Omit target, pass target=${selected.role}, or pass the controller Instance ID with target=${requested}.`,
13097
+ data: this._routingErrorData()
13098
+ });
13099
+ }
13100
+ return requested;
13101
+ }
12079
13102
  // Honor an explicitly selected client for viewport/input operations; otherwise
12080
13103
  // prefer the first client in the scope, then retain the default Peer's Instance.
12081
13104
  _resolveRuntime(instance_id) {
12082
13105
  const resolved = this.bridge.resolveTarget({ instance_id, target: void 0 });
12083
- if (!resolved.ok)
13106
+ let scopePeers;
13107
+ let defaultInstanceId;
13108
+ if (resolved.ok) {
13109
+ if (resolved.mode !== "single") {
13110
+ throw new RoutingFailure({
13111
+ code: "target_role_not_present_on_instance",
13112
+ message: "A single runtime target is required.",
13113
+ data: this._routingErrorData()
13114
+ });
13115
+ }
13116
+ scopePeers = this.bridge.getPeersInScope(resolved.targetInstanceId);
13117
+ defaultInstanceId = resolved.targetInstanceId;
13118
+ } else if (resolved.error.code === "target_role_required") {
13119
+ scopePeers = instance_id !== void 0 ? this.bridge.getPeersInScope(instance_id) : this.bridge.getPeers();
13120
+ } else {
12084
13121
  throw new RoutingFailure(resolved.error);
12085
- if (resolved.mode !== "single") {
13122
+ }
13123
+ const named = this._peerSelectedByInstanceId(instance_id);
13124
+ const client = (named && isClientRole(named.role) ? named : void 0) ?? scopePeers.filter((peer) => isClientRole(peer.role)).sort((a, b) => a.role.localeCompare(b.role) || a.peerId.localeCompare(b.peerId))[0];
13125
+ if (client)
13126
+ return { instanceId: client.instanceId, clientRole: client.role };
13127
+ if (defaultInstanceId === void 0) {
12086
13128
  throw new RoutingFailure({
12087
13129
  code: "target_role_not_present_on_instance",
12088
- message: "A single runtime target is required.",
13130
+ message: `No edit or client viewport is connected in this scope. Connected roles: ${scopePeers.map((peer) => peer.role).join(", ")}.`,
12089
13131
  data: this._routingErrorData()
12090
13132
  });
12091
13133
  }
12092
- const clients = this.bridge.getPeersInScope(resolved.targetInstanceId).filter((peer) => /^client-\d+$/.test(peer.role));
12093
- const selectedClients = clients.filter((peer) => instance_id === peer.instanceId || instance_id === `${peer.instanceId}-${peer.role}`);
12094
- const client = selectedClients.length === 1 ? selectedClients[0] : clients.sort((a, b) => a.role.localeCompare(b.role) || a.peerId.localeCompare(b.peerId))[0];
12095
- return {
12096
- instanceId: client?.instanceId ?? resolved.targetInstanceId,
12097
- clientRole: client?.role
12098
- };
13134
+ return { instanceId: defaultInstanceId };
12099
13135
  }
12100
13136
  // Resolve synchronously after the caller has refreshed and captured its topology.
12101
13137
  _resolveInstanceIdOnly(instance_id) {
@@ -12163,7 +13199,7 @@ var RobloxStudioTools = class {
12163
13199
  return compact;
12164
13200
  }
12165
13201
  _resolveDeviceSimulatorSingleTarget(target, instance_id, toolName) {
12166
- const selectedTarget = target ?? "edit";
13202
+ const selectedTarget = this._targetForInstanceId(instance_id, { requested: target, fallback: "edit", accepts: isClientRole });
12167
13203
  if (selectedTarget === "server" || selectedTarget === "all" || selectedTarget === "all-clients") {
12168
13204
  throw new Error(`${toolName} target must be "edit" or "client-N" (got: ${selectedTarget})`);
12169
13205
  }
@@ -12174,7 +13210,7 @@ var RobloxStudioTools = class {
12174
13210
  return { ...resolved, selectedTarget };
12175
13211
  }
12176
13212
  _resolveDeviceSimulatorSetTargets(target, instance_id) {
12177
- const selectedTarget = target ?? "edit";
13213
+ const selectedTarget = this._targetForInstanceId(instance_id, { requested: target, fallback: "edit", accepts: isClientRole });
12178
13214
  if (selectedTarget === "all-clients") {
12179
13215
  const instanceId = this._resolveInstanceIdOnly(instance_id);
12180
13216
  const roles = this._clientRolesForScope(instanceId);
@@ -12198,7 +13234,7 @@ var RobloxStudioTools = class {
12198
13234
  return selectedInclude;
12199
13235
  }
12200
13236
  _resolveSimulationTargets(target, instance_id, toolName) {
12201
- const selectedTarget = target ?? "edit-and-clients";
13237
+ const selectedTarget = this._targetForInstanceId(instance_id, { requested: target, fallback: "edit-and-clients", accepts: isClientRole });
12202
13238
  if (selectedTarget === "server" || selectedTarget === "all") {
12203
13239
  throw new Error(`${toolName} target must be "edit", "client-N", "all-clients", or "edit-and-clients" (got: ${selectedTarget})`);
12204
13240
  }
@@ -12377,8 +13413,8 @@ var RobloxStudioTools = class {
12377
13413
  timedOut: true
12378
13414
  };
12379
13415
  }
12380
- async getFileTree(path9 = "", instance_id) {
12381
- const response = await this._callSingle("/api/file-tree", { path: path9 }, void 0, instance_id);
13416
+ async getFileTree(path11 = "", instance_id) {
13417
+ const response = await this._callSingle("/api/file-tree", { path: path11 }, void 0, instance_id);
12382
13418
  return {
12383
13419
  content: [
12384
13420
  {
@@ -12410,6 +13446,28 @@ var RobloxStudioTools = class {
12410
13446
  ]
12411
13447
  };
12412
13448
  }
13449
+ async manageMonetization(request, instance_id) {
13450
+ return this._textResult(await runManageMonetization(request, {
13451
+ client: this.openCloudClient,
13452
+ connectedUniverseId: () => this._connectedUniverseId(instance_id)
13453
+ }));
13454
+ }
13455
+ // Monetization APIs address a universe, which Studio reports as game.GameId.
13456
+ async _connectedUniverseId(instance_id) {
13457
+ let info;
13458
+ try {
13459
+ info = await this._callSingle("/api/place-info", {}, void 0, instance_id);
13460
+ } catch (error) {
13461
+ if (error instanceof RoutingFailure) {
13462
+ throw new RoutingFailure({
13463
+ ...error.routingError,
13464
+ message: `${error.routingError.message} Pass universe_id to skip Studio.`
13465
+ });
13466
+ }
13467
+ throw error;
13468
+ }
13469
+ return typeof info.gameId === "number" ? info.gameId : 0;
13470
+ }
12413
13471
  async searchObjects(query, searchType = "name", propertyName, instance_id) {
12414
13472
  const response = await this._callSingle("/api/search-objects", {
12415
13473
  query,
@@ -12470,9 +13528,9 @@ var RobloxStudioTools = class {
12470
13528
  ]
12471
13529
  };
12472
13530
  }
12473
- async getProjectStructure(path9, maxDepth, scriptsOnly, instance_id) {
13531
+ async getProjectStructure(path11, maxDepth, scriptsOnly, instance_id) {
12474
13532
  const response = await this._callSingle("/api/project-structure", {
12475
- path: path9,
13533
+ path: path11,
12476
13534
  maxDepth,
12477
13535
  scriptsOnly
12478
13536
  }, void 0, instance_id);
@@ -12633,7 +13691,7 @@ var RobloxStudioTools = class {
12633
13691
  };
12634
13692
  }
12635
13693
  async setSelection(paths, mode, instance_id) {
12636
- if (!Array.isArray(paths) || paths.some((path9) => typeof path9 !== "string" || path9.length === 0)) {
13694
+ if (!Array.isArray(paths) || paths.some((path11) => typeof path11 !== "string" || path11.length === 0)) {
12637
13695
  throw new Error("selection paths must contain only non-empty instance paths");
12638
13696
  }
12639
13697
  const selectionMode = mode ?? "set";
@@ -12683,7 +13741,7 @@ var RobloxStudioTools = class {
12683
13741
  if (!code) {
12684
13742
  throw new Error("Code is required for execute_luau");
12685
13743
  }
12686
- const response = await this._callSingle("/api/execute-luau", { code }, target || "edit", instance_id, void 0, void 0, operation_id);
13744
+ const response = await this._callSingle("/api/execute-luau", { code }, { requested: target, fallback: "edit", accepts: anyRole }, instance_id, void 0, void 0, operation_id);
12687
13745
  return {
12688
13746
  content: [
12689
13747
  {
@@ -12711,11 +13769,10 @@ var RobloxStudioTools = class {
12711
13769
  if (!code) {
12712
13770
  throw new Error("Code is required for eval_client_runtime");
12713
13771
  }
12714
- const clientTarget = target || "client-1";
12715
- if (!clientTarget.startsWith("client-")) {
12716
- throw new Error(`eval_client_runtime requires target=client-N (got: ${clientTarget})`);
13772
+ if (target && !target.startsWith("client-")) {
13773
+ throw new Error(`eval_client_runtime requires target=client-N (got: ${target})`);
12717
13774
  }
12718
- const response = await this._callSingle("/api/eval-runtime", { code }, clientTarget, instance_id);
13775
+ const response = await this._callSingle("/api/eval-runtime", { code }, { requested: target, fallback: "client-1", accepts: isClientRole }, instance_id);
12719
13776
  return {
12720
13777
  content: [
12721
13778
  {
@@ -12732,7 +13789,7 @@ var RobloxStudioTools = class {
12732
13789
  await refresh;
12733
13790
  const instanceId = this._resolveInstanceIdOnly(instance_id);
12734
13791
  const clientRoles = this._clientRolesForScope(instanceId);
12735
- const selectedTarget = target ?? "client-1";
13792
+ const selectedTarget = this._targetForInstanceId(instance_id, { requested: target, fallback: "client-1", accepts: isClientRole });
12736
13793
  let targetRoles;
12737
13794
  if (selectedTarget === "all-clients") {
12738
13795
  targetRoles = clientRoles;
@@ -13338,7 +14395,6 @@ var RobloxStudioTools = class {
13338
14395
  return this._textResult(result);
13339
14396
  }
13340
14397
  async captureScriptProfiler(target, request = {}, instance_id) {
13341
- const targetRole = target ?? "server";
13342
14398
  const data = { ...request };
13343
14399
  const outputPath = data.output_path;
13344
14400
  delete data.output_path;
@@ -13351,6 +14407,7 @@ var RobloxStudioTools = class {
13351
14407
  const refresh = this.bridge.refreshTopologyForRouting();
13352
14408
  if (refresh)
13353
14409
  await refresh;
14410
+ const targetRole = this._targetForInstanceId(instance_id, { requested: target, fallback: "server", accepts: isRuntimeRole2 });
13354
14411
  const resolved = this.bridge.resolveTarget({ instance_id, target: targetRole });
13355
14412
  if (!resolved.ok)
13356
14413
  throw new RoutingFailure(resolved.error);
@@ -13370,8 +14427,8 @@ var RobloxStudioTools = class {
13370
14427
  const rawJson = mutable.raw_json;
13371
14428
  if (typeof rawJson === "string") {
13372
14429
  if (typeof outputPath === "string" && outputPath !== "") {
13373
- const resolvedOutputPath = path8.resolve(outputPath);
13374
- fs5.mkdirSync(path8.dirname(resolvedOutputPath), { recursive: true });
14430
+ const resolvedOutputPath = path10.resolve(outputPath);
14431
+ fs5.mkdirSync(path10.dirname(resolvedOutputPath), { recursive: true });
13375
14432
  fs5.writeFileSync(resolvedOutputPath, rawJson, "utf8");
13376
14433
  mutable.output_path = resolvedOutputPath;
13377
14434
  }
@@ -13381,7 +14438,6 @@ var RobloxStudioTools = class {
13381
14438
  return { content: [{ type: "text", text: JSON.stringify(body) }] };
13382
14439
  }
13383
14440
  async captureMicroProfiler(target, request = {}, instance_id) {
13384
- const targetRole = target ?? "server";
13385
14441
  const data = { ...request };
13386
14442
  const outputPath = data.output_path;
13387
14443
  const summaryOutputPath = data.summary_output_path;
@@ -13414,6 +14470,7 @@ var RobloxStudioTools = class {
13414
14470
  const refresh = this.bridge.refreshTopologyForRouting();
13415
14471
  if (refresh)
13416
14472
  await refresh;
14473
+ const targetRole = this._targetForInstanceId(instance_id, { requested: target, fallback: "server", accepts: isRuntimeRole2 });
13417
14474
  const resolved = this.bridge.resolveTarget({ instance_id, target: targetRole });
13418
14475
  if (!resolved.ok)
13419
14476
  throw new RoutingFailure(resolved.error);
@@ -13433,8 +14490,8 @@ var RobloxStudioTools = class {
13433
14490
  const rawSnapshotBase64 = mutable.raw_snapshot_base64;
13434
14491
  if (typeof rawSnapshotBase64 === "string") {
13435
14492
  if (typeof outputPath === "string" && outputPath !== "") {
13436
- const resolvedOutputPath = path8.resolve(outputPath);
13437
- fs5.mkdirSync(path8.dirname(resolvedOutputPath), { recursive: true });
14493
+ const resolvedOutputPath = path10.resolve(outputPath);
14494
+ fs5.mkdirSync(path10.dirname(resolvedOutputPath), { recursive: true });
13438
14495
  fs5.writeFileSync(resolvedOutputPath, Buffer.from(rawSnapshotBase64, "base64"));
13439
14496
  mutable.output_path = resolvedOutputPath;
13440
14497
  }
@@ -13449,8 +14506,8 @@ var RobloxStudioTools = class {
13449
14506
  });
13450
14507
  }
13451
14508
  if (typeof summaryOutputPath === "string" && summaryOutputPath !== "") {
13452
- const resolvedSummaryPath = path8.resolve(summaryOutputPath);
13453
- fs5.mkdirSync(path8.dirname(resolvedSummaryPath), { recursive: true });
14509
+ const resolvedSummaryPath = path10.resolve(summaryOutputPath);
14510
+ fs5.mkdirSync(path10.dirname(resolvedSummaryPath), { recursive: true });
13454
14511
  fs5.writeFileSync(resolvedSummaryPath, JSON.stringify(mutable, null, 2), "utf8");
13455
14512
  mutable.summary_output_path = resolvedSummaryPath;
13456
14513
  }
@@ -13464,13 +14521,13 @@ var RobloxStudioTools = class {
13464
14521
  if (!action || typeof action !== "string") {
13465
14522
  throw new Error("breakpoints requires action=set|remove|clear|list");
13466
14523
  }
13467
- const targetRole = target ?? "edit";
13468
14524
  const data = { ...request, action };
13469
14525
  delete data.target;
13470
14526
  delete data.instance_id;
13471
14527
  const refresh = this.bridge.refreshTopologyForRouting();
13472
14528
  if (refresh)
13473
14529
  await refresh;
14530
+ const targetRole = this._targetForInstanceId(instance_id, { requested: target, fallback: "edit", accepts: isRuntimeRole2 });
13474
14531
  const resolved = this.bridge.resolveTarget({ instance_id, target: targetRole });
13475
14532
  if (!resolved.ok)
13476
14533
  throw new RoutingFailure(resolved.error);
@@ -13513,7 +14570,7 @@ var RobloxStudioTools = class {
13513
14570
  return record.placeId !== void 0 && instance.placeId === record.placeId;
13514
14571
  }
13515
14572
  if ((record.source === "baseplate" || record.source === "local_file") && record.localPlaceFile) {
13516
- const expectedName = path8.basename(record.localPlaceFile);
14573
+ const expectedName = path10.basename(record.localPlaceFile);
13517
14574
  return instance.placeName === expectedName || instance.dataModelName === expectedName;
13518
14575
  }
13519
14576
  return true;
@@ -14162,7 +15219,7 @@ var RobloxStudioTools = class {
14162
15219
  });
14163
15220
  }
14164
15221
  if (action === "leave_client") {
14165
- const body2 = this._parseTextResult(await this.multiplayerTestLeaveClient(target ?? "client-1", timeout, instance_id));
15222
+ const body2 = this._parseTextResult(await this.multiplayerTestLeaveClient(target, timeout, instance_id));
14166
15223
  return this._textResult(body2.success === true && body2.left === true ? {
14167
15224
  success: true,
14168
15225
  action,
@@ -14295,14 +15352,15 @@ var RobloxStudioTools = class {
14295
15352
  }]
14296
15353
  };
14297
15354
  }
14298
- async multiplayerTestLeaveClient(target = "client-1", timeout, instance_id) {
14299
- if (!/^client-\d+$/.test(target)) {
15355
+ async multiplayerTestLeaveClient(target, timeout, instance_id) {
15356
+ if (target && !/^client-\d+$/.test(target)) {
14300
15357
  throw new Error(`multiplayer_test_leave_client requires target=client-N (got: ${target})`);
14301
15358
  }
14302
15359
  const refresh = this.bridge.refreshTopologyForRouting();
14303
15360
  if (refresh)
14304
15361
  await refresh;
14305
- const clientTarget = this._resolveSingleTarget(target, instance_id);
15362
+ const role = this._targetForInstanceId(instance_id, { requested: target, fallback: "client-1", accepts: isClientRole });
15363
+ const clientTarget = this._resolveSingleTarget(role, instance_id);
14306
15364
  const group = this.bridge.getMultiplayerGroups().find((candidate) => candidate.instanceIds.includes(clientTarget.instanceId));
14307
15365
  const scopeInstanceId = group?.controllerInstanceId ?? clientTarget.instanceId;
14308
15366
  const response = await this._requestPeer("/api/multiplayer-test-leave-client", {}, clientTarget.targetPeerId);
@@ -14848,7 +15906,7 @@ var RobloxStudioTools = class {
14848
15906
  throw new Error(`File not found: ${filePath}`);
14849
15907
  }
14850
15908
  const fileContent = fs5.readFileSync(filePath);
14851
- const fileName = path8.basename(filePath);
15909
+ const fileName = path10.basename(filePath);
14852
15910
  const resolvedGroupId = groupId || process.env.ROBLOX_CREATOR_GROUP_ID;
14853
15911
  const resolvedUserId = userId || process.env.ROBLOX_CREATOR_USER_ID;
14854
15912
  if (assetType === "Decal" && this.cookieClient.hasCookie()) {
@@ -14924,12 +15982,13 @@ var RobloxStudioTools = class {
14924
15982
  if (refresh)
14925
15983
  await refresh;
14926
15984
  const { instanceId, clientRole } = this._resolveRuntime(instance_id);
15985
+ const role = this._targetForInstanceId(instance_id, { requested: target, fallback: clientRole ?? "edit", accepts: isClientRole });
14927
15986
  const response = await this._callSingle("/api/simulate-mouse-input", {
14928
15987
  action,
14929
15988
  x,
14930
15989
  y,
14931
15990
  button
14932
- }, target || clientRole || "edit", instanceId);
15991
+ }, role, instanceId);
14933
15992
  return {
14934
15993
  content: [{
14935
15994
  type: "text",
@@ -14945,12 +16004,13 @@ var RobloxStudioTools = class {
14945
16004
  if (refresh)
14946
16005
  await refresh;
14947
16006
  const { instanceId, clientRole } = this._resolveRuntime(instance_id);
16007
+ const role = this._targetForInstanceId(instance_id, { requested: target, fallback: clientRole ?? "edit", accepts: isClientRole });
14948
16008
  const response = await this._callSingle("/api/simulate-keyboard-input", {
14949
16009
  keyCode,
14950
16010
  action,
14951
16011
  duration,
14952
16012
  text
14953
- }, target || clientRole || "edit", instanceId);
16013
+ }, role, instanceId);
14954
16014
  return {
14955
16015
  content: [{
14956
16016
  type: "text",
@@ -14978,13 +16038,13 @@ var RobloxStudioTools = class {
14978
16038
  };
14979
16039
  }
14980
16040
  async getMemoryBreakdown(target, tags, instance_id) {
14981
- const tgt = target ?? "all";
14982
16041
  const data = {};
14983
16042
  if (tags !== void 0)
14984
16043
  data.tags = tags;
14985
16044
  const refresh = this.bridge.refreshTopologyForRouting();
14986
16045
  if (refresh)
14987
16046
  await refresh;
16047
+ const tgt = this._targetForInstanceId(instance_id, { requested: target, fallback: "all", accepts: anyRole });
14988
16048
  const resolved = this.bridge.resolveTarget({ instance_id, target: tgt });
14989
16049
  if (!resolved.ok)
14990
16050
  throw new RoutingFailure(resolved.error);
@@ -15010,7 +16070,6 @@ var RobloxStudioTools = class {
15010
16070
  return { content: [{ type: "text", text: JSON.stringify(body) }] };
15011
16071
  }
15012
16072
  async getSceneAnalysis(mode, target, topN, raw, instance_id) {
15013
- const tgt = target ?? "all";
15014
16073
  const data = {};
15015
16074
  if (mode !== void 0)
15016
16075
  data.mode = mode;
@@ -15021,6 +16080,7 @@ var RobloxStudioTools = class {
15021
16080
  const refresh = this.bridge.refreshTopologyForRouting();
15022
16081
  if (refresh)
15023
16082
  await refresh;
16083
+ const tgt = this._targetForInstanceId(instance_id, { requested: target, fallback: "all", accepts: anyRole });
15024
16084
  const resolved = this.bridge.resolveTarget({ instance_id, target: tgt });
15025
16085
  if (!resolved.ok)
15026
16086
  throw new RoutingFailure(resolved.error);
@@ -15052,11 +16112,10 @@ var RobloxStudioTools = class {
15052
16112
  if (!outputPath || typeof outputPath !== "string") {
15053
16113
  throw new Error("output_path is required for export_rbxm");
15054
16114
  }
15055
- const tgt = target || "edit";
15056
- if (tgt !== "edit" && tgt !== "server") {
15057
- throw new Error(`export_rbxm target must be "edit" or "server" (got: ${tgt})`);
16115
+ if (target && target !== "edit" && target !== "server") {
16116
+ throw new Error(`export_rbxm target must be "edit" or "server" (got: ${target})`);
15058
16117
  }
15059
- const response = await this._callSingle("/api/export-rbxm", { instance_paths: instancePaths }, tgt, instance_id);
16118
+ const response = await this._callSingle("/api/export-rbxm", { instance_paths: instancePaths }, { requested: target, fallback: "edit", accepts: (role) => role === "server" }, instance_id);
15060
16119
  if (response.error) {
15061
16120
  return { content: [{ type: "text", text: JSON.stringify({ error: response.error }) }] };
15062
16121
  }
@@ -15064,9 +16123,9 @@ var RobloxStudioTools = class {
15064
16123
  return { content: [{ type: "text", text: JSON.stringify({ error: "plugin returned no base64 payload" }) }] };
15065
16124
  }
15066
16125
  const bytes = Buffer.from(response.base64, "base64");
15067
- const resolved = path8.resolve(outputPath);
16126
+ const resolved = path10.resolve(outputPath);
15068
16127
  try {
15069
- fs5.mkdirSync(path8.dirname(resolved), { recursive: true });
16128
+ fs5.mkdirSync(path10.dirname(resolved), { recursive: true });
15070
16129
  fs5.writeFileSync(resolved, bytes);
15071
16130
  } catch (err2) {
15072
16131
  return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err2.message}` }) }] };
@@ -15089,9 +16148,8 @@ var RobloxStudioTools = class {
15089
16148
  if (!parentPath || typeof parentPath !== "string") {
15090
16149
  throw new Error("parent_path is required for import_rbxm");
15091
16150
  }
15092
- const tgt = target || "edit";
15093
- if (tgt !== "edit" && tgt !== "server") {
15094
- throw new Error(`import_rbxm target must be "edit" or "server" (got: ${tgt})`);
16151
+ if (target && target !== "edit" && target !== "server") {
16152
+ throw new Error(`import_rbxm target must be "edit" or "server" (got: ${target})`);
15095
16153
  }
15096
16154
  const modes = ["path", "url", "base64"].filter((k) => source[k] !== void 0);
15097
16155
  if (modes.length !== 1) {
@@ -15108,7 +16166,7 @@ var RobloxStudioTools = class {
15108
16166
  base64: bytes.toString("base64"),
15109
16167
  parent_path: parentPath,
15110
16168
  source_label: sourceLabel
15111
- }, tgt, instance_id, void 0, signal);
16169
+ }, { requested: target, fallback: "edit", accepts: (role) => role === "server" }, instance_id, void 0, signal);
15112
16170
  return { content: [{ type: "text", text: JSON.stringify(response) }] };
15113
16171
  }
15114
16172
  // Even native capture must wait while another call has fitted the simulator
@@ -15427,7 +16485,7 @@ var RobloxStudioTools = class {
15427
16485
  };
15428
16486
 
15429
16487
  // ../core/dist/proxy-bridge-service.js
15430
- import { randomUUID as randomUUID4 } from "crypto";
16488
+ import { randomUUID as randomUUID5 } from "crypto";
15431
16489
  var PROXY_RESPONSE_GRACE_MS = 5e3;
15432
16490
  function parseRequestStatus(value) {
15433
16491
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -15486,7 +16544,7 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
15486
16544
  super();
15487
16545
  this.primaryBaseUrl = primaryBaseUrl;
15488
16546
  this.authToken = authToken;
15489
- this.proxyInstanceId = randomUUID4();
16547
+ this.proxyInstanceId = randomUUID5();
15490
16548
  this.initialRefresh = this.refreshTopology();
15491
16549
  this.refreshTimer = setInterval(() => this.refreshTopology(), _ProxyBridgeService.REFRESH_INTERVAL_MS);
15492
16550
  }
@@ -15672,7 +16730,7 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
15672
16730
  return status;
15673
16731
  }
15674
16732
  async sendRequest(endpoint, data, targetPeerId, timeoutMs = this.proxyRequestTimeout, signal, operationId) {
15675
- const requestId = operationId ?? randomUUID4();
16733
+ const requestId = operationId ?? randomUUID5();
15676
16734
  const details = { requestId, targetPeerId, stage: "queued", outcome: "not_executed", executionOutcome: "not_executed" };
15677
16735
  if (typeof requestId !== "string" || requestId.trim().length === 0 || requestId.length > 128) {
15678
16736
  throw new RequestFailure("operationId must be a nonempty string of at most 128 characters", "invalid_operation_id", details);
@@ -16553,7 +17611,7 @@ var TOOL_DEFINITIONS = [
16553
17611
  },
16554
17612
  target: {
16555
17613
  type: "string",
16556
- description: "Client peer; defaults to client-1."
17614
+ description: "Client peer; defaults to the client named by instance_id, else client-1."
16557
17615
  },
16558
17616
  instance_id: {
16559
17617
  type: "string",
@@ -17570,7 +18628,7 @@ var TOOL_DEFINITIONS = [
17570
18628
  includeAudio: {
17571
18629
  type: "boolean",
17572
18630
  default: true,
17573
- description: "Return inline audio; needs asset:read and never writes files."
18631
+ description: "Return inline audio; needs legacy-asset:manage and never writes files."
17574
18632
  },
17575
18633
  maxAudioPreviews: {
17576
18634
  type: "number",
@@ -17646,6 +18704,72 @@ var TOOL_DEFINITIONS = [
17646
18704
  }
17647
18705
  }
17648
18706
  },
18707
+ // === Monetization ===
18708
+ {
18709
+ name: "manage_monetization",
18710
+ category: "write",
18711
+ description: "Use to list, inspect, create, or update developer products and game passes.",
18712
+ inputSchema: {
18713
+ type: "object",
18714
+ properties: {
18715
+ action: {
18716
+ type: "string",
18717
+ enum: ["list", "get", "create", "update"],
18718
+ description: "Operation; create and update change live Roblox items."
18719
+ },
18720
+ kind: {
18721
+ type: "string",
18722
+ enum: ["developer_product", "game_pass"],
18723
+ description: "Item type."
18724
+ },
18725
+ universe_id: {
18726
+ type: "integer",
18727
+ minimum: 1,
18728
+ description: "Universe ID; defaults to the connected Studio place."
18729
+ },
18730
+ id: {
18731
+ type: "integer",
18732
+ minimum: 1,
18733
+ description: "Product or pass ID; required for get and update."
18734
+ },
18735
+ name: {
18736
+ type: "string",
18737
+ description: "Item name; required for create."
18738
+ },
18739
+ description: {
18740
+ type: "string",
18741
+ description: "Item description shown to buyers."
18742
+ },
18743
+ price: {
18744
+ type: "integer",
18745
+ minimum: 1,
18746
+ maximum: 1e9,
18747
+ description: "Default price in Robux."
18748
+ },
18749
+ for_sale: {
18750
+ type: "boolean",
18751
+ description: "Purchasable; create defaults to false."
18752
+ },
18753
+ managed_pricing: {
18754
+ type: "boolean",
18755
+ description: "Let Roblox price optimization set the price."
18756
+ },
18757
+ image_path: {
18758
+ type: "string",
18759
+ description: "Local PNG, JPEG, or BMP icon file."
18760
+ },
18761
+ page_token: {
18762
+ type: "string",
18763
+ description: "Prior list page token."
18764
+ },
18765
+ instance_id: {
18766
+ type: "string",
18767
+ description: "Studio process ID when ambiguous."
18768
+ }
18769
+ },
18770
+ required: ["action", "kind"]
18771
+ }
18772
+ },
17649
18773
  // === Input Simulation ===
17650
18774
  {
17651
18775
  name: "simulate_mouse_input",
@@ -17944,10 +19068,10 @@ var getAllTools = () => [...TOOL_DEFINITIONS];
17944
19068
 
17945
19069
  // ../core/dist/install-plugin-helpers.js
17946
19070
  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";
19071
+ 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
19072
  import { execSync } from "child_process";
17949
- import { randomUUID as randomUUID5 } from "crypto";
17950
- import { basename as basename3, join as join8 } from "path";
19073
+ import { randomUUID as randomUUID6 } from "crypto";
19074
+ import { basename as basename3, join as join9 } from "path";
17951
19075
  import { homedir as homedir4 } from "os";
17952
19076
  import { isUtf8 } from "buffer";
17953
19077
  var DEFAULT_PLUGIN_PORT = 58741;
@@ -17999,15 +19123,15 @@ function getPluginsFolder() {
17999
19123
  if (process.env.MCP_PLUGINS_DIR)
18000
19124
  return process.env.MCP_PLUGINS_DIR;
18001
19125
  if (process.platform === "win32") {
18002
- return join8(process.env.LOCALAPPDATA || join8(homedir4(), "AppData", "Local"), "Roblox", "Plugins");
19126
+ return join9(process.env.LOCALAPPDATA || join9(homedir4(), "AppData", "Local"), "Roblox", "Plugins");
18003
19127
  }
18004
19128
  if (isWSL()) {
18005
19129
  const localAppData = getWindowsUserLocalAppData();
18006
19130
  if (localAppData)
18007
- return join8(localAppData.linuxPath, "Roblox", "Plugins");
19131
+ return join9(localAppData.linuxPath, "Roblox", "Plugins");
18008
19132
  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
19133
  }
18010
- return join8(homedir4(), "Documents", "Roblox", "Plugins");
19134
+ return join9(homedir4(), "Documents", "Roblox", "Plugins");
18011
19135
  }
18012
19136
  var STALE_TEST_PLUGINS_DIRECTORY = "RsmcpIsolatedPlugins";
18013
19137
  function pluginDirectorySettingPattern() {
@@ -18022,7 +19146,7 @@ function repairStaleStudioPluginDirectorySetting({ settingsPath, studioPluginsDi
18022
19146
  if (studioPluginsDirectory === "") {
18023
19147
  throw new Error("studioPluginsDirectory must be a non-empty path.");
18024
19148
  }
18025
- const contents = readFileSync8(settingsPath, "utf8");
19149
+ const contents = readFileSync9(settingsPath, "utf8");
18026
19150
  const matches = [...contents.matchAll(pluginDirectorySettingPattern())];
18027
19151
  if (matches.length !== 1) {
18028
19152
  throw new Error(`Expected exactly one Studio.PluginsDir QDir in ${settingsPath}; found ${matches.length}.`);
@@ -18031,15 +19155,15 @@ function repairStaleStudioPluginDirectorySetting({ settingsPath, studioPluginsDi
18031
19155
  return false;
18032
19156
  const studioPath = escapeXmlText(studioPluginsDirectory);
18033
19157
  const updated = contents.replace(pluginDirectorySettingPattern(), (_match, open4, _value, close) => `${open4}${studioPath}${close}`);
18034
- const temporaryPath = `${settingsPath}.rsmcp-install-${process.pid}-${randomUUID5()}.tmp`;
19158
+ const temporaryPath = `${settingsPath}.rsmcp-install-${process.pid}-${randomUUID6()}.tmp`;
18035
19159
  try {
18036
- writeFileSync4(temporaryPath, updated, {
19160
+ writeFileSync5(temporaryPath, updated, {
18037
19161
  encoding: "utf8",
18038
- mode: statSync3(settingsPath).mode
19162
+ mode: statSync4(settingsPath).mode
18039
19163
  });
18040
19164
  renameSync(temporaryPath, settingsPath);
18041
19165
  } finally {
18042
- rmSync4(temporaryPath, { force: true });
19166
+ rmSync5(temporaryPath, { force: true });
18043
19167
  }
18044
19168
  return true;
18045
19169
  }
@@ -18050,16 +19174,16 @@ function repairDefaultStudioPluginDirectorySetting(pluginsFolder, log) {
18050
19174
  let defaultPluginsFolder;
18051
19175
  let studioPluginsDirectory;
18052
19176
  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");
19177
+ const localAppData = process.env.LOCALAPPDATA || join9(homedir4(), "AppData", "Local");
19178
+ defaultPluginsFolder = join9(localAppData, "Roblox", "Plugins");
19179
+ settingsPath = join9(localAppData, "Roblox", "GlobalSettings_13.xml");
18056
19180
  studioPluginsDirectory = defaultPluginsFolder.replaceAll("\\", "/");
18057
19181
  } else if (isWSL()) {
18058
19182
  const localAppData = getWindowsUserLocalAppData();
18059
19183
  if (!localAppData)
18060
19184
  return;
18061
- defaultPluginsFolder = join8(localAppData.linuxPath, "Roblox", "Plugins");
18062
- settingsPath = join8(localAppData.linuxPath, "Roblox", "GlobalSettings_13.xml");
19185
+ defaultPluginsFolder = join9(localAppData.linuxPath, "Roblox", "Plugins");
19186
+ settingsPath = join9(localAppData.linuxPath, "Roblox", "GlobalSettings_13.xml");
18063
19187
  studioPluginsDirectory = [
18064
19188
  localAppData.windowsPath.replace(/[\\/]+$/u, ""),
18065
19189
  "Roblox",
@@ -18075,12 +19199,12 @@ function repairDefaultStudioPluginDirectorySetting(pluginsFolder, log) {
18075
19199
  }
18076
19200
  }
18077
19201
  function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
18078
- const otherDest = join8(pluginsFolder, otherAssetName);
19202
+ const otherDest = join9(pluginsFolder, otherAssetName);
18079
19203
  if (!existsSync7(otherDest))
18080
19204
  return;
18081
19205
  if (replace) {
18082
19206
  try {
18083
- unlinkSync2(otherDest);
19207
+ unlinkSync3(otherDest);
18084
19208
  log(`Removed conflicting ${otherAssetName}.`);
18085
19209
  } catch (err2) {
18086
19210
  warn(`[install-plugin] Could not remove ${otherDest}: ${err2}. Continuing.`);
@@ -18094,18 +19218,18 @@ Delete ${otherAssetName} manually or use the default CLI installer behavior to r
18094
19218
  `);
18095
19219
  }
18096
19220
  var PLUGIN_INSTALL_LOCK_NAME = ".robloxstudio-mcp-plugin-install.lock";
18097
- function errorCode(error) {
19221
+ function errorCode2(error) {
18098
19222
  return error !== null && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
18099
19223
  }
18100
19224
  function pluginInstallLockError(lockPath, reason) {
18101
19225
  return new Error(`${reason}: ${lockPath}. If no installer or lock recovery is running, remove that lock directory and retry.`);
18102
19226
  }
18103
19227
  function readPluginInstallLockOwner(lockPath) {
18104
- const ownerPath = join8(lockPath, "owner.json");
19228
+ const ownerPath = join9(lockPath, "owner.json");
18105
19229
  if (!lstatSync2(lockPath).isDirectory() || !lstatSync2(ownerPath).isFile()) {
18106
19230
  throw new Error("Lock and owner metadata must be a directory and regular file");
18107
19231
  }
18108
- const owner = JSON.parse(readFileSync8(ownerPath, "utf8"));
19232
+ const owner = JSON.parse(readFileSync9(ownerPath, "utf8"));
18109
19233
  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
19234
  throw new Error("Invalid lock owner metadata");
18111
19235
  }
@@ -18116,9 +19240,9 @@ function assertPluginInstallOwnerExited(lockPath, owner) {
18116
19240
  try {
18117
19241
  process.kill(owner.pid, 0);
18118
19242
  } catch (error) {
18119
- if (errorCode(error) === "ESRCH")
19243
+ if (errorCode2(error) === "ESRCH")
18120
19244
  return;
18121
- throw pluginInstallLockError(lockPath, `Cannot confirm that plugin installation owner PID ${owner.pid} has exited (${errorCode(error) ?? error})`);
19245
+ throw pluginInstallLockError(lockPath, `Cannot confirm that plugin installation owner PID ${owner.pid} has exited (${errorCode2(error) ?? error})`);
18122
19246
  }
18123
19247
  }
18124
19248
  throw pluginInstallLockError(lockPath, `Another Studio plugin installation is already in progress (owner PID ${owner.pid})`);
@@ -18131,13 +19255,13 @@ function recoverPluginInstallLock(lockPath, warn) {
18131
19255
  throw pluginInstallLockError(lockPath, `Cannot safely read plugin installation lock owner (${error})`);
18132
19256
  }
18133
19257
  assertPluginInstallOwnerExited(lockPath, owner);
18134
- const claimPath = join8(lockPath, `recovery-${owner.token}`);
19258
+ const claimPath = join9(lockPath, `recovery-${owner.token}`);
18135
19259
  try {
18136
- mkdirSync4(claimPath);
19260
+ mkdirSync5(claimPath);
18137
19261
  } 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})`);
19262
+ 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
19263
  }
18140
- const abandonedPath = `${lockPath}.${randomUUID5()}.abandoned`;
19264
+ const abandonedPath = `${lockPath}.${randomUUID6()}.abandoned`;
18141
19265
  let moved = false;
18142
19266
  try {
18143
19267
  let currentOwner;
@@ -18157,49 +19281,49 @@ function recoverPluginInstallLock(lockPath, warn) {
18157
19281
  try {
18158
19282
  rmdirSync(claimPath);
18159
19283
  } catch (error) {
18160
- if (errorCode(error) !== "ENOENT") {
19284
+ if (errorCode2(error) !== "ENOENT") {
18161
19285
  warn(`[install-plugin] Could not release lock recovery claim ${claimPath}: ${error}`);
18162
19286
  }
18163
19287
  }
18164
19288
  }
18165
19289
  }
18166
19290
  try {
18167
- rmSync4(abandonedPath, { recursive: true, force: true });
19291
+ rmSync5(abandonedPath, { recursive: true, force: true });
18168
19292
  } catch (error) {
18169
19293
  warn(`[install-plugin] Could not clean abandoned install lock ${abandonedPath}: ${error}`);
18170
19294
  }
18171
19295
  }
18172
19296
  function acquirePluginInstallLock(pluginsFolder, warn) {
18173
- const lockPath = join8(pluginsFolder, PLUGIN_INSTALL_LOCK_NAME);
18174
- const ownerPath = join8(lockPath, "owner.json");
19297
+ const lockPath = join9(pluginsFolder, PLUGIN_INSTALL_LOCK_NAME);
19298
+ const ownerPath = join9(lockPath, "owner.json");
18175
19299
  const owner = {
18176
19300
  pid: process.pid,
18177
- token: randomUUID5(),
19301
+ token: randomUUID6(),
18178
19302
  createdAt: Date.now()
18179
19303
  };
18180
19304
  try {
18181
- mkdirSync4(lockPath);
19305
+ mkdirSync5(lockPath);
18182
19306
  } catch (error) {
18183
- if (errorCode(error) !== "EEXIST")
19307
+ if (errorCode2(error) !== "EEXIST")
18184
19308
  throw error;
18185
19309
  recoverPluginInstallLock(lockPath, warn);
18186
19310
  try {
18187
- mkdirSync4(lockPath);
19311
+ mkdirSync5(lockPath);
18188
19312
  } catch (retryError) {
18189
- if (errorCode(retryError) !== "EEXIST")
19313
+ if (errorCode2(retryError) !== "EEXIST")
18190
19314
  throw retryError;
18191
19315
  throw pluginInstallLockError(lockPath, "Another Studio plugin installation is already in progress");
18192
19316
  }
18193
19317
  }
18194
19318
  try {
18195
- writeFileSync4(ownerPath, `${JSON.stringify(owner)}
19319
+ writeFileSync5(ownerPath, `${JSON.stringify(owner)}
18196
19320
  `, {
18197
19321
  encoding: "utf8",
18198
19322
  flag: "wx"
18199
19323
  });
18200
19324
  } catch (error) {
18201
19325
  try {
18202
- rmSync4(lockPath, { recursive: true, force: true });
19326
+ rmSync5(lockPath, { recursive: true, force: true });
18203
19327
  } catch (cleanupError) {
18204
19328
  warn(`[install-plugin] Could not clean failed install lock ${lockPath}: ${cleanupError}`);
18205
19329
  }
@@ -18209,10 +19333,10 @@ function acquirePluginInstallLock(pluginsFolder, warn) {
18209
19333
  try {
18210
19334
  const currentOwner = readPluginInstallLockOwner(lockPath);
18211
19335
  if (currentOwner.pid === owner.pid && currentOwner.token === owner.token) {
18212
- rmSync4(lockPath, { recursive: true, force: true });
19336
+ rmSync5(lockPath, { recursive: true, force: true });
18213
19337
  }
18214
19338
  } catch (error) {
18215
- if (errorCode(error) !== "ENOENT") {
19339
+ if (errorCode2(error) !== "ENOENT") {
18216
19340
  warn(`[install-plugin] Could not release install lock ${lockPath}: ${error}`);
18217
19341
  }
18218
19342
  }
@@ -18305,7 +19429,7 @@ function installedFileMatches(source, destination) {
18305
19429
  return false;
18306
19430
  if (!lstatSync2(destination).isFile())
18307
19431
  return false;
18308
- const installed = readFileSync8(destination);
19432
+ const installed = readFileSync9(destination);
18309
19433
  return installed.length === source.length && installed.equals(source);
18310
19434
  }
18311
19435
  function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source, expectedVersion, expectedVariant, rawPort, replaceVariant = true, log = console.log, warn = console.warn }) {
@@ -18316,22 +19440,22 @@ function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source,
18316
19440
  }
18317
19441
  const configured = configurePluginAssetForPort(source, rawPort);
18318
19442
  assertPluginAssetIdentity(configured, assetName, expectedVersion, expectedVariant);
18319
- mkdirSync4(pluginsFolder, { recursive: true });
18320
- const destination = join8(pluginsFolder, assetName);
19443
+ mkdirSync5(pluginsFolder, { recursive: true });
19444
+ const destination = join9(pluginsFolder, assetName);
18321
19445
  const releaseLock = acquirePluginInstallLock(pluginsFolder, warn);
18322
19446
  try {
18323
19447
  let installed = false;
18324
19448
  if (!installedFileMatches(configured, destination)) {
18325
- const staged = join8(pluginsFolder, `.${assetName}.${process.pid}.${randomUUID5()}.tmp`);
19449
+ const staged = join9(pluginsFolder, `.${assetName}.${process.pid}.${randomUUID6()}.tmp`);
18326
19450
  try {
18327
- writeFileSync4(staged, configured, { flag: "wx" });
19451
+ writeFileSync5(staged, configured, { flag: "wx" });
18328
19452
  renameSync(staged, destination);
18329
19453
  installed = true;
18330
19454
  } finally {
18331
19455
  try {
18332
- unlinkSync2(staged);
19456
+ unlinkSync3(staged);
18333
19457
  } catch (error) {
18334
- if (errorCode(error) !== "ENOENT") {
19458
+ if (errorCode2(error) !== "ENOENT") {
18335
19459
  warn(`[install-plugin] Could not remove staging file ${staged}: ${error}`);
18336
19460
  }
18337
19461
  }
@@ -18352,8 +19476,8 @@ function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source,
18352
19476
  }
18353
19477
 
18354
19478
  // ../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";
19479
+ import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
19480
+ import { dirname as dirname5, join as join10 } from "path";
18357
19481
  import { fileURLToPath } from "url";
18358
19482
  import { get } from "https";
18359
19483
  var REPO = "chrrxs/robloxstudio-mcp";
@@ -18417,8 +19541,8 @@ function createPluginInstaller(moduleUrl, variant) {
18417
19541
  }
18418
19542
  function bundledAssetPath() {
18419
19543
  const candidates = [
18420
- join9(currentDir, "..", "studio-plugin", ASSET_NAME),
18421
- join9(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
19544
+ join10(currentDir, "..", "studio-plugin", ASSET_NAME),
19545
+ join10(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
18422
19546
  ];
18423
19547
  return candidates.find((candidate) => existsSync8(candidate)) ?? null;
18424
19548
  }
@@ -18428,7 +19552,7 @@ function createPluginInstaller(moduleUrl, variant) {
18428
19552
  return existsSync8(sourcePath) ? sourcePath : null;
18429
19553
  }
18430
19554
  function packageVersion() {
18431
- const pkg = JSON.parse(readFileSync9(join9(currentDir, "..", "package.json"), "utf8"));
19555
+ const pkg = JSON.parse(readFileSync10(join10(currentDir, "..", "package.json"), "utf8"));
18432
19556
  if (!pkg.version) {
18433
19557
  throw new Error("Package version not found");
18434
19558
  }
@@ -18453,7 +19577,7 @@ function createPluginInstaller(moduleUrl, variant) {
18453
19577
  if (!sourcePath) {
18454
19578
  throw new Error(`Bundled ${ASSET_NAME} not found. Run npm run ${buildCommand} in this worktree first.`);
18455
19579
  }
18456
- const result = installSource(readFileSync9(sourcePath), options);
19580
+ const result = installSource(readFileSync10(sourcePath), options);
18457
19581
  if (result.installed) {
18458
19582
  log(`Installed ${ASSET_NAME} to ${result.destination}`);
18459
19583
  }
@@ -18465,7 +19589,7 @@ function createPluginInstaller(moduleUrl, variant) {
18465
19589
  throw new Error(`Plugin asset not found at explicit path ${options.sourcePath}.`);
18466
19590
  }
18467
19591
  if (bundled) {
18468
- const result2 = installSource(readFileSync9(bundled), options);
19592
+ const result2 = installSource(readFileSync10(bundled), options);
18469
19593
  if (result2.installed) {
18470
19594
  log(`Installed bundled ${ASSET_NAME} to ${result2.destination}`);
18471
19595
  } else {