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