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