@chrrxs/robloxstudio-mcp 3.1.5 → 3.1.6
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 +1039 -462
- package/package.json +1 -1
- package/studio-plugin/MCPPlugin.rbxmx +31 -11
package/dist/index.js
CHANGED
|
@@ -2710,7 +2710,7 @@ var BridgeService = class {
|
|
|
2710
2710
|
if (this.pendingRequests.size >= MAX_PENDING_REQUESTS || this.pendingRequestBytes + requestBytes > MAX_PENDING_REQUEST_BYTES) {
|
|
2711
2711
|
return Promise.reject(new RequestFailure(`Request ${requestId} rejected at admission: pending capacity exceeded (${this.pendingRequests.size}/${MAX_PENDING_REQUESTS} requests, ${this.pendingRequestBytes + requestBytes}/${MAX_PENDING_REQUEST_BYTES} bytes); queued; not_executed`, "request_capacity_exceeded", { ...details, bytes: this.pendingRequestBytes + requestBytes, limitBytes: MAX_PENDING_REQUEST_BYTES }));
|
|
2712
2712
|
}
|
|
2713
|
-
const { promise, resolve:
|
|
2713
|
+
const { promise, resolve: resolve6, reject } = Promise.withResolvers();
|
|
2714
2714
|
const abortListener = () => {
|
|
2715
2715
|
const pending = this.pendingRequests.get(requestId);
|
|
2716
2716
|
if (pending)
|
|
@@ -2728,7 +2728,7 @@ var BridgeService = class {
|
|
|
2728
2728
|
data,
|
|
2729
2729
|
targetPeerId,
|
|
2730
2730
|
timestamp: now,
|
|
2731
|
-
resolve:
|
|
2731
|
+
resolve: resolve6,
|
|
2732
2732
|
reject,
|
|
2733
2733
|
promise,
|
|
2734
2734
|
timeoutId,
|
|
@@ -3127,19 +3127,20 @@ async function fetchDocCatalog() {
|
|
|
3127
3127
|
}
|
|
3128
3128
|
const controller = new AbortController();
|
|
3129
3129
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
3130
|
-
let
|
|
3130
|
+
let html;
|
|
3131
3131
|
try {
|
|
3132
|
-
response = await fetch(DOCS_INDEX_URL, {
|
|
3132
|
+
const response = await fetch(DOCS_INDEX_URL, {
|
|
3133
3133
|
signal: controller.signal,
|
|
3134
3134
|
headers: { Accept: "text/html" }
|
|
3135
3135
|
});
|
|
3136
|
+
if (!response.ok) {
|
|
3137
|
+
await response.body?.cancel();
|
|
3138
|
+
throw new Error(`Failed to fetch Roblox docs index: HTTP ${response.status}`);
|
|
3139
|
+
}
|
|
3140
|
+
html = await response.text();
|
|
3136
3141
|
} finally {
|
|
3137
3142
|
clearTimeout(timer);
|
|
3138
3143
|
}
|
|
3139
|
-
if (!response.ok) {
|
|
3140
|
-
throw new Error(`Failed to fetch Roblox docs index: HTTP ${response.status}`);
|
|
3141
|
-
}
|
|
3142
|
-
const html = await response.text();
|
|
3143
3144
|
const pages = /* @__PURE__ */ new Map();
|
|
3144
3145
|
const routePattern = /reference\/engine\/(classes|enums|datatypes|libraries|globals)\/([A-Za-z0-9_]+)/g;
|
|
3145
3146
|
for (const match of html.matchAll(routePattern)) {
|
|
@@ -3192,27 +3193,30 @@ async function fetchRobloxDoc(category, name) {
|
|
|
3192
3193
|
}
|
|
3193
3194
|
const controller = new AbortController();
|
|
3194
3195
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
3195
|
-
let response;
|
|
3196
3196
|
try {
|
|
3197
|
-
response = await fetch(docUrl(category, name), {
|
|
3197
|
+
const response = await fetch(docUrl(category, name), {
|
|
3198
3198
|
signal: controller.signal,
|
|
3199
3199
|
headers: { Accept: "text/markdown, text/plain" }
|
|
3200
3200
|
});
|
|
3201
|
+
if (response.status === 404) {
|
|
3202
|
+
await response.body?.cancel();
|
|
3203
|
+
cacheSet(key, { fetchedAt: Date.now(), notFound: true });
|
|
3204
|
+
throw new DocNotFoundError(category, name);
|
|
3205
|
+
}
|
|
3206
|
+
if (!response.ok) {
|
|
3207
|
+
await response.body?.cancel();
|
|
3208
|
+
throw new Error(`HTTP ${response.status}`);
|
|
3209
|
+
}
|
|
3210
|
+
const content = await response.text();
|
|
3211
|
+
cacheSet(key, { fetchedAt: Date.now(), content });
|
|
3212
|
+
return content;
|
|
3201
3213
|
} catch (error) {
|
|
3214
|
+
if (error instanceof DocNotFoundError)
|
|
3215
|
+
throw error;
|
|
3202
3216
|
throw new Error(`Failed to fetch Roblox docs for ${key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3203
3217
|
} finally {
|
|
3204
3218
|
clearTimeout(timer);
|
|
3205
3219
|
}
|
|
3206
|
-
if (response.status === 404) {
|
|
3207
|
-
cacheSet(key, { fetchedAt: Date.now(), notFound: true });
|
|
3208
|
-
throw new DocNotFoundError(category, name);
|
|
3209
|
-
}
|
|
3210
|
-
if (!response.ok) {
|
|
3211
|
-
throw new Error(`Failed to fetch Roblox docs for ${key}: HTTP ${response.status}`);
|
|
3212
|
-
}
|
|
3213
|
-
const content = await response.text();
|
|
3214
|
-
cacheSet(key, { fetchedAt: Date.now(), content });
|
|
3215
|
-
return content;
|
|
3216
3220
|
}
|
|
3217
3221
|
function listSections(markdown) {
|
|
3218
3222
|
const sections = [];
|
|
@@ -3414,14 +3418,100 @@ import { execFile, execFileSync as execFileSync2, spawn } from "child_process";
|
|
|
3414
3418
|
import { copyFileSync, existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, realpathSync, rmSync, statSync } from "fs";
|
|
3415
3419
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
3416
3420
|
import * as os2 from "os";
|
|
3417
|
-
import * as
|
|
3421
|
+
import * as path3 from "path";
|
|
3418
3422
|
import { promisify } from "util";
|
|
3419
3423
|
|
|
3424
|
+
// ../core/dist/studio-sign-in.js
|
|
3425
|
+
import { open, readdir } from "fs/promises";
|
|
3426
|
+
import * as path from "path";
|
|
3427
|
+
import { setTimeout as delay } from "timers/promises";
|
|
3428
|
+
var SESSION_LOG = /^[\d.]+_(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z_Studio_[A-Fa-f0-9]+_last\.log$/u;
|
|
3429
|
+
var LINE_TIME = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z),/u;
|
|
3430
|
+
var SIGN_IN_START = /\[FLog::StudioKeyEvents\] login \([^)]*\) \[start\]/u;
|
|
3431
|
+
var SIGN_IN_END = /\[FLog::StudioKeyEvents\] login \[end\]\[(success|failure)\]/u;
|
|
3432
|
+
var HEAD_BYTES = 1024 * 1024;
|
|
3433
|
+
var FILETIME_UNIX_EPOCH = 116444736000000000n;
|
|
3434
|
+
function fileTimeToUnixMs(fileTime) {
|
|
3435
|
+
return Number((BigInt(fileTime) - FILETIME_UNIX_EPOCH) / 10000n);
|
|
3436
|
+
}
|
|
3437
|
+
function studioSessionLogStartMs(name) {
|
|
3438
|
+
const match = SESSION_LOG.exec(name);
|
|
3439
|
+
if (!match)
|
|
3440
|
+
return void 0;
|
|
3441
|
+
const [, year, month, day, hour, minute, second] = match.map(Number);
|
|
3442
|
+
return Date.UTC(year, month - 1, day, hour, minute, second);
|
|
3443
|
+
}
|
|
3444
|
+
function parseStudioSignIn(name, text) {
|
|
3445
|
+
const session = { name, signingIn: false };
|
|
3446
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
3447
|
+
if (SIGN_IN_START.test(line)) {
|
|
3448
|
+
session.signingIn = true;
|
|
3449
|
+
continue;
|
|
3450
|
+
}
|
|
3451
|
+
const end = SIGN_IN_END.exec(line);
|
|
3452
|
+
if (!end)
|
|
3453
|
+
continue;
|
|
3454
|
+
session.signingIn = false;
|
|
3455
|
+
session.outcome = end[1];
|
|
3456
|
+
const time = LINE_TIME.exec(line)?.[1];
|
|
3457
|
+
if (time !== void 0)
|
|
3458
|
+
session.lastEndMs = Date.parse(time);
|
|
3459
|
+
}
|
|
3460
|
+
return session;
|
|
3461
|
+
}
|
|
3462
|
+
async function readStudioSignInSessions(logsRoot, fromMs, toMs) {
|
|
3463
|
+
let names;
|
|
3464
|
+
try {
|
|
3465
|
+
names = await readdir(logsRoot);
|
|
3466
|
+
} catch (error) {
|
|
3467
|
+
if (error.code === "ENOENT")
|
|
3468
|
+
return [];
|
|
3469
|
+
throw error;
|
|
3470
|
+
}
|
|
3471
|
+
const sessions = [];
|
|
3472
|
+
for (const name of names) {
|
|
3473
|
+
const startedAt = studioSessionLogStartMs(name);
|
|
3474
|
+
if (startedAt === void 0 || startedAt < fromMs || startedAt > toMs)
|
|
3475
|
+
continue;
|
|
3476
|
+
const file = await open(path.join(logsRoot, name), "r");
|
|
3477
|
+
try {
|
|
3478
|
+
const buffer = Buffer.alloc(HEAD_BYTES);
|
|
3479
|
+
const { bytesRead } = await file.read(buffer, 0, HEAD_BYTES, 0);
|
|
3480
|
+
sessions.push(parseStudioSignIn(name, buffer.subarray(0, bytesRead).toString("utf8")));
|
|
3481
|
+
} finally {
|
|
3482
|
+
await file.close();
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
return sessions;
|
|
3486
|
+
}
|
|
3487
|
+
async function waitForStudioSignInToSettle({ logsRoot, processStartedAtMs, timeoutMs = 15e3, settleMs = 1500, youngMs = 5e3, now = Date.now, sleep: sleep2 = delay }) {
|
|
3488
|
+
const begin = now();
|
|
3489
|
+
for (; ; ) {
|
|
3490
|
+
const sessions = await readStudioSignInSessions(logsRoot, processStartedAtMs - 2e3, processStartedAtMs + 3e4);
|
|
3491
|
+
const signingIn = sessions.some((session) => session.signingIn);
|
|
3492
|
+
const concluded = sessions.some((session) => session.outcome !== void 0);
|
|
3493
|
+
const undecided = !signingIn && !concluded && now() - processStartedAtMs < youngMs;
|
|
3494
|
+
if (!signingIn && !undecided) {
|
|
3495
|
+
const ends = sessions.flatMap((session) => session.lastEndMs === void 0 ? [] : [session.lastEndMs]);
|
|
3496
|
+
if (!ends.length)
|
|
3497
|
+
return "idle";
|
|
3498
|
+
const sinceEnd = now() - Math.max(...ends);
|
|
3499
|
+
const remaining = Math.min(settleMs, Math.max(0, settleMs - sinceEnd));
|
|
3500
|
+
if (remaining > 0)
|
|
3501
|
+
await sleep2(remaining);
|
|
3502
|
+
return "settled";
|
|
3503
|
+
}
|
|
3504
|
+
if (now() - begin >= timeoutMs)
|
|
3505
|
+
return "timeout";
|
|
3506
|
+
await sleep2(250);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3420
3510
|
// ../core/dist/managed-instance-registry.js
|
|
3421
3511
|
import * as fs from "fs/promises";
|
|
3422
3512
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3423
3513
|
import * as os from "os";
|
|
3424
|
-
import * as
|
|
3514
|
+
import * as path2 from "path";
|
|
3425
3515
|
import { setTimeout as delayTimer } from "timers/promises";
|
|
3426
3516
|
var REGISTRY_VERSION = 1;
|
|
3427
3517
|
var LOCK_STALE_MS = 1e4;
|
|
@@ -3438,14 +3528,14 @@ function defaultManagedInstanceRegistryDir() {
|
|
|
3438
3528
|
return process.env.ROBLOXSTUDIO_MCP_MANAGED_INSTANCE_REGISTRY_DIR;
|
|
3439
3529
|
}
|
|
3440
3530
|
if (process.platform === "win32" && process.env.LOCALAPPDATA) {
|
|
3441
|
-
return
|
|
3531
|
+
return path2.join(process.env.LOCALAPPDATA, "robloxstudio-mcp", "managed-instances", "v1");
|
|
3442
3532
|
}
|
|
3443
3533
|
if (process.platform === "darwin") {
|
|
3444
|
-
return
|
|
3534
|
+
return path2.join(os.homedir(), "Library", "Application Support", "robloxstudio-mcp", "managed-instances", "v1");
|
|
3445
3535
|
}
|
|
3446
|
-
return
|
|
3536
|
+
return path2.join(os.homedir(), ".local", "state", "robloxstudio-mcp", "managed-instances", "v1");
|
|
3447
3537
|
}
|
|
3448
|
-
function
|
|
3538
|
+
function delay2(ms) {
|
|
3449
3539
|
return delayTimer(ms, void 0, { ref: false });
|
|
3450
3540
|
}
|
|
3451
3541
|
function ymd(timestamp) {
|
|
@@ -3540,7 +3630,7 @@ var ManagedInstanceRegistry = class {
|
|
|
3540
3630
|
}
|
|
3541
3631
|
async withLock(fn) {
|
|
3542
3632
|
await this.ensureDir();
|
|
3543
|
-
const lockDir =
|
|
3633
|
+
const lockDir = path2.join(this.dir, ".lock");
|
|
3544
3634
|
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
3545
3635
|
const owner = {
|
|
3546
3636
|
pid: process.pid,
|
|
@@ -3553,7 +3643,7 @@ var ManagedInstanceRegistry = class {
|
|
|
3553
3643
|
activeLockTokens.add(owner.token);
|
|
3554
3644
|
activeLockPaths.add(lockDir);
|
|
3555
3645
|
try {
|
|
3556
|
-
await fs.writeFile(
|
|
3646
|
+
await fs.writeFile(path2.join(lockDir, "owner.json"), `${JSON.stringify(owner)}
|
|
3557
3647
|
`, {
|
|
3558
3648
|
encoding: "utf8",
|
|
3559
3649
|
flag: "wx"
|
|
@@ -3583,7 +3673,7 @@ var ManagedInstanceRegistry = class {
|
|
|
3583
3673
|
if (Date.now() > deadline) {
|
|
3584
3674
|
throw new Error(`Timed out waiting for managed instance registry lock: ${lockDir}`);
|
|
3585
3675
|
}
|
|
3586
|
-
await
|
|
3676
|
+
await delay2(LOCK_RETRY_MS);
|
|
3587
3677
|
}
|
|
3588
3678
|
}
|
|
3589
3679
|
try {
|
|
@@ -3604,7 +3694,7 @@ var ManagedInstanceRegistry = class {
|
|
|
3604
3694
|
}
|
|
3605
3695
|
async readLockOwner(lockPath, isDirectory) {
|
|
3606
3696
|
try {
|
|
3607
|
-
const contents = await fs.readFile(isDirectory ?
|
|
3697
|
+
const contents = await fs.readFile(isDirectory ? path2.join(lockPath, "owner.json") : lockPath, "utf8");
|
|
3608
3698
|
const parsed = JSON.parse(contents);
|
|
3609
3699
|
return isLockOwner(parsed) ? parsed : void 0;
|
|
3610
3700
|
} catch {
|
|
@@ -3615,10 +3705,10 @@ var ManagedInstanceRegistry = class {
|
|
|
3615
3705
|
await fs.mkdir(this.dir, { recursive: true });
|
|
3616
3706
|
}
|
|
3617
3707
|
recordPath(recordId) {
|
|
3618
|
-
return
|
|
3708
|
+
return path2.join(this.dir, `${recordId}.json`);
|
|
3619
3709
|
}
|
|
3620
3710
|
async recordFilesUnlocked() {
|
|
3621
|
-
return (await fs.readdir(this.dir)).filter((name) => name.endsWith(".json")).map((name) =>
|
|
3711
|
+
return (await fs.readdir(this.dir)).filter((name) => name.endsWith(".json")).map((name) => path2.join(this.dir, name));
|
|
3622
3712
|
}
|
|
3623
3713
|
async readRecordUnlocked(recordId) {
|
|
3624
3714
|
try {
|
|
@@ -3647,7 +3737,7 @@ var ManagedInstanceRegistry = class {
|
|
|
3647
3737
|
async writeRecordUnlocked(record) {
|
|
3648
3738
|
await this.ensureDir();
|
|
3649
3739
|
const finalPath = this.recordPath(record.recordId);
|
|
3650
|
-
const tmpPath =
|
|
3740
|
+
const tmpPath = path2.join(this.dir, `${record.recordId}.${process.pid}.${Date.now()}.tmp`);
|
|
3651
3741
|
const fd = await fs.open(tmpPath, "w");
|
|
3652
3742
|
try {
|
|
3653
3743
|
await fd.writeFile(`${JSON.stringify(record, null, 2)}
|
|
@@ -3662,7 +3752,7 @@ var ManagedInstanceRegistry = class {
|
|
|
3662
3752
|
await fs.rm(this.recordPath(recordId), { force: true });
|
|
3663
3753
|
}
|
|
3664
3754
|
async appendEventUnlocked(event, now) {
|
|
3665
|
-
const file =
|
|
3755
|
+
const file = path2.join(this.dir, `events-${ymd(now)}.jsonl`);
|
|
3666
3756
|
await fs.appendFile(file, `${JSON.stringify({
|
|
3667
3757
|
ts: new Date(now).toISOString(),
|
|
3668
3758
|
...event
|
|
@@ -3676,7 +3766,7 @@ var ManagedInstanceRegistry = class {
|
|
|
3676
3766
|
if (!date || date >= cutoff)
|
|
3677
3767
|
continue;
|
|
3678
3768
|
try {
|
|
3679
|
-
await fs.rm(
|
|
3769
|
+
await fs.rm(path2.join(this.dir, name), { force: true });
|
|
3680
3770
|
} catch {
|
|
3681
3771
|
}
|
|
3682
3772
|
}
|
|
@@ -3928,7 +4018,7 @@ function getStudioPlatformCapabilities() {
|
|
|
3928
4018
|
}
|
|
3929
4019
|
|
|
3930
4020
|
// ../core/dist/studio-instance-manager.js
|
|
3931
|
-
var BASEPLATE_TEMP_DIR =
|
|
4021
|
+
var BASEPLATE_TEMP_DIR = path3.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
|
|
3932
4022
|
var BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
|
|
3933
4023
|
var BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
|
|
3934
4024
|
var retainedLaunchControls = /* @__PURE__ */ new Map();
|
|
@@ -4054,7 +4144,7 @@ async function toWslPathAsync(windowsPath) {
|
|
|
4054
4144
|
return runAsync("wslpath", ["-u", windowsPath]);
|
|
4055
4145
|
}
|
|
4056
4146
|
async function toStudioLaunchArgAsync(arg) {
|
|
4057
|
-
if (!isWsl() || !
|
|
4147
|
+
if (!isWsl() || !path3.isAbsolute(arg) || !existsSync2(arg))
|
|
4058
4148
|
return arg;
|
|
4059
4149
|
return runAsync("wslpath", ["-w", arg]);
|
|
4060
4150
|
}
|
|
@@ -4546,6 +4636,19 @@ function buildWindowsStudioStopScript(processId, startedAt) {
|
|
|
4546
4636
|
"} finally { if ($null -ne $studio) { $studio.Dispose() } }"
|
|
4547
4637
|
].join("; ");
|
|
4548
4638
|
}
|
|
4639
|
+
var studioLogsRoot;
|
|
4640
|
+
async function settleWindowsStudioSignIn(startedAt) {
|
|
4641
|
+
studioLogsRoot ??= (async () => {
|
|
4642
|
+
const localAppData = await windowsLocalAppDataAsync();
|
|
4643
|
+
return localAppData ? path3.join(await toWslPathAsync(localAppData), "Roblox", "logs") : void 0;
|
|
4644
|
+
})();
|
|
4645
|
+
const logsRoot = await studioLogsRoot;
|
|
4646
|
+
if (!logsRoot)
|
|
4647
|
+
return;
|
|
4648
|
+
const result = await waitForStudioSignInToSettle({ logsRoot, processStartedAtMs: fileTimeToUnixMs(startedAt) });
|
|
4649
|
+
if (result === "timeout")
|
|
4650
|
+
console.error("[studio] Studio was still signing in after 15 s; closing it anyway.");
|
|
4651
|
+
}
|
|
4549
4652
|
async function stopWindowsStudio(processId, startedAt, timeoutMs = 15e3) {
|
|
4550
4653
|
await powershellAsync(buildWindowsStudioStopScript(processId, startedAt), timeoutMs);
|
|
4551
4654
|
}
|
|
@@ -4559,7 +4662,7 @@ async function spawnWindowsStudio(exe, args, processEnvironment, studioWorkingDi
|
|
|
4559
4662
|
stdio: ["pipe", "pipe", "pipe"],
|
|
4560
4663
|
windowsHide: true
|
|
4561
4664
|
});
|
|
4562
|
-
return new Promise((
|
|
4665
|
+
return new Promise((resolve6, reject) => {
|
|
4563
4666
|
let stdout = "";
|
|
4564
4667
|
let controlOutput = "";
|
|
4565
4668
|
let stderr = "";
|
|
@@ -4706,7 +4809,7 @@ async function spawnWindowsStudio(exe, args, processEnvironment, studioWorkingDi
|
|
|
4706
4809
|
throw new Error("PowerShell returned an invalid native Studio process identity.");
|
|
4707
4810
|
}
|
|
4708
4811
|
identity = { pid: nativePid, startedAt: nativeStartedAt };
|
|
4709
|
-
|
|
4812
|
+
resolve6({
|
|
4710
4813
|
pid: nativePid,
|
|
4711
4814
|
nativePid,
|
|
4712
4815
|
nativeStartedAt,
|
|
@@ -4774,21 +4877,21 @@ function resolveEntrypointDir() {
|
|
|
4774
4877
|
if (!entrypoint)
|
|
4775
4878
|
return void 0;
|
|
4776
4879
|
try {
|
|
4777
|
-
return
|
|
4880
|
+
return path3.dirname(realpathSync(entrypoint));
|
|
4778
4881
|
} catch {
|
|
4779
|
-
return
|
|
4882
|
+
return path3.dirname(path3.resolve(entrypoint));
|
|
4780
4883
|
}
|
|
4781
4884
|
}
|
|
4782
4885
|
function resolveBaseplateTemplatePath() {
|
|
4783
4886
|
const entrypointDir = resolveEntrypointDir();
|
|
4784
4887
|
const candidates = [
|
|
4785
4888
|
...entrypointDir ? [
|
|
4786
|
-
|
|
4787
|
-
|
|
4889
|
+
path3.join(entrypointDir, "assets", BASEPLATE_TEMPLATE_NAME),
|
|
4890
|
+
path3.join(entrypointDir, "..", "assets", BASEPLATE_TEMPLATE_NAME)
|
|
4788
4891
|
] : [],
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4892
|
+
path3.join(process.cwd(), "packages", "core", "assets", BASEPLATE_TEMPLATE_NAME),
|
|
4893
|
+
path3.join(process.cwd(), "packages", "robloxstudio-mcp", "dist", "assets", BASEPLATE_TEMPLATE_NAME),
|
|
4894
|
+
path3.join(process.cwd(), "assets", BASEPLATE_TEMPLATE_NAME)
|
|
4792
4895
|
];
|
|
4793
4896
|
for (const candidate of candidates) {
|
|
4794
4897
|
if (existsSync2(candidate))
|
|
@@ -4821,7 +4924,7 @@ function sweepStaleBaseplateFiles() {
|
|
|
4821
4924
|
continue;
|
|
4822
4925
|
if (Number(match[1]) !== process.pid && isProcessAlive2(Number(match[1])))
|
|
4823
4926
|
continue;
|
|
4824
|
-
const file =
|
|
4927
|
+
const file = path3.join(BASEPLATE_TEMP_DIR, entry);
|
|
4825
4928
|
try {
|
|
4826
4929
|
if (statSync(file).mtimeMs < cutoff)
|
|
4827
4930
|
rmSync(file, { force: true });
|
|
@@ -4832,13 +4935,13 @@ function sweepStaleBaseplateFiles() {
|
|
|
4832
4935
|
function createBaseplatePlaceFile() {
|
|
4833
4936
|
mkdirSync(BASEPLATE_TEMP_DIR, { recursive: true });
|
|
4834
4937
|
sweepStaleBaseplateFiles();
|
|
4835
|
-
const file =
|
|
4938
|
+
const file = path3.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
|
|
4836
4939
|
copyFileSync(resolveBaseplateTemplatePath(), file);
|
|
4837
4940
|
return file;
|
|
4838
4941
|
}
|
|
4839
4942
|
function isGeneratedBaseplatePlaceFile(file) {
|
|
4840
|
-
const resolvedFile =
|
|
4841
|
-
return
|
|
4943
|
+
const resolvedFile = path3.resolve(file);
|
|
4944
|
+
return path3.dirname(resolvedFile) === path3.resolve(BASEPLATE_TEMP_DIR) && BASEPLATE_TEMP_NAME.test(path3.basename(resolvedFile));
|
|
4842
4945
|
}
|
|
4843
4946
|
function cleanupManagedBaseplateFiles(record) {
|
|
4844
4947
|
if (record.source !== "baseplate" || !record.localPlaceFile)
|
|
@@ -4864,14 +4967,14 @@ function resolveStudioExeFromVersions(root) {
|
|
|
4864
4967
|
if (!existsSync2(root)) {
|
|
4865
4968
|
throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
4866
4969
|
}
|
|
4867
|
-
const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) =>
|
|
4970
|
+
const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path3.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
4868
4971
|
if (candidates.length === 0) {
|
|
4869
4972
|
throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
4870
4973
|
}
|
|
4871
4974
|
const exe = candidates[0];
|
|
4872
|
-
const installationDir =
|
|
4975
|
+
const installationDir = path3.dirname(exe);
|
|
4873
4976
|
const hasIncompleteDownload = readdirSync(installationDir).some((name) => /\.crdownload$/i.test(name));
|
|
4874
|
-
const settings = statSync(
|
|
4977
|
+
const settings = statSync(path3.join(installationDir, "AppSettings.xml"), { throwIfNoEntry: false });
|
|
4875
4978
|
if (hasIncompleteDownload || !settings?.isFile() || settings.size === 0) {
|
|
4876
4979
|
const reason = hasIncompleteDownload ? "an unfinished .crdownload download is present" : "AppSettings.xml is missing, empty, or not a regular file";
|
|
4877
4980
|
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.`);
|
|
@@ -4888,7 +4991,7 @@ function resolveStudioExe() {
|
|
|
4888
4991
|
throw new Error("Roblox Studio executable auto-discovery is only supported on Windows, WSL, and macOS. Set ROBLOX_STUDIO_EXE.");
|
|
4889
4992
|
}
|
|
4890
4993
|
const localAppData = windowsLocalAppData();
|
|
4891
|
-
const root = localAppData ?
|
|
4994
|
+
const root = localAppData ? path3.join(toWslPath(localAppData), "Roblox", "Versions") : path3.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
|
|
4892
4995
|
return resolveStudioExeFromVersions(root);
|
|
4893
4996
|
}
|
|
4894
4997
|
async function resolveStudioExeAsync() {
|
|
@@ -4901,7 +5004,7 @@ async function resolveStudioExeAsync() {
|
|
|
4901
5004
|
throw new Error("Roblox Studio executable auto-discovery is only supported on Windows, WSL, and macOS. Set ROBLOX_STUDIO_EXE.");
|
|
4902
5005
|
}
|
|
4903
5006
|
const localAppData = await windowsLocalAppDataAsync();
|
|
4904
|
-
const root = localAppData ?
|
|
5007
|
+
const root = localAppData ? path3.join(await toWslPathAsync(localAppData), "Roblox", "Versions") : path3.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
|
|
4905
5008
|
return resolveStudioExeFromVersions(root);
|
|
4906
5009
|
}
|
|
4907
5010
|
var WINDOWS_STUDIO_PROCESS_QUERY = [
|
|
@@ -5005,11 +5108,11 @@ function buildStudioLaunchArgs(options) {
|
|
|
5005
5108
|
];
|
|
5006
5109
|
}
|
|
5007
5110
|
}
|
|
5008
|
-
function
|
|
5009
|
-
return new Promise((
|
|
5111
|
+
function delay3(ms) {
|
|
5112
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
5010
5113
|
}
|
|
5011
5114
|
function basenameAny(filePath) {
|
|
5012
|
-
return
|
|
5115
|
+
return path3.basename(filePath.replace(/\\/g, "/"));
|
|
5013
5116
|
}
|
|
5014
5117
|
var StudioInstanceManager = class {
|
|
5015
5118
|
managedByInstanceId = /* @__PURE__ */ new Map();
|
|
@@ -5026,6 +5129,7 @@ var StudioInstanceManager = class {
|
|
|
5026
5129
|
snapshotCacheMs;
|
|
5027
5130
|
launchCompletionTimeoutMs;
|
|
5028
5131
|
closeTimeoutMs;
|
|
5132
|
+
settleSignIn;
|
|
5029
5133
|
coordinatorTimer;
|
|
5030
5134
|
coordinatorRefresh;
|
|
5031
5135
|
cachedSnapshot;
|
|
@@ -5041,6 +5145,7 @@ var StudioInstanceManager = class {
|
|
|
5041
5145
|
this.snapshotCacheMs = options.snapshotCacheMs ?? 0;
|
|
5042
5146
|
this.launchCompletionTimeoutMs = options.launchCompletionTimeoutMs ?? LAUNCH_COMPLETION_TIMEOUT_MS;
|
|
5043
5147
|
this.closeTimeoutMs = options.closeTimeoutMs ?? 1e4;
|
|
5148
|
+
this.settleSignIn = options.settleSignIn ?? (!this.processAdapter.stopProcess && (process.platform === "win32" || isWsl()) ? settleWindowsStudioSignIn : void 0);
|
|
5044
5149
|
}
|
|
5045
5150
|
getLifecycleCapabilities() {
|
|
5046
5151
|
const platform = this.platformCapabilities ?? getStudioPlatformCapabilities();
|
|
@@ -5220,8 +5325,8 @@ var StudioInstanceManager = class {
|
|
|
5220
5325
|
async launch(options) {
|
|
5221
5326
|
const previous = this.launchQueue;
|
|
5222
5327
|
let release;
|
|
5223
|
-
this.launchQueue = new Promise((
|
|
5224
|
-
release =
|
|
5328
|
+
this.launchQueue = new Promise((resolve6) => {
|
|
5329
|
+
release = resolve6;
|
|
5225
5330
|
});
|
|
5226
5331
|
await previous;
|
|
5227
5332
|
try {
|
|
@@ -5380,7 +5485,7 @@ var StudioInstanceManager = class {
|
|
|
5380
5485
|
await this.persist(record);
|
|
5381
5486
|
break;
|
|
5382
5487
|
}
|
|
5383
|
-
await
|
|
5488
|
+
await delay3(250);
|
|
5384
5489
|
}
|
|
5385
5490
|
if (record.nativeProcessId === void 0 && process.platform !== "win32" && !isWsl()) {
|
|
5386
5491
|
record.nativeProcessId = proc.pid;
|
|
@@ -5438,7 +5543,7 @@ var StudioInstanceManager = class {
|
|
|
5438
5543
|
instanceId: record.instanceId
|
|
5439
5544
|
};
|
|
5440
5545
|
}
|
|
5441
|
-
|
|
5546
|
+
let deadline = Date.now() + this.closeTimeoutMs;
|
|
5442
5547
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
5443
5548
|
if (!processId) {
|
|
5444
5549
|
throw new Error(`Cannot close ${record.instanceId ?? "Studio launch"} because its process id was not detected.`);
|
|
@@ -5477,6 +5582,8 @@ var StudioInstanceManager = class {
|
|
|
5477
5582
|
};
|
|
5478
5583
|
}
|
|
5479
5584
|
}
|
|
5585
|
+
if (!abort)
|
|
5586
|
+
deadline = await this.beforeStudioStop(record.nativeProcessStartedAt, deadline);
|
|
5480
5587
|
try {
|
|
5481
5588
|
await this.closeProcess(processId, record.nativeProcessStartedAt, abort, deadline);
|
|
5482
5589
|
} catch (error) {
|
|
@@ -5525,7 +5632,7 @@ var StudioInstanceManager = class {
|
|
|
5525
5632
|
};
|
|
5526
5633
|
}
|
|
5527
5634
|
async closeConnectedInstance(instance) {
|
|
5528
|
-
|
|
5635
|
+
let deadline = Date.now() + this.closeTimeoutMs;
|
|
5529
5636
|
const snapshot = await beforeCloseDeadline(() => this.readProcessSnapshot(deadline - Date.now()), deadline, 0);
|
|
5530
5637
|
if (snapshot.status === "error") {
|
|
5531
5638
|
throw new Error(`Could not enumerate Studio processes: ${snapshot.error}`);
|
|
@@ -5534,8 +5641,20 @@ var StudioInstanceManager = class {
|
|
|
5534
5641
|
if (!process2) {
|
|
5535
5642
|
throw new Error(`Could not find a Studio process for connected instance "${instance.instanceId}".`);
|
|
5536
5643
|
}
|
|
5644
|
+
deadline = await this.beforeStudioStop(process2.StartTimeUtcFileTime, deadline);
|
|
5537
5645
|
await this.closeProcess(process2.Id, process2.StartTimeUtcFileTime, void 0, deadline);
|
|
5538
5646
|
}
|
|
5647
|
+
async beforeStudioStop(startedAt, deadline) {
|
|
5648
|
+
if (!this.settleSignIn || startedAt === void 0 || !/^[1-9]\d*$/u.test(startedAt))
|
|
5649
|
+
return deadline;
|
|
5650
|
+
const waitStarted = Date.now();
|
|
5651
|
+
try {
|
|
5652
|
+
await this.settleSignIn(startedAt);
|
|
5653
|
+
} catch (error) {
|
|
5654
|
+
console.error(`[studio] Could not check Studio sign-in before closing: ${error instanceof Error ? error.message : String(error)}`);
|
|
5655
|
+
}
|
|
5656
|
+
return deadline + (Date.now() - waitStarted);
|
|
5657
|
+
}
|
|
5539
5658
|
async closeProcess(processId, startedAt, stop, deadline = Date.now() + this.closeTimeoutMs) {
|
|
5540
5659
|
this.cachedSnapshot = void 0;
|
|
5541
5660
|
await beforeCloseDeadline(async () => {
|
|
@@ -5557,7 +5676,7 @@ var StudioInstanceManager = class {
|
|
|
5557
5676
|
}
|
|
5558
5677
|
}
|
|
5559
5678
|
}, deadline, processId);
|
|
5560
|
-
|
|
5679
|
+
for (; ; ) {
|
|
5561
5680
|
const observation = await beforeCloseDeadline(() => this.observeClosingProcess(processId, startedAt, deadline), deadline, processId);
|
|
5562
5681
|
if (observation.status === "not_running")
|
|
5563
5682
|
return;
|
|
@@ -5568,7 +5687,7 @@ var StudioInstanceManager = class {
|
|
|
5568
5687
|
if (remaining <= 0) {
|
|
5569
5688
|
throw new StudioCloseVerificationError(`Timed out closing Studio process ${processId}: the process is still running. Close can be retried.`, observation);
|
|
5570
5689
|
}
|
|
5571
|
-
await
|
|
5690
|
+
await delay3(Math.min(100, remaining));
|
|
5572
5691
|
if (Date.now() >= deadline) {
|
|
5573
5692
|
throw new StudioCloseVerificationError(`Timed out closing Studio process ${processId}: the process is still running. Close can be retried.`, observation);
|
|
5574
5693
|
}
|
|
@@ -5736,13 +5855,13 @@ var StudioInstanceManager = class {
|
|
|
5736
5855
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
5737
5856
|
if (record.spawnPid && record.spawnPid === processId && studioProcess.Id === processId)
|
|
5738
5857
|
return true;
|
|
5739
|
-
const processPath = studioProcess.Path ?
|
|
5740
|
-
const exePath = record.exe ?
|
|
5858
|
+
const processPath = studioProcess.Path ? path3.normalize(studioProcess.Path).toLowerCase() : "";
|
|
5859
|
+
const exePath = record.exe ? path3.normalize(record.exe).toLowerCase() : "";
|
|
5741
5860
|
if (processPath && exePath && (processPath === exePath || basenameAny(processPath) === basenameAny(exePath))) {
|
|
5742
5861
|
return true;
|
|
5743
5862
|
}
|
|
5744
5863
|
const commandLine = studioProcess.CommandLine ?? "";
|
|
5745
|
-
if (record.localPlaceFile && commandLine.includes(
|
|
5864
|
+
if (record.localPlaceFile && commandLine.includes(path3.basename(record.localPlaceFile)))
|
|
5746
5865
|
return true;
|
|
5747
5866
|
if (record.placeId !== void 0 && commandLine.includes(String(record.placeId)))
|
|
5748
5867
|
return true;
|
|
@@ -6273,11 +6392,21 @@ function createToolHttpHandler(options) {
|
|
|
6273
6392
|
|
|
6274
6393
|
// ../core/dist/auth.js
|
|
6275
6394
|
import { randomBytes, createHash as createHash2, timingSafeEqual } from "crypto";
|
|
6276
|
-
import {
|
|
6277
|
-
import { join as
|
|
6395
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync, linkSync, unlinkSync } from "fs";
|
|
6396
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
6278
6397
|
import { homedir as homedir3 } from "os";
|
|
6279
6398
|
function authTokenFilePath() {
|
|
6280
|
-
return
|
|
6399
|
+
return join4(homedir3(), ".robloxstudio-mcp", "auth-token");
|
|
6400
|
+
}
|
|
6401
|
+
var EMPTY_TOKEN_RETRY_MS = 25;
|
|
6402
|
+
var EMPTY_TOKEN_RETRIES = 20;
|
|
6403
|
+
function readTokenFile(filePath) {
|
|
6404
|
+
for (let attempt = 0; ; attempt++) {
|
|
6405
|
+
const token = readFileSync3(filePath, "utf8").trim();
|
|
6406
|
+
if (token || attempt >= EMPTY_TOKEN_RETRIES)
|
|
6407
|
+
return token;
|
|
6408
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, EMPTY_TOKEN_RETRY_MS);
|
|
6409
|
+
}
|
|
6281
6410
|
}
|
|
6282
6411
|
function resolveAuthToken() {
|
|
6283
6412
|
const noAuth = (process.env.ROBLOX_STUDIO_NO_AUTH || "").toLowerCase();
|
|
@@ -6290,20 +6419,36 @@ function resolveAuthToken() {
|
|
|
6290
6419
|
}
|
|
6291
6420
|
const filePath = authTokenFilePath();
|
|
6292
6421
|
try {
|
|
6293
|
-
|
|
6294
|
-
const existing =
|
|
6295
|
-
if (existing)
|
|
6296
|
-
|
|
6297
|
-
}
|
|
6422
|
+
try {
|
|
6423
|
+
const existing = readTokenFile(filePath);
|
|
6424
|
+
if (!existing)
|
|
6425
|
+
throw new Error("Auth token file is empty; remove it while all MCP servers are stopped to reinitialize it.");
|
|
6426
|
+
return { token: existing, source: "file", filePath };
|
|
6427
|
+
} catch (error) {
|
|
6428
|
+
if (error.code !== "ENOENT")
|
|
6429
|
+
throw error;
|
|
6298
6430
|
}
|
|
6299
6431
|
const fresh = randomBytes(32).toString("hex");
|
|
6300
6432
|
mkdirSync2(dirname2(filePath), { recursive: true, mode: 448 });
|
|
6301
|
-
|
|
6433
|
+
const stagingPath = `${filePath}.${process.pid}.${randomBytes(16).toString("hex")}.tmp`;
|
|
6434
|
+
writeFileSync(stagingPath, fresh + "\n", { encoding: "utf8", mode: 384, flag: "wx" });
|
|
6302
6435
|
try {
|
|
6303
|
-
|
|
6304
|
-
|
|
6436
|
+
try {
|
|
6437
|
+
linkSync(stagingPath, filePath);
|
|
6438
|
+
} catch (error) {
|
|
6439
|
+
if (error.code !== "EEXIST")
|
|
6440
|
+
throw error;
|
|
6441
|
+
}
|
|
6442
|
+
const token = readTokenFile(filePath);
|
|
6443
|
+
if (!token)
|
|
6444
|
+
throw new Error("Auth token file is empty.");
|
|
6445
|
+
return { token, source: "file", filePath };
|
|
6446
|
+
} finally {
|
|
6447
|
+
try {
|
|
6448
|
+
unlinkSync(stagingPath);
|
|
6449
|
+
} catch {
|
|
6450
|
+
}
|
|
6305
6451
|
}
|
|
6306
|
-
return { token: fresh, source: "file", filePath };
|
|
6307
6452
|
} catch (err2) {
|
|
6308
6453
|
console.error(`[auth] Could not read/create ${filePath} (${err2 instanceof Error ? err2.message : err2}). Using an in-memory token for this process; set ROBLOX_STUDIO_AUTH_TOKEN to share one across sessions.`);
|
|
6309
6454
|
return { token: randomBytes(32).toString("hex"), source: "file" };
|
|
@@ -6821,7 +6966,7 @@ var TOOL_HANDLERS = {
|
|
|
6821
6966
|
get_memory_breakdown: (tools, body) => tools.getMemoryBreakdown(body.target, body.tags, body.instance_id),
|
|
6822
6967
|
get_scene_analysis: (tools, body) => tools.getSceneAnalysis(body.mode, body.target, body.topN, body.raw, body.instance_id),
|
|
6823
6968
|
export_rbxm: (tools, body) => tools.exportRbxm(body.instance_paths, body.output_path, body.target, body.instance_id),
|
|
6824
|
-
import_rbxm: (tools, body) => tools.importRbxm(body.source, body.parent_path, body.target, body.instance_id),
|
|
6969
|
+
import_rbxm: (tools, body, context) => tools.importRbxm(body.source, body.parent_path, body.target, body.instance_id, context?.signal),
|
|
6825
6970
|
find_and_replace_in_scripts: (tools, body) => tools.findAndReplaceInScripts(body.pattern, body.replacement, {
|
|
6826
6971
|
caseSensitive: body.caseSensitive,
|
|
6827
6972
|
usePattern: body.usePattern,
|
|
@@ -6990,8 +7135,8 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
6990
7135
|
});
|
|
6991
7136
|
const authToken = security?.authToken;
|
|
6992
7137
|
const authRequired = (requestPath) => {
|
|
6993
|
-
const
|
|
6994
|
-
return
|
|
7138
|
+
const path9 = requestPath.toLowerCase().replace(/\/+$/, "");
|
|
7139
|
+
return path9 === "/mcp" || path9.startsWith("/mcp/") || path9 === "/proxy" || path9 === "/topology" || path9 === "/request-status" || path9 === "/unregister-instance-id" || path9 === "/create-multiplayer-group" || path9 === "/remove-multiplayer-group";
|
|
6995
7140
|
};
|
|
6996
7141
|
app.use((req, res, next) => {
|
|
6997
7142
|
if (!authToken || !authRequired(req.path)) {
|
|
@@ -7420,6 +7565,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
7420
7565
|
studioTransport.close();
|
|
7421
7566
|
webSocketServer.close();
|
|
7422
7567
|
await mcpHandler?.close();
|
|
7568
|
+
await tools.dispose();
|
|
7423
7569
|
};
|
|
7424
7570
|
return app;
|
|
7425
7571
|
}
|
|
@@ -7440,7 +7586,7 @@ async function listenWithRetry(app, host, startPort, maxAttempts = 5) {
|
|
|
7440
7586
|
throw new Error(`All ports ${startPort}-${startPort + maxAttempts - 1} are in use. Stop some MCP server instances and retry.`);
|
|
7441
7587
|
}
|
|
7442
7588
|
function bindPort(app, host, port) {
|
|
7443
|
-
const { promise, resolve:
|
|
7589
|
+
const { promise, resolve: resolve6, reject } = Promise.withResolvers();
|
|
7444
7590
|
const server = http.createServer(app);
|
|
7445
7591
|
const onError = (err2) => {
|
|
7446
7592
|
server.removeListener("error", onError);
|
|
@@ -7453,7 +7599,7 @@ function bindPort(app, host, port) {
|
|
|
7453
7599
|
if ("attachStudioTransport" in app && typeof app.attachStudioTransport === "function") {
|
|
7454
7600
|
app.attachStudioTransport(server);
|
|
7455
7601
|
}
|
|
7456
|
-
|
|
7602
|
+
resolve6(server);
|
|
7457
7603
|
} catch (error) {
|
|
7458
7604
|
server.close();
|
|
7459
7605
|
reject(error);
|
|
@@ -7462,6 +7608,123 @@ function bindPort(app, host, port) {
|
|
|
7462
7608
|
return promise;
|
|
7463
7609
|
}
|
|
7464
7610
|
|
|
7611
|
+
// ../core/dist/rbxm-input.js
|
|
7612
|
+
import { open as open3 } from "fs/promises";
|
|
7613
|
+
import { resolve as resolve2 } from "path";
|
|
7614
|
+
var ENVELOPE_RESERVE_BYTES = 16 * 1024;
|
|
7615
|
+
var DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
7616
|
+
function rbxmInputByteLimit(parentPath, sourceLabel) {
|
|
7617
|
+
const metadataBytes = Buffer.byteLength(JSON.stringify({
|
|
7618
|
+
base64: "",
|
|
7619
|
+
parent_path: parentPath,
|
|
7620
|
+
source_label: sourceLabel
|
|
7621
|
+
}));
|
|
7622
|
+
const encodedBytes = Math.min(HTTP_BODY_LIMIT_BYTES, MAX_STUDIO_FRAME_BYTES) - ENVELOPE_RESERVE_BYTES - metadataBytes;
|
|
7623
|
+
if (encodedBytes < 4)
|
|
7624
|
+
throw new Error("RBXM import metadata exceeds the transport byte budget.");
|
|
7625
|
+
return Math.floor(encodedBytes / 4) * 3;
|
|
7626
|
+
}
|
|
7627
|
+
function tooLarge(limit) {
|
|
7628
|
+
return new Error(`RBXM input exceeds the ${limit}-byte limit after allowing for base64 and transport overhead.`);
|
|
7629
|
+
}
|
|
7630
|
+
async function readBoundedResponse(response, limit) {
|
|
7631
|
+
const declared = Number(response.headers.get("content-length"));
|
|
7632
|
+
if (Number.isFinite(declared) && declared > limit) {
|
|
7633
|
+
await response.body?.cancel();
|
|
7634
|
+
throw tooLarge(limit);
|
|
7635
|
+
}
|
|
7636
|
+
if (!response.body)
|
|
7637
|
+
return Buffer.alloc(0);
|
|
7638
|
+
const reader = response.body.getReader();
|
|
7639
|
+
const chunks = [];
|
|
7640
|
+
let size = 0;
|
|
7641
|
+
try {
|
|
7642
|
+
for (; ; ) {
|
|
7643
|
+
const { done, value } = await reader.read();
|
|
7644
|
+
if (done)
|
|
7645
|
+
break;
|
|
7646
|
+
size += value.byteLength;
|
|
7647
|
+
if (size > limit)
|
|
7648
|
+
throw tooLarge(limit);
|
|
7649
|
+
chunks.push(Buffer.from(value));
|
|
7650
|
+
}
|
|
7651
|
+
return Buffer.concat(chunks, size);
|
|
7652
|
+
} finally {
|
|
7653
|
+
await reader.cancel().catch(() => {
|
|
7654
|
+
});
|
|
7655
|
+
reader.releaseLock();
|
|
7656
|
+
}
|
|
7657
|
+
}
|
|
7658
|
+
async function readRbxmInput(source, parentPath, signal) {
|
|
7659
|
+
const modes = ["path", "url", "base64"];
|
|
7660
|
+
const provided = modes.filter((key) => source[key] !== void 0);
|
|
7661
|
+
if (provided.length !== 1 || typeof source[provided[0]] !== "string") {
|
|
7662
|
+
throw new Error("source must contain exactly one string: path, url, or base64");
|
|
7663
|
+
}
|
|
7664
|
+
signal?.throwIfAborted();
|
|
7665
|
+
if (source.path !== void 0) {
|
|
7666
|
+
const sourceLabel = resolve2(source.path);
|
|
7667
|
+
const limit2 = rbxmInputByteLimit(parentPath, sourceLabel);
|
|
7668
|
+
const file = await open3(sourceLabel, "r");
|
|
7669
|
+
try {
|
|
7670
|
+
const stats = await file.stat();
|
|
7671
|
+
if (!stats.isFile())
|
|
7672
|
+
throw new Error("RBXM source must be a regular file.");
|
|
7673
|
+
if (stats.size > limit2)
|
|
7674
|
+
throw tooLarge(limit2);
|
|
7675
|
+
const chunks = [];
|
|
7676
|
+
let size = 0;
|
|
7677
|
+
for (; ; ) {
|
|
7678
|
+
signal?.throwIfAborted();
|
|
7679
|
+
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, limit2 - size + 1));
|
|
7680
|
+
const { bytesRead } = await file.read(chunk);
|
|
7681
|
+
if (bytesRead === 0)
|
|
7682
|
+
break;
|
|
7683
|
+
size += bytesRead;
|
|
7684
|
+
if (size > limit2)
|
|
7685
|
+
throw tooLarge(limit2);
|
|
7686
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
7687
|
+
}
|
|
7688
|
+
return { bytes: Buffer.concat(chunks, size), sourceLabel };
|
|
7689
|
+
} finally {
|
|
7690
|
+
await file.close();
|
|
7691
|
+
}
|
|
7692
|
+
}
|
|
7693
|
+
if (source.url !== void 0) {
|
|
7694
|
+
const url = new URL(source.url);
|
|
7695
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
7696
|
+
throw new Error(`import_rbxm url must use http(s); got ${url.protocol}`);
|
|
7697
|
+
}
|
|
7698
|
+
const limit2 = rbxmInputByteLimit(parentPath, source.url);
|
|
7699
|
+
const deadline = AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS);
|
|
7700
|
+
const response = await fetch(url, {
|
|
7701
|
+
signal: signal ? AbortSignal.any([signal, deadline]) : deadline
|
|
7702
|
+
});
|
|
7703
|
+
if (!response.ok) {
|
|
7704
|
+
await response.body?.cancel();
|
|
7705
|
+
throw new Error(`RBXM download returned HTTP ${response.status}`);
|
|
7706
|
+
}
|
|
7707
|
+
return { bytes: await readBoundedResponse(response, limit2), sourceLabel: source.url };
|
|
7708
|
+
}
|
|
7709
|
+
const limit = rbxmInputByteLimit(parentPath, "base64(9999999999B)");
|
|
7710
|
+
const maxEncoded = 4 * Math.ceil(limit / 3);
|
|
7711
|
+
if (source.base64.length > maxEncoded + 2 * Math.ceil(maxEncoded / 64))
|
|
7712
|
+
throw tooLarge(limit);
|
|
7713
|
+
const encoded = source.base64.replace(/[\t\n\r ]+/g, "");
|
|
7714
|
+
if (encoded.length > maxEncoded)
|
|
7715
|
+
throw tooLarge(limit);
|
|
7716
|
+
if (encoded.length % 4 === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(encoded) || encoded.includes("=") && encoded.length % 4 !== 0) {
|
|
7717
|
+
throw new Error("RBXM base64 must be valid base64.");
|
|
7718
|
+
}
|
|
7719
|
+
const bytes = Buffer.from(encoded, "base64");
|
|
7720
|
+
if (bytes.length > limit)
|
|
7721
|
+
throw tooLarge(limit);
|
|
7722
|
+
if (bytes.toString("base64").replace(/=+$/, "") !== encoded.replace(/=+$/, "")) {
|
|
7723
|
+
throw new Error("RBXM base64 is not canonical.");
|
|
7724
|
+
}
|
|
7725
|
+
return { bytes, sourceLabel: `base64(${bytes.length}B)` };
|
|
7726
|
+
}
|
|
7727
|
+
|
|
7465
7728
|
// ../core/dist/tools/studio-client.js
|
|
7466
7729
|
var StudioHttpClient = class {
|
|
7467
7730
|
bridge;
|
|
@@ -7529,7 +7792,6 @@ var OpenCloudClient = class {
|
|
|
7529
7792
|
body: body ? JSON.stringify(body) : void 0,
|
|
7530
7793
|
signal: controller.signal
|
|
7531
7794
|
});
|
|
7532
|
-
clearTimeout(timeoutId);
|
|
7533
7795
|
if (!response.ok) {
|
|
7534
7796
|
const errorBody = await response.text();
|
|
7535
7797
|
let errorMessage2;
|
|
@@ -7551,7 +7813,8 @@ var OpenCloudClient = class {
|
|
|
7551
7813
|
}
|
|
7552
7814
|
return await response.json();
|
|
7553
7815
|
} catch (error) {
|
|
7554
|
-
|
|
7816
|
+
if (controller.signal.aborted)
|
|
7817
|
+
throw new Error("Request timed out");
|
|
7555
7818
|
if (error instanceof Error) {
|
|
7556
7819
|
if (error.name === "AbortError") {
|
|
7557
7820
|
throw new Error("Request timed out");
|
|
@@ -7559,6 +7822,8 @@ var OpenCloudClient = class {
|
|
|
7559
7822
|
throw error;
|
|
7560
7823
|
}
|
|
7561
7824
|
throw new Error(`Unknown error: ${String(error)}`);
|
|
7825
|
+
} finally {
|
|
7826
|
+
clearTimeout(timeoutId);
|
|
7562
7827
|
}
|
|
7563
7828
|
}
|
|
7564
7829
|
async searchAssets(params) {
|
|
@@ -7593,7 +7858,8 @@ var OpenCloudClient = class {
|
|
|
7593
7858
|
async getAssetThumbnail(assetId, size = "420x420") {
|
|
7594
7859
|
const url = `https://thumbnails.roblox.com/v1/assets?assetIds=${assetId}&size=${size}&format=Png`;
|
|
7595
7860
|
try {
|
|
7596
|
-
const
|
|
7861
|
+
const signal = AbortSignal.timeout(this.timeout);
|
|
7862
|
+
const response = await fetch(url, { signal });
|
|
7597
7863
|
if (!response.ok)
|
|
7598
7864
|
return null;
|
|
7599
7865
|
const data = await response.json();
|
|
@@ -7601,7 +7867,7 @@ var OpenCloudClient = class {
|
|
|
7601
7867
|
if (!thumbnail || thumbnail.state !== "Completed" || !thumbnail.imageUrl) {
|
|
7602
7868
|
return null;
|
|
7603
7869
|
}
|
|
7604
|
-
const imageResponse = await fetch(thumbnail.imageUrl);
|
|
7870
|
+
const imageResponse = await fetch(thumbnail.imageUrl, { signal });
|
|
7605
7871
|
if (!imageResponse.ok)
|
|
7606
7872
|
return null;
|
|
7607
7873
|
const arrayBuffer = await imageResponse.arrayBuffer();
|
|
@@ -7622,7 +7888,7 @@ var OpenCloudClient = class {
|
|
|
7622
7888
|
for (const batch of batches) {
|
|
7623
7889
|
const url = `https://thumbnails.roblox.com/v1/assets?assetIds=${batch.join(",")}&size=${size}&format=Png`;
|
|
7624
7890
|
try {
|
|
7625
|
-
const response = await fetch(url);
|
|
7891
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(this.timeout) });
|
|
7626
7892
|
if (response.ok) {
|
|
7627
7893
|
const data = await response.json();
|
|
7628
7894
|
for (const thumbnail of data.data) {
|
|
@@ -7683,7 +7949,7 @@ var OpenCloudClient = class {
|
|
|
7683
7949
|
const chunks = [];
|
|
7684
7950
|
let totalBytes = 0;
|
|
7685
7951
|
const reader = contentResponse.body.getReader();
|
|
7686
|
-
|
|
7952
|
+
for (; ; ) {
|
|
7687
7953
|
const { done, value } = await reader.read();
|
|
7688
7954
|
if (done)
|
|
7689
7955
|
break;
|
|
@@ -7764,7 +8030,6 @@ var OpenCloudClient = class {
|
|
|
7764
8030
|
body: formData,
|
|
7765
8031
|
signal: controller.signal
|
|
7766
8032
|
});
|
|
7767
|
-
clearTimeout(timeoutId);
|
|
7768
8033
|
if (!response.ok) {
|
|
7769
8034
|
const errorBody = await response.text();
|
|
7770
8035
|
let errorMessage2;
|
|
@@ -7786,7 +8051,8 @@ var OpenCloudClient = class {
|
|
|
7786
8051
|
}
|
|
7787
8052
|
return await response.json();
|
|
7788
8053
|
} catch (error) {
|
|
7789
|
-
|
|
8054
|
+
if (controller.signal.aborted)
|
|
8055
|
+
throw new Error("Request timed out");
|
|
7790
8056
|
if (error instanceof Error) {
|
|
7791
8057
|
if (error.name === "AbortError") {
|
|
7792
8058
|
throw new Error("Request timed out");
|
|
@@ -7794,6 +8060,8 @@ var OpenCloudClient = class {
|
|
|
7794
8060
|
throw error;
|
|
7795
8061
|
}
|
|
7796
8062
|
throw new Error(`Unknown error: ${String(error)}`);
|
|
8063
|
+
} finally {
|
|
8064
|
+
clearTimeout(timeoutId);
|
|
7797
8065
|
}
|
|
7798
8066
|
}
|
|
7799
8067
|
async pollOperation(operationPath, maxAttempts = 30, intervalMs = 2e3) {
|
|
@@ -7805,7 +8073,7 @@ var OpenCloudClient = class {
|
|
|
7805
8073
|
if (result.error) {
|
|
7806
8074
|
throw new Error(`Asset upload failed: ${result.error.message}`);
|
|
7807
8075
|
}
|
|
7808
|
-
await new Promise((
|
|
8076
|
+
await new Promise((resolve6) => setTimeout(resolve6, intervalMs));
|
|
7809
8077
|
}
|
|
7810
8078
|
throw new Error(`Asset upload timed out after ${maxAttempts * intervalMs / 1e3}s. Operation ID: ${operationId}`);
|
|
7811
8079
|
}
|
|
@@ -7822,6 +8090,7 @@ var RobloxCookieClient = class {
|
|
|
7822
8090
|
return !!this.cookie;
|
|
7823
8091
|
}
|
|
7824
8092
|
async fetchWithCsrf(url, options = {}) {
|
|
8093
|
+
options = { ...options, signal: options.signal ?? AbortSignal.timeout(3e4) };
|
|
7825
8094
|
const headers = {
|
|
7826
8095
|
Cookie: `.ROBLOSECURITY=${this.cookie}`,
|
|
7827
8096
|
...options.headers || {}
|
|
@@ -7835,6 +8104,7 @@ var RobloxCookieClient = class {
|
|
|
7835
8104
|
if (newToken) {
|
|
7836
8105
|
this.csrfToken = newToken;
|
|
7837
8106
|
headers["X-CSRF-TOKEN"] = newToken;
|
|
8107
|
+
await response.body?.cancel();
|
|
7838
8108
|
return fetch(url, { ...options, headers });
|
|
7839
8109
|
}
|
|
7840
8110
|
}
|
|
@@ -7954,7 +8224,7 @@ var RobloxCookieClient = class {
|
|
|
7954
8224
|
throw new Error("Image upload completed without an asset ID.");
|
|
7955
8225
|
}
|
|
7956
8226
|
if (attempt < maxAttempts - 1) {
|
|
7957
|
-
await new Promise((
|
|
8227
|
+
await new Promise((resolve6) => setTimeout(resolve6, intervalMs));
|
|
7958
8228
|
}
|
|
7959
8229
|
}
|
|
7960
8230
|
throw new Error(`Image upload timed out after ${maxAttempts * intervalMs / 1e3}s. Operation ID: ${operationId}`);
|
|
@@ -7978,7 +8248,7 @@ var RobloxCookieClient = class {
|
|
|
7978
8248
|
|
|
7979
8249
|
// ../core/dist/image-decode.js
|
|
7980
8250
|
import * as fs2 from "fs";
|
|
7981
|
-
import * as
|
|
8251
|
+
import * as path4 from "path";
|
|
7982
8252
|
import { inflateSync } from "zlib";
|
|
7983
8253
|
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
7984
8254
|
var MAX_EDITABLE_IMAGE_DIMENSION = 1024;
|
|
@@ -8310,14 +8580,14 @@ function decodePngBase64ToRgba(encoded) {
|
|
|
8310
8580
|
return decodePngToRgba(data);
|
|
8311
8581
|
}
|
|
8312
8582
|
function decodeImagePathToRgba(imagePath) {
|
|
8313
|
-
const resolved =
|
|
8583
|
+
const resolved = path4.resolve(imagePath);
|
|
8314
8584
|
return decodePngToRgba(readPngFileWithinLimit(resolved, imagePath));
|
|
8315
8585
|
}
|
|
8316
8586
|
|
|
8317
8587
|
// ../core/dist/studio-skills.js
|
|
8318
8588
|
import { createHash as createHash3 } from "crypto";
|
|
8319
|
-
import { existsSync as
|
|
8320
|
-
import * as
|
|
8589
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
8590
|
+
import * as path5 from "path";
|
|
8321
8591
|
|
|
8322
8592
|
// ../../node_modules/fzstd/esm/index.mjs
|
|
8323
8593
|
var ab = ArrayBuffer;
|
|
@@ -9205,7 +9475,7 @@ function parseBuiltInStudioSkills(buffer) {
|
|
|
9205
9475
|
}).sort((left, right) => left.name.localeCompare(right.name));
|
|
9206
9476
|
}
|
|
9207
9477
|
function discoverNamedFile(root, fileName, depth = 0) {
|
|
9208
|
-
if (depth > MAX_DISCOVERY_DEPTH || !
|
|
9478
|
+
if (depth > MAX_DISCOVERY_DEPTH || !existsSync3(root))
|
|
9209
9479
|
return [];
|
|
9210
9480
|
const matches = [];
|
|
9211
9481
|
let entries;
|
|
@@ -9215,7 +9485,7 @@ function discoverNamedFile(root, fileName, depth = 0) {
|
|
|
9215
9485
|
return matches;
|
|
9216
9486
|
}
|
|
9217
9487
|
for (const entry of entries) {
|
|
9218
|
-
const entryPath =
|
|
9488
|
+
const entryPath = path5.join(root, entry.name);
|
|
9219
9489
|
if (entry.isFile() && entry.name.toLowerCase() === fileName.toLowerCase()) {
|
|
9220
9490
|
matches.push(entryPath);
|
|
9221
9491
|
} else if (entry.isDirectory()) {
|
|
@@ -9227,20 +9497,20 @@ function discoverNamedFile(root, fileName, depth = 0) {
|
|
|
9227
9497
|
function resolveAssistantBundlePath(studioExe) {
|
|
9228
9498
|
const override = process.env.ROBLOX_STUDIO_ASSISTANT_BUNDLE;
|
|
9229
9499
|
if (override) {
|
|
9230
|
-
if (!
|
|
9500
|
+
if (!existsSync3(override)) {
|
|
9231
9501
|
throw new Error(`ROBLOX_STUDIO_ASSISTANT_BUNDLE does not exist: ${override}`);
|
|
9232
9502
|
}
|
|
9233
9503
|
return override;
|
|
9234
9504
|
}
|
|
9235
|
-
const exeDirectory =
|
|
9505
|
+
const exeDirectory = path5.dirname(studioExe ?? resolveStudioExe());
|
|
9236
9506
|
const roots = [
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9507
|
+
path5.join(exeDirectory, "BuiltInStandalonePlugins"),
|
|
9508
|
+
path5.resolve(exeDirectory, "..", "Resources", "BuiltInStandalonePlugins"),
|
|
9509
|
+
path5.resolve(exeDirectory, "..", "..", "OTAPlugins")
|
|
9240
9510
|
];
|
|
9241
|
-
const direct =
|
|
9511
|
+
const direct = path5.join(exeDirectory, "BuiltInStandalonePlugins", "Optimized_Embedded_Signature", "Assistant.rbxm");
|
|
9242
9512
|
const candidates = /* @__PURE__ */ new Set();
|
|
9243
|
-
if (
|
|
9513
|
+
if (existsSync3(direct))
|
|
9244
9514
|
candidates.add(direct);
|
|
9245
9515
|
for (const root of roots) {
|
|
9246
9516
|
for (const candidate of discoverNamedFile(root, "Assistant.rbxm"))
|
|
@@ -9263,7 +9533,7 @@ function loadBuiltInStudioSkills(bundlePath = resolveAssistantBundlePath()) {
|
|
|
9263
9533
|
if (skills.length === 0) {
|
|
9264
9534
|
throw new Error(`No built-in skill documents found in ${bundlePath}`);
|
|
9265
9535
|
}
|
|
9266
|
-
const studioVersion = bundlePath.split(
|
|
9536
|
+
const studioVersion = bundlePath.split(path5.sep).find((segment) => /^version-[a-z0-9]+$/i.test(segment));
|
|
9267
9537
|
const value = {
|
|
9268
9538
|
bundlePath,
|
|
9269
9539
|
bundleModifiedAt: stats.mtime.toISOString(),
|
|
@@ -10284,10 +10554,280 @@ function rgbaToPng(rgba, width, height) {
|
|
|
10284
10554
|
}
|
|
10285
10555
|
|
|
10286
10556
|
// ../core/dist/host-capture.js
|
|
10557
|
+
import { spawn as spawn3 } from "child_process";
|
|
10558
|
+
import * as fs4 from "fs";
|
|
10559
|
+
import * as os4 from "os";
|
|
10560
|
+
import * as path7 from "path";
|
|
10561
|
+
|
|
10562
|
+
// ../core/dist/host-capture-macos.js
|
|
10287
10563
|
import { spawn as spawn2 } from "child_process";
|
|
10288
10564
|
import * as fs3 from "fs";
|
|
10289
10565
|
import * as os3 from "os";
|
|
10290
|
-
import * as
|
|
10566
|
+
import * as path6 from "path";
|
|
10567
|
+
var MAX_IMAGE_BYTES = 128 * 1024 * 1024;
|
|
10568
|
+
var MAX_OUTPUT_BYTES = 64 * 1024;
|
|
10569
|
+
var STUDIO_BUNDLE_ID = "com.Roblox.RobloxStudio";
|
|
10570
|
+
var MACOS_CAPTURE_SOURCE = String.raw`
|
|
10571
|
+
import Foundation
|
|
10572
|
+
import AppKit
|
|
10573
|
+
import CoreGraphics
|
|
10574
|
+
import ScreenCaptureKit
|
|
10575
|
+
|
|
10576
|
+
struct CaptureFailure: Error { let message: String }
|
|
10577
|
+
func fail(_ message: String) throws -> Never { throw CaptureFailure(message: message) }
|
|
10578
|
+
func emit(_ value: [String: Any]) {
|
|
10579
|
+
if let data = try? JSONSerialization.data(withJSONObject: value), let text = String(data: data, encoding: .utf8) {
|
|
10580
|
+
print(text)
|
|
10581
|
+
}
|
|
10582
|
+
}
|
|
10583
|
+
|
|
10584
|
+
// Local files can use their absolute path in the macOS window title while
|
|
10585
|
+
// Studio reports only the basename as DataModel.Name. Match that basename
|
|
10586
|
+
// after removing Studio's exact title suffix; retain ambiguity checks below.
|
|
10587
|
+
func matchesStudioTitle(_ title: String, hint: String) -> Bool {
|
|
10588
|
+
if hint.isEmpty || title == hint || title.hasPrefix(hint) { return true }
|
|
10589
|
+
let suffix = " - Roblox Studio"
|
|
10590
|
+
guard title.hasSuffix(suffix) else { return false }
|
|
10591
|
+
let place = String(title.dropLast(suffix.count))
|
|
10592
|
+
return place.hasPrefix("/") && (place as NSString).lastPathComponent == hint
|
|
10593
|
+
}
|
|
10594
|
+
|
|
10595
|
+
@available(macOS 14.0, *)
|
|
10596
|
+
@MainActor
|
|
10597
|
+
func capture() async throws {
|
|
10598
|
+
// Never request or bypass Screen Recording permission from an MCP call.
|
|
10599
|
+
guard CGPreflightScreenCaptureAccess() else {
|
|
10600
|
+
try fail("macOS Screen Recording permission is not granted to the MCP host. Enable it in System Settings > Privacy & Security > Screen Recording, then restart the MCP host.")
|
|
10601
|
+
}
|
|
10602
|
+
// A command-line helper must establish its WindowServer connection before
|
|
10603
|
+
// ScreenCaptureKit uses Core Graphics. Do not activate or create any UI.
|
|
10604
|
+
NSApplication.shared.setActivationPolicy(.prohibited)
|
|
10605
|
+
let env = ProcessInfo.processInfo.environment
|
|
10606
|
+
let hint = env["MCP_CAPTURE_TITLE_HINT"] ?? ""
|
|
10607
|
+
let content = try await SCShareableContent.excludingDesktopWindows(true, onScreenWindowsOnly: true)
|
|
10608
|
+
let candidates = content.windows.filter { window in
|
|
10609
|
+
window.owningApplication?.bundleIdentifier == "com.Roblox.RobloxStudio" &&
|
|
10610
|
+
window.windowLayer == 0 && window.isOnScreen &&
|
|
10611
|
+
window.frame.width > 0 && window.frame.height > 0 &&
|
|
10612
|
+
matchesStudioTitle(window.title ?? "", hint: hint)
|
|
10613
|
+
}
|
|
10614
|
+
guard candidates.count == 1, let window = candidates.first, let app = window.owningApplication else {
|
|
10615
|
+
try fail(candidates.isEmpty ? "no visible Roblox Studio window matches the requested place title" : "multiple Roblox Studio windows match the requested place title; the capture is ambiguous")
|
|
10616
|
+
}
|
|
10617
|
+
if let expected = env["MCP_CAPTURE_EXPECTED_IDENTITY"], !expected.isEmpty {
|
|
10618
|
+
guard let data = expected.data(using: .utf8),
|
|
10619
|
+
let identity = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
10620
|
+
(identity["windowId"] as? NSNumber)?.uint32Value == window.windowID,
|
|
10621
|
+
(identity["processId"] as? NSNumber)?.int32Value == app.processID,
|
|
10622
|
+
identity["bundleIdentifier"] as? String == app.bundleIdentifier else {
|
|
10623
|
+
try fail("the selected Roblox Studio window identity changed between captures")
|
|
10624
|
+
}
|
|
10625
|
+
}
|
|
10626
|
+
let filter = SCContentFilter(desktopIndependentWindow: window)
|
|
10627
|
+
let scale = CGFloat(filter.pointPixelScale)
|
|
10628
|
+
let pixelWidth = filter.contentRect.width * scale
|
|
10629
|
+
let pixelHeight = filter.contentRect.height * scale
|
|
10630
|
+
guard pixelWidth.isFinite, pixelHeight.isFinite, pixelWidth > 0, pixelHeight > 0,
|
|
10631
|
+
pixelWidth <= 16384, pixelHeight <= 16384 else { try fail("invalid Studio window capture dimensions") }
|
|
10632
|
+
let width = Int(pixelWidth.rounded(.up))
|
|
10633
|
+
let height = Int(pixelHeight.rounded(.up))
|
|
10634
|
+
guard width * height * 4 <= 134217728 else { try fail("Studio window capture exceeds the image byte limit") }
|
|
10635
|
+
let config = SCStreamConfiguration()
|
|
10636
|
+
config.width = width
|
|
10637
|
+
config.height = height
|
|
10638
|
+
config.showsCursor = false
|
|
10639
|
+
config.ignoreShadowsSingleWindow = true
|
|
10640
|
+
config.ignoreGlobalClipSingleWindow = true
|
|
10641
|
+
let image = try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: config)
|
|
10642
|
+
guard image.width == width, image.height == height else { try fail("Studio window capture returned unexpected dimensions") }
|
|
10643
|
+
var bytes = [UInt8](repeating: 0, count: width * height * 4)
|
|
10644
|
+
try bytes.withUnsafeMutableBytes { raw in
|
|
10645
|
+
guard let context = CGContext(data: raw.baseAddress, width: width, height: height,
|
|
10646
|
+
bitsPerComponent: 8, bytesPerRow: width * 4, space: CGColorSpace(name: CGColorSpace.sRGB)!,
|
|
10647
|
+
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue) else {
|
|
10648
|
+
try fail("could not allocate the RGBA capture context")
|
|
10649
|
+
}
|
|
10650
|
+
// Quartz draws the image upright into row-major top-to-bottom bitmap storage.
|
|
10651
|
+
context.setBlendMode(.copy)
|
|
10652
|
+
context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
|
|
10653
|
+
}
|
|
10654
|
+
guard let output = env["MCP_CAPTURE_OUT"] else { try fail("missing capture output path") }
|
|
10655
|
+
try Data(bytes).write(to: URL(fileURLWithPath: output), options: .atomic)
|
|
10656
|
+
emit(["ok": true, "width": width, "height": height, "title": String((window.title ?? "").prefix(2048)),
|
|
10657
|
+
"identity": ["windowId": window.windowID, "processId": app.processID, "bundleIdentifier": app.bundleIdentifier]])
|
|
10658
|
+
}
|
|
10659
|
+
|
|
10660
|
+
@main struct Main {
|
|
10661
|
+
@MainActor
|
|
10662
|
+
static func main() async {
|
|
10663
|
+
do {
|
|
10664
|
+
if #available(macOS 14.0, *) { try await capture() }
|
|
10665
|
+
else { try fail("macOS host window capture requires macOS 14 or newer") }
|
|
10666
|
+
} catch {
|
|
10667
|
+
emit(["ok": false, "error": String(((error as? CaptureFailure)?.message ?? error.localizedDescription).prefix(2048))])
|
|
10668
|
+
}
|
|
10669
|
+
}
|
|
10670
|
+
}
|
|
10671
|
+
`;
|
|
10672
|
+
function runBoundedCaptureProcess(executable, args, timeoutMs, env = process.env) {
|
|
10673
|
+
return new Promise((resolve6, reject) => {
|
|
10674
|
+
const processGroup = process.platform !== "win32";
|
|
10675
|
+
const child = spawn2(executable, args, { env, stdio: ["ignore", "pipe", "pipe"], detached: processGroup });
|
|
10676
|
+
let stdout = "";
|
|
10677
|
+
let stderr = "";
|
|
10678
|
+
let outputBytes = 0;
|
|
10679
|
+
let failure;
|
|
10680
|
+
const stop = (message) => {
|
|
10681
|
+
failure ??= new Error(message);
|
|
10682
|
+
try {
|
|
10683
|
+
if (processGroup && child.pid)
|
|
10684
|
+
process.kill(-child.pid, "SIGKILL");
|
|
10685
|
+
else
|
|
10686
|
+
child.kill("SIGKILL");
|
|
10687
|
+
} catch {
|
|
10688
|
+
child.kill("SIGKILL");
|
|
10689
|
+
}
|
|
10690
|
+
};
|
|
10691
|
+
const timer = setTimeout(() => stop(`macOS host capture helper timed out after ${timeoutMs}ms`), timeoutMs);
|
|
10692
|
+
child.stdout.setEncoding("utf8");
|
|
10693
|
+
child.stderr.setEncoding("utf8");
|
|
10694
|
+
const collect = (chunk, isError) => {
|
|
10695
|
+
outputBytes += Buffer.byteLength(chunk);
|
|
10696
|
+
if (outputBytes > MAX_OUTPUT_BYTES) {
|
|
10697
|
+
stop("macOS host capture helper exceeded its output byte limit");
|
|
10698
|
+
return;
|
|
10699
|
+
}
|
|
10700
|
+
if (isError)
|
|
10701
|
+
stderr += chunk;
|
|
10702
|
+
else
|
|
10703
|
+
stdout += chunk;
|
|
10704
|
+
};
|
|
10705
|
+
child.stdout.on("data", (chunk) => collect(chunk, false));
|
|
10706
|
+
child.stderr.on("data", (chunk) => collect(chunk, true));
|
|
10707
|
+
child.on("error", (error) => {
|
|
10708
|
+
failure ??= error;
|
|
10709
|
+
});
|
|
10710
|
+
child.on("close", (code) => {
|
|
10711
|
+
clearTimeout(timer);
|
|
10712
|
+
if (failure)
|
|
10713
|
+
reject(failure);
|
|
10714
|
+
else if (code !== 0)
|
|
10715
|
+
reject(new Error(`macOS host capture helper exited with code ${code}: ${stderr.trim().slice(0, 600)}`));
|
|
10716
|
+
else
|
|
10717
|
+
resolve6(stdout);
|
|
10718
|
+
});
|
|
10719
|
+
});
|
|
10720
|
+
}
|
|
10721
|
+
var MISSING_SWIFT_TOOLCHAIN = "macOS host capture requires Xcode or the Xcode Command Line Tools (xcrun swiftc). Install them with `xcode-select --install`, then retry the capture.";
|
|
10722
|
+
async function assertSwiftToolchain(env = process.env, selectedDeveloperDir = () => runBoundedCaptureProcess("/usr/bin/xcode-select", ["-p"], 5e3, env), exists = fs3.existsSync) {
|
|
10723
|
+
let developerDir = env.DEVELOPER_DIR?.trim();
|
|
10724
|
+
if (!developerDir) {
|
|
10725
|
+
try {
|
|
10726
|
+
developerDir = (await selectedDeveloperDir()).trim();
|
|
10727
|
+
} catch {
|
|
10728
|
+
throw new Error(MISSING_SWIFT_TOOLCHAIN);
|
|
10729
|
+
}
|
|
10730
|
+
}
|
|
10731
|
+
const compilers = [
|
|
10732
|
+
path6.join(developerDir, "usr", "bin", "swiftc"),
|
|
10733
|
+
path6.join(developerDir, "Toolchains", "XcodeDefault.xctoolchain", "usr", "bin", "swiftc")
|
|
10734
|
+
];
|
|
10735
|
+
if (!path6.isAbsolute(developerDir) || !compilers.some((compiler) => exists(compiler))) {
|
|
10736
|
+
throw new Error(MISSING_SWIFT_TOOLCHAIN);
|
|
10737
|
+
}
|
|
10738
|
+
}
|
|
10739
|
+
var compiledHelper;
|
|
10740
|
+
async function getHelper() {
|
|
10741
|
+
if (!compiledHelper) {
|
|
10742
|
+
compiledHelper = (async () => {
|
|
10743
|
+
await assertSwiftToolchain();
|
|
10744
|
+
const directory = fs3.mkdtempSync(path6.join(os3.tmpdir(), "robloxstudio-mcp-macos-helper-"));
|
|
10745
|
+
const cleanup = () => fs3.rmSync(directory, { recursive: true, force: true });
|
|
10746
|
+
try {
|
|
10747
|
+
const source = path6.join(directory, "capture.swift");
|
|
10748
|
+
const executable = path6.join(directory, "capture");
|
|
10749
|
+
fs3.writeFileSync(source, MACOS_CAPTURE_SOURCE, { mode: 384 });
|
|
10750
|
+
await runBoundedCaptureProcess("/usr/bin/xcrun", [
|
|
10751
|
+
"swiftc",
|
|
10752
|
+
"-parse-as-library",
|
|
10753
|
+
source,
|
|
10754
|
+
"-o",
|
|
10755
|
+
executable,
|
|
10756
|
+
"-module-cache-path",
|
|
10757
|
+
path6.join(directory, "modules")
|
|
10758
|
+
], 6e4);
|
|
10759
|
+
process.once("exit", cleanup);
|
|
10760
|
+
return executable;
|
|
10761
|
+
} catch (error) {
|
|
10762
|
+
cleanup();
|
|
10763
|
+
throw error;
|
|
10764
|
+
}
|
|
10765
|
+
})();
|
|
10766
|
+
compiledHelper.catch(() => {
|
|
10767
|
+
compiledHelper = void 0;
|
|
10768
|
+
});
|
|
10769
|
+
}
|
|
10770
|
+
return compiledHelper;
|
|
10771
|
+
}
|
|
10772
|
+
async function prepareMacHostCapture() {
|
|
10773
|
+
await getHelper();
|
|
10774
|
+
}
|
|
10775
|
+
function validIdentity(value) {
|
|
10776
|
+
if (!value || typeof value !== "object")
|
|
10777
|
+
return false;
|
|
10778
|
+
const id = value;
|
|
10779
|
+
return Number.isSafeInteger(id.windowId) && id.windowId > 0 && id.windowId <= 4294967295 && Number.isSafeInteger(id.processId) && id.processId > 0 && id.processId <= 2147483647 && id.bundleIdentifier === STUDIO_BUNDLE_ID;
|
|
10780
|
+
}
|
|
10781
|
+
function parseMacCaptureReport(stdout, expectedIdentity) {
|
|
10782
|
+
if (Buffer.byteLength(stdout) > MAX_OUTPUT_BYTES)
|
|
10783
|
+
throw new Error("macOS capture report exceeds its byte limit");
|
|
10784
|
+
const value = JSON.parse(stdout.trim());
|
|
10785
|
+
if (!value || typeof value !== "object")
|
|
10786
|
+
throw new Error("invalid macOS capture report");
|
|
10787
|
+
if (value.ok === false && typeof value.error === "string")
|
|
10788
|
+
return { ok: false, error: value.error.slice(0, 2048) };
|
|
10789
|
+
const { width, height, title, identity } = value;
|
|
10790
|
+
if (value.ok !== true || !Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0 || width > 16384 || height > 16384 || width * height * 4 > MAX_IMAGE_BYTES || typeof title !== "string" || title.length > 2048 || !validIdentity(identity)) {
|
|
10791
|
+
throw new Error("invalid macOS capture dimensions or window identity");
|
|
10792
|
+
}
|
|
10793
|
+
if (expectedIdentity && (identity.windowId !== expectedIdentity.windowId || identity.processId !== expectedIdentity.processId || identity.bundleIdentifier !== expectedIdentity.bundleIdentifier)) {
|
|
10794
|
+
throw new Error("the selected Roblox Studio window identity changed between captures");
|
|
10795
|
+
}
|
|
10796
|
+
return { ok: true, width, height, title, identity };
|
|
10797
|
+
}
|
|
10798
|
+
async function runMacCapture(output, titleHint, expectedIdentity) {
|
|
10799
|
+
const executable = await getHelper();
|
|
10800
|
+
return runBoundedCaptureProcess(executable, [], 2e4, {
|
|
10801
|
+
...process.env,
|
|
10802
|
+
MCP_CAPTURE_TITLE_HINT: titleHint ?? "",
|
|
10803
|
+
MCP_CAPTURE_OUT: output,
|
|
10804
|
+
MCP_CAPTURE_EXPECTED_IDENTITY: expectedIdentity ? JSON.stringify(expectedIdentity) : ""
|
|
10805
|
+
});
|
|
10806
|
+
}
|
|
10807
|
+
async function captureMacStudioWindow(titleHint, expectedIdentity, runCapture = runMacCapture) {
|
|
10808
|
+
const directory = fs3.mkdtempSync(path6.join(os3.tmpdir(), "robloxstudio-mcp-macos-capture-"));
|
|
10809
|
+
try {
|
|
10810
|
+
const output = path6.join(directory, "capture.rgba");
|
|
10811
|
+
const stdout = await runCapture(output, titleHint, expectedIdentity);
|
|
10812
|
+
const report = parseMacCaptureReport(stdout, expectedIdentity);
|
|
10813
|
+
if (!report.ok)
|
|
10814
|
+
return report;
|
|
10815
|
+
const expectedBytes = report.width * report.height * 4;
|
|
10816
|
+
const stat2 = fs3.lstatSync(output);
|
|
10817
|
+
if (!stat2.isFile() || stat2.size !== expectedBytes)
|
|
10818
|
+
throw new Error("macOS capture returned an invalid RGBA byte count");
|
|
10819
|
+
const rgba = fs3.readFileSync(output);
|
|
10820
|
+
if (rgba.length !== expectedBytes)
|
|
10821
|
+
throw new Error("macOS capture RGBA byte count changed during read");
|
|
10822
|
+
return { ok: true, capture: { width: report.width, height: report.height, title: report.title, identity: report.identity, rgba } };
|
|
10823
|
+
} catch (error) {
|
|
10824
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
10825
|
+
} finally {
|
|
10826
|
+
fs3.rmSync(directory, { recursive: true, force: true });
|
|
10827
|
+
}
|
|
10828
|
+
}
|
|
10829
|
+
|
|
10830
|
+
// ../core/dist/host-capture.js
|
|
10291
10831
|
var HOST_CAPTURE_TIMEOUT_MS = 2e4;
|
|
10292
10832
|
var MARKER_MIN_RB = 200;
|
|
10293
10833
|
var MARKER_MAX_G = 60;
|
|
@@ -10297,10 +10837,14 @@ function isHostCaptureDisabled(env = process.env) {
|
|
|
10297
10837
|
return value === "0" || value === "false" || value === "off";
|
|
10298
10838
|
}
|
|
10299
10839
|
function isHostCaptureSupported(platform = process.platform) {
|
|
10300
|
-
return platform === "win32";
|
|
10840
|
+
return platform === "win32" || platform === "darwin";
|
|
10301
10841
|
}
|
|
10302
10842
|
function hostCaptureUnsupportedReason(platform = process.platform) {
|
|
10303
|
-
return `host window capture is only implemented on Windows (this is ${platform})`;
|
|
10843
|
+
return `host window capture is only implemented on Windows and macOS (this is ${platform})`;
|
|
10844
|
+
}
|
|
10845
|
+
async function prepareHostWindowCapture() {
|
|
10846
|
+
if (process.platform === "darwin" && !isHostCaptureDisabled())
|
|
10847
|
+
await prepareMacHostCapture();
|
|
10304
10848
|
}
|
|
10305
10849
|
function isUniformFrame(rgba, width, height) {
|
|
10306
10850
|
const pixels = width * height;
|
|
@@ -10450,17 +10994,40 @@ public static class McpStudioCapture {
|
|
|
10450
10994
|
}
|
|
10451
10995
|
"@
|
|
10452
10996
|
function Emit($obj) { Write-Output ($obj | ConvertTo-Json -Compress) }
|
|
10997
|
+
# Local files show their absolute path in the window title while Studio reports
|
|
10998
|
+
# only the basename as DataModel.Name. Match that basename after removing
|
|
10999
|
+
# Studio's exact title suffix; the ambiguity checks below still apply.
|
|
11000
|
+
function Test-StudioTitle([string]$title, [string]$hint) {
|
|
11001
|
+
if (-not $hint -or $title.StartsWith($hint, [StringComparison]::Ordinal)) { return $true }
|
|
11002
|
+
$suffix = ' - Roblox Studio'
|
|
11003
|
+
if (-not $title.EndsWith($suffix, [StringComparison]::Ordinal)) { return $false }
|
|
11004
|
+
$place = $title.Substring(0, $title.Length - $suffix.Length)
|
|
11005
|
+
if ($place -cnotmatch '^(?:[A-Za-z]:[\\/]|\\\\|/)') { return $false }
|
|
11006
|
+
return $place.Substring($place.LastIndexOfAny([char[]]@([char]'\', [char]'/')) + 1) -ceq $hint
|
|
11007
|
+
}
|
|
10453
11008
|
$hint = $env:MCP_CAPTURE_TITLE_HINT
|
|
10454
11009
|
$outFile = $env:MCP_CAPTURE_OUT
|
|
10455
11010
|
$candidates = [McpStudioCapture]::Find('RobloxStudioBeta')
|
|
10456
11011
|
if ($candidates.Count -eq 0) { Emit @{ ok = $false; error = 'no visible Roblox Studio window was found' }; exit 0 }
|
|
10457
|
-
$
|
|
10458
|
-
|
|
10459
|
-
if ($
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
|
|
11012
|
+
$matching = @($candidates | Where-Object { Test-StudioTitle $_.Title $hint })
|
|
11013
|
+
$expectedJson = $env:MCP_CAPTURE_EXPECTED_IDENTITY
|
|
11014
|
+
if ($expectedJson) {
|
|
11015
|
+
try { $expected = $expectedJson | ConvertFrom-Json -ErrorAction Stop }
|
|
11016
|
+
catch { Emit @{ ok = $false; error = 'invalid expected Studio window identity' }; exit 0 }
|
|
11017
|
+
$matching = @($matching | Where-Object {
|
|
11018
|
+
$_.Handle.ToInt64() -eq $expected.windowId -and $_.Pid -eq $expected.processId -and
|
|
11019
|
+
$expected.bundleIdentifier -ceq 'RobloxStudioBeta'
|
|
11020
|
+
})
|
|
11021
|
+
if ($matching.Count -ne 1) {
|
|
11022
|
+
Emit @{ ok = $false; error = 'the selected Roblox Studio window identity changed between captures' }; exit 0
|
|
11023
|
+
}
|
|
11024
|
+
} elseif ($matching.Count -ne 1) {
|
|
11025
|
+
$reason = if ($matching.Count -eq 0) { 'no visible Roblox Studio window matches the requested place title' }
|
|
11026
|
+
else { 'multiple Roblox Studio windows match the requested place title; the capture is ambiguous' }
|
|
11027
|
+
Emit @{ ok = $false; error = $reason }; exit 0
|
|
10463
11028
|
}
|
|
11029
|
+
$pick = $matching[0]
|
|
11030
|
+
$identity = @{ windowId = $pick.Handle.ToInt64(); processId = [long]$pick.Pid; bundleIdentifier = 'RobloxStudioBeta' }
|
|
10464
11031
|
if ([McpStudioCapture]::IsIconic($pick.Handle)) {
|
|
10465
11032
|
Emit @{ ok = $false; error = "the Studio window '$($pick.Title)' is minimized; restore it (it may stay behind other windows)" }; exit 0
|
|
10466
11033
|
}
|
|
@@ -10482,83 +11049,110 @@ $bytes = New-Object byte[] ($data.Stride * $h)
|
|
|
10482
11049
|
$stride = $data.Stride
|
|
10483
11050
|
$bmp.UnlockBits($data); $bmp.Dispose()
|
|
10484
11051
|
[System.IO.File]::WriteAllBytes($outFile, $bytes)
|
|
10485
|
-
Emit @{ ok = $true; width = $w; height = $h; stride = $stride; title = $pick.Title }
|
|
11052
|
+
Emit @{ ok = $true; width = $w; height = $h; stride = $stride; title = $pick.Title; identity = $identity }
|
|
10486
11053
|
`;
|
|
11054
|
+
function validWindowsIdentity(value) {
|
|
11055
|
+
return value !== null && typeof value === "object" && "windowId" in value && typeof value.windowId === "number" && Number.isSafeInteger(value.windowId) && value.windowId > 0 && "processId" in value && typeof value.processId === "number" && Number.isSafeInteger(value.processId) && value.processId > 0 && value.processId <= 4294967295 && "bundleIdentifier" in value && value.bundleIdentifier === "RobloxStudioBeta";
|
|
11056
|
+
}
|
|
11057
|
+
function parseWindowsCaptureReport(value, expectedIdentity) {
|
|
11058
|
+
if (value === null || typeof value !== "object" || !("ok" in value))
|
|
11059
|
+
throw new Error("invalid Windows capture report");
|
|
11060
|
+
if (value.ok === false && "error" in value && typeof value.error === "string")
|
|
11061
|
+
return { ok: false, error: value.error };
|
|
11062
|
+
if (!("identity" in value) || !validWindowsIdentity(value.identity))
|
|
11063
|
+
throw new Error("invalid Windows capture window identity");
|
|
11064
|
+
const identity = value.identity;
|
|
11065
|
+
if (expectedIdentity && (identity.windowId !== expectedIdentity.windowId || identity.processId !== expectedIdentity.processId || identity.bundleIdentifier !== expectedIdentity.bundleIdentifier)) {
|
|
11066
|
+
throw new Error("the selected Roblox Studio window identity changed between captures");
|
|
11067
|
+
}
|
|
11068
|
+
if (value.ok !== true || !("width" in value) || typeof value.width !== "number" || !Number.isSafeInteger(value.width) || value.width <= 0 || !("height" in value) || typeof value.height !== "number" || !Number.isSafeInteger(value.height) || value.height <= 0 || !("stride" in value) || typeof value.stride !== "number" || !Number.isSafeInteger(value.stride) || value.stride < value.width * 4 || value.width > 16384 || value.height > 16384 || value.stride * value.height > 128 * 1024 * 1024 || !("title" in value) || typeof value.title !== "string") {
|
|
11069
|
+
throw new Error("invalid Windows capture dimensions or title");
|
|
11070
|
+
}
|
|
11071
|
+
return { ok: true, width: value.width, height: value.height, stride: value.stride, title: value.title, identity };
|
|
11072
|
+
}
|
|
10487
11073
|
function powershellPath() {
|
|
10488
11074
|
const systemRoot = process.env.SystemRoot ?? process.env.windir;
|
|
10489
11075
|
if (systemRoot) {
|
|
10490
|
-
const candidate =
|
|
10491
|
-
if (
|
|
11076
|
+
const candidate = path7.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
11077
|
+
if (fs4.existsSync(candidate))
|
|
10492
11078
|
return candidate;
|
|
10493
11079
|
}
|
|
10494
11080
|
return "powershell.exe";
|
|
10495
11081
|
}
|
|
10496
|
-
function runWindowsCapture(titleHint, outFile) {
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10500
|
-
|
|
10501
|
-
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10508
|
-
|
|
10509
|
-
|
|
10510
|
-
|
|
10511
|
-
|
|
10512
|
-
|
|
10513
|
-
|
|
10514
|
-
|
|
10515
|
-
|
|
10516
|
-
child.
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
|
|
10520
|
-
|
|
10521
|
-
|
|
10522
|
-
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
11082
|
+
function runWindowsCapture(titleHint, outFile, expectedIdentity) {
|
|
11083
|
+
const { promise, resolve: resolve6, reject } = Promise.withResolvers();
|
|
11084
|
+
const encoded = Buffer.from(WINDOWS_CAPTURE_SCRIPT, "utf16le").toString("base64");
|
|
11085
|
+
const child = spawn3(powershellPath(), ["-NoProfile", "-NonInteractive", "-NoLogo", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded], {
|
|
11086
|
+
env: {
|
|
11087
|
+
...process.env,
|
|
11088
|
+
MCP_CAPTURE_TITLE_HINT: titleHint ?? "",
|
|
11089
|
+
MCP_CAPTURE_OUT: outFile,
|
|
11090
|
+
MCP_CAPTURE_EXPECTED_IDENTITY: expectedIdentity ? JSON.stringify(expectedIdentity) : ""
|
|
11091
|
+
},
|
|
11092
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
11093
|
+
windowsHide: true
|
|
11094
|
+
});
|
|
11095
|
+
let stdout = "";
|
|
11096
|
+
let stderr = "";
|
|
11097
|
+
let settled = false;
|
|
11098
|
+
const timer = setTimeout(() => {
|
|
11099
|
+
if (settled)
|
|
11100
|
+
return;
|
|
11101
|
+
settled = true;
|
|
11102
|
+
child.kill();
|
|
11103
|
+
reject(new Error(`host window capture timed out after ${HOST_CAPTURE_TIMEOUT_MS}ms`));
|
|
11104
|
+
}, HOST_CAPTURE_TIMEOUT_MS);
|
|
11105
|
+
child.stdout.setEncoding("utf8");
|
|
11106
|
+
child.stderr.setEncoding("utf8");
|
|
11107
|
+
child.stdout.on("data", (chunk) => {
|
|
11108
|
+
stdout += chunk;
|
|
11109
|
+
});
|
|
11110
|
+
child.stderr.on("data", (chunk) => {
|
|
11111
|
+
stderr += chunk;
|
|
11112
|
+
});
|
|
11113
|
+
child.on("error", (error) => {
|
|
11114
|
+
if (settled)
|
|
11115
|
+
return;
|
|
11116
|
+
settled = true;
|
|
11117
|
+
clearTimeout(timer);
|
|
11118
|
+
reject(new Error(`could not start PowerShell for host window capture: ${error.message}`));
|
|
11119
|
+
});
|
|
11120
|
+
child.on("close", (code) => {
|
|
11121
|
+
if (settled)
|
|
11122
|
+
return;
|
|
11123
|
+
settled = true;
|
|
11124
|
+
clearTimeout(timer);
|
|
11125
|
+
const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
11126
|
+
const last = lines[lines.length - 1];
|
|
11127
|
+
if (last && last.startsWith("{")) {
|
|
11128
|
+
try {
|
|
11129
|
+
resolve6(JSON.parse(last));
|
|
10531
11130
|
return;
|
|
10532
|
-
|
|
10533
|
-
clearTimeout(timer);
|
|
10534
|
-
const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
10535
|
-
const last = lines[lines.length - 1];
|
|
10536
|
-
if (last && last.startsWith("{")) {
|
|
10537
|
-
try {
|
|
10538
|
-
resolve5(JSON.parse(last));
|
|
10539
|
-
return;
|
|
10540
|
-
} catch {
|
|
10541
|
-
}
|
|
11131
|
+
} catch {
|
|
10542
11132
|
}
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
});
|
|
11133
|
+
}
|
|
11134
|
+
const detail = (stderr.trim() || stdout.trim()).slice(0, 600);
|
|
11135
|
+
reject(new Error(`host window capture helper exited with code ${code}${detail ? `: ${detail}` : ""}`));
|
|
10546
11136
|
});
|
|
11137
|
+
return promise;
|
|
10547
11138
|
}
|
|
10548
|
-
async function captureStudioWindow(titleHint) {
|
|
11139
|
+
async function captureStudioWindow(titleHint, expectedIdentity) {
|
|
10549
11140
|
if (isHostCaptureDisabled()) {
|
|
10550
11141
|
return { ok: false, error: "host window capture is disabled by ROBLOX_STUDIO_HOST_CAPTURE" };
|
|
10551
11142
|
}
|
|
10552
11143
|
if (!isHostCaptureSupported()) {
|
|
10553
11144
|
return { ok: false, error: hostCaptureUnsupportedReason() };
|
|
10554
11145
|
}
|
|
10555
|
-
|
|
11146
|
+
if (process.platform === "darwin")
|
|
11147
|
+
return captureMacStudioWindow(titleHint, expectedIdentity);
|
|
11148
|
+
const outFile = path7.join(os4.tmpdir(), `robloxstudio-mcp-capture-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.bgra`);
|
|
10556
11149
|
try {
|
|
10557
|
-
const report = await runWindowsCapture(titleHint, outFile);
|
|
10558
|
-
if (!report.ok
|
|
10559
|
-
return
|
|
10560
|
-
|
|
10561
|
-
|
|
11150
|
+
const report = parseWindowsCaptureReport(await runWindowsCapture(titleHint, outFile, expectedIdentity), expectedIdentity);
|
|
11151
|
+
if (!report.ok)
|
|
11152
|
+
return report;
|
|
11153
|
+
const raw = fs4.readFileSync(outFile);
|
|
11154
|
+
if (raw.length !== report.stride * report.height)
|
|
11155
|
+
throw new Error("Windows capture returned an invalid BGRA byte count");
|
|
10562
11156
|
const { width, height, stride } = report;
|
|
10563
11157
|
const rgba = Buffer.alloc(width * height * 4);
|
|
10564
11158
|
for (let y = 0; y < height; y++) {
|
|
@@ -10573,17 +11167,20 @@ async function captureStudioWindow(titleHint) {
|
|
|
10573
11167
|
rgba[d + 3] = 255;
|
|
10574
11168
|
}
|
|
10575
11169
|
}
|
|
10576
|
-
return { ok: true, capture: { width, height, rgba, title: report.title
|
|
11170
|
+
return { ok: true, capture: { width, height, rgba, title: report.title, identity: report.identity } };
|
|
10577
11171
|
} catch (error) {
|
|
10578
11172
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
10579
11173
|
} finally {
|
|
10580
|
-
|
|
11174
|
+
fs4.rmSync(outFile, { force: true });
|
|
10581
11175
|
}
|
|
10582
11176
|
}
|
|
10583
11177
|
|
|
10584
11178
|
// ../core/dist/tools/index.js
|
|
10585
|
-
import * as
|
|
10586
|
-
import * as
|
|
11179
|
+
import * as fs5 from "fs";
|
|
11180
|
+
import * as path8 from "path";
|
|
11181
|
+
function sameHostWindowIdentity(actual, expected) {
|
|
11182
|
+
return actual?.windowId === expected.windowId && actual.processId === expected.processId && actual.bundleIdentifier === expected.bundleIdentifier;
|
|
11183
|
+
}
|
|
10587
11184
|
var HOST_VIEWPORT_RECT_TTL_MS = 6e4;
|
|
10588
11185
|
var MAX_INLINE_IMAGE_BYTES = 6e6;
|
|
10589
11186
|
var MAX_MATRIX_IMAGE_BYTES = 8e6;
|
|
@@ -10669,7 +11266,7 @@ function encodeImageFromRgbaResponse(response, format, quality) {
|
|
|
10669
11266
|
};
|
|
10670
11267
|
}
|
|
10671
11268
|
function sleep(ms) {
|
|
10672
|
-
return new Promise((
|
|
11269
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
10673
11270
|
}
|
|
10674
11271
|
function errorMessage(error) {
|
|
10675
11272
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -10763,9 +11360,9 @@ function compactSoundReference(sound) {
|
|
|
10763
11360
|
name: stringField(sound, "name"),
|
|
10764
11361
|
className: stringField(sound, "className")
|
|
10765
11362
|
};
|
|
10766
|
-
const
|
|
10767
|
-
if (
|
|
10768
|
-
compact.path =
|
|
11363
|
+
const path9 = stringField(sound, "path");
|
|
11364
|
+
if (path9)
|
|
11365
|
+
compact.path = path9;
|
|
10769
11366
|
const assetId = robloxAssetIdFromContentId(sound.assetId ?? sound.soundId ?? sound.asset);
|
|
10770
11367
|
if (assetId !== void 0)
|
|
10771
11368
|
compact.assetId = assetId;
|
|
@@ -10828,8 +11425,8 @@ function loadMicroProfilerBaseline(source, sourcePath) {
|
|
|
10828
11425
|
if (typeof sourcePath !== "string" || sourcePath === "") {
|
|
10829
11426
|
throw new Error("baseline_path must be a non-empty string when provided");
|
|
10830
11427
|
}
|
|
10831
|
-
const resolved =
|
|
10832
|
-
const parsed = JSON.parse(
|
|
11428
|
+
const resolved = path8.resolve(sourcePath);
|
|
11429
|
+
const parsed = JSON.parse(fs5.readFileSync(resolved, "utf8"));
|
|
10833
11430
|
const record = asRecord(parsed);
|
|
10834
11431
|
if (!record)
|
|
10835
11432
|
throw new Error(`baseline_path did not contain a JSON object: ${resolved}`);
|
|
@@ -11331,6 +11928,7 @@ var RobloxStudioTools = class {
|
|
|
11331
11928
|
cookieClient;
|
|
11332
11929
|
instanceManager;
|
|
11333
11930
|
managedConnectionAssociations = Promise.resolve();
|
|
11931
|
+
unsubscribePeerRegistered;
|
|
11334
11932
|
hostWindowCapture = captureStudioWindow;
|
|
11335
11933
|
hostViewportRects = /* @__PURE__ */ new Map();
|
|
11336
11934
|
viewportCaptureQueues = /* @__PURE__ */ new Map();
|
|
@@ -11340,7 +11938,7 @@ var RobloxStudioTools = class {
|
|
|
11340
11938
|
this.openCloudClient = new OpenCloudClient();
|
|
11341
11939
|
this.cookieClient = new RobloxCookieClient();
|
|
11342
11940
|
this.instanceManager = new StudioInstanceManager();
|
|
11343
|
-
this.bridge.onPeerRegistered((peer) => {
|
|
11941
|
+
this.unsubscribePeerRegistered = this.bridge.onPeerRegistered((peer) => {
|
|
11344
11942
|
const instanceManager = this.instanceManager;
|
|
11345
11943
|
const association = this.managedConnectionAssociations.then(() => this._associateManagedEditConnection(peer, instanceManager));
|
|
11346
11944
|
this.managedConnectionAssociations = association.catch((error) => {
|
|
@@ -11351,6 +11949,10 @@ var RobloxStudioTools = class {
|
|
|
11351
11949
|
getStudioLifecycleCapabilities() {
|
|
11352
11950
|
return this.instanceManager.getLifecycleCapabilities();
|
|
11353
11951
|
}
|
|
11952
|
+
async dispose() {
|
|
11953
|
+
this.unsubscribePeerRegistered();
|
|
11954
|
+
await this.managedConnectionAssociations;
|
|
11955
|
+
}
|
|
11354
11956
|
_textResult(body) {
|
|
11355
11957
|
return { content: [{ type: "text", text: JSON.stringify(body) }] };
|
|
11356
11958
|
}
|
|
@@ -11474,8 +12076,8 @@ var RobloxStudioTools = class {
|
|
|
11474
12076
|
}
|
|
11475
12077
|
return this._requestPeer(endpoint, data, resolved.targetPeerId, timeoutMs, signal, operationId);
|
|
11476
12078
|
}
|
|
11477
|
-
//
|
|
11478
|
-
//
|
|
12079
|
+
// Honor an explicitly selected client for viewport/input operations; otherwise
|
|
12080
|
+
// prefer the first client in the scope, then retain the default Peer's Instance.
|
|
11479
12081
|
_resolveRuntime(instance_id) {
|
|
11480
12082
|
const resolved = this.bridge.resolveTarget({ instance_id, target: void 0 });
|
|
11481
12083
|
if (!resolved.ok)
|
|
@@ -11487,7 +12089,9 @@ var RobloxStudioTools = class {
|
|
|
11487
12089
|
data: this._routingErrorData()
|
|
11488
12090
|
});
|
|
11489
12091
|
}
|
|
11490
|
-
const
|
|
12092
|
+
const clients = this.bridge.getPeersInScope(resolved.targetInstanceId).filter((peer) => /^client-\d+$/.test(peer.role));
|
|
12093
|
+
const selectedClients = clients.filter((peer) => instance_id === peer.instanceId || instance_id === `${peer.instanceId}-${peer.role}`);
|
|
12094
|
+
const client = selectedClients.length === 1 ? selectedClients[0] : clients.sort((a, b) => a.role.localeCompare(b.role) || a.peerId.localeCompare(b.peerId))[0];
|
|
11491
12095
|
return {
|
|
11492
12096
|
instanceId: client?.instanceId ?? resolved.targetInstanceId,
|
|
11493
12097
|
clientRole: client?.role
|
|
@@ -11773,8 +12377,8 @@ var RobloxStudioTools = class {
|
|
|
11773
12377
|
timedOut: true
|
|
11774
12378
|
};
|
|
11775
12379
|
}
|
|
11776
|
-
async getFileTree(
|
|
11777
|
-
const response = await this._callSingle("/api/file-tree", { path:
|
|
12380
|
+
async getFileTree(path9 = "", instance_id) {
|
|
12381
|
+
const response = await this._callSingle("/api/file-tree", { path: path9 }, void 0, instance_id);
|
|
11778
12382
|
return {
|
|
11779
12383
|
content: [
|
|
11780
12384
|
{
|
|
@@ -11866,9 +12470,9 @@ var RobloxStudioTools = class {
|
|
|
11866
12470
|
]
|
|
11867
12471
|
};
|
|
11868
12472
|
}
|
|
11869
|
-
async getProjectStructure(
|
|
12473
|
+
async getProjectStructure(path9, maxDepth, scriptsOnly, instance_id) {
|
|
11870
12474
|
const response = await this._callSingle("/api/project-structure", {
|
|
11871
|
-
path:
|
|
12475
|
+
path: path9,
|
|
11872
12476
|
maxDepth,
|
|
11873
12477
|
scriptsOnly
|
|
11874
12478
|
}, void 0, instance_id);
|
|
@@ -12029,7 +12633,7 @@ var RobloxStudioTools = class {
|
|
|
12029
12633
|
};
|
|
12030
12634
|
}
|
|
12031
12635
|
async setSelection(paths, mode, instance_id) {
|
|
12032
|
-
if (!Array.isArray(paths) || paths.some((
|
|
12636
|
+
if (!Array.isArray(paths) || paths.some((path9) => typeof path9 !== "string" || path9.length === 0)) {
|
|
12033
12637
|
throw new Error("selection paths must contain only non-empty instance paths");
|
|
12034
12638
|
}
|
|
12035
12639
|
const selectionMode = mode ?? "set";
|
|
@@ -12766,9 +13370,9 @@ var RobloxStudioTools = class {
|
|
|
12766
13370
|
const rawJson = mutable.raw_json;
|
|
12767
13371
|
if (typeof rawJson === "string") {
|
|
12768
13372
|
if (typeof outputPath === "string" && outputPath !== "") {
|
|
12769
|
-
const resolvedOutputPath =
|
|
12770
|
-
|
|
12771
|
-
|
|
13373
|
+
const resolvedOutputPath = path8.resolve(outputPath);
|
|
13374
|
+
fs5.mkdirSync(path8.dirname(resolvedOutputPath), { recursive: true });
|
|
13375
|
+
fs5.writeFileSync(resolvedOutputPath, rawJson, "utf8");
|
|
12772
13376
|
mutable.output_path = resolvedOutputPath;
|
|
12773
13377
|
}
|
|
12774
13378
|
delete mutable.raw_json;
|
|
@@ -12829,9 +13433,9 @@ var RobloxStudioTools = class {
|
|
|
12829
13433
|
const rawSnapshotBase64 = mutable.raw_snapshot_base64;
|
|
12830
13434
|
if (typeof rawSnapshotBase64 === "string") {
|
|
12831
13435
|
if (typeof outputPath === "string" && outputPath !== "") {
|
|
12832
|
-
const resolvedOutputPath =
|
|
12833
|
-
|
|
12834
|
-
|
|
13436
|
+
const resolvedOutputPath = path8.resolve(outputPath);
|
|
13437
|
+
fs5.mkdirSync(path8.dirname(resolvedOutputPath), { recursive: true });
|
|
13438
|
+
fs5.writeFileSync(resolvedOutputPath, Buffer.from(rawSnapshotBase64, "base64"));
|
|
12835
13439
|
mutable.output_path = resolvedOutputPath;
|
|
12836
13440
|
}
|
|
12837
13441
|
delete mutable.raw_snapshot_base64;
|
|
@@ -12845,9 +13449,9 @@ var RobloxStudioTools = class {
|
|
|
12845
13449
|
});
|
|
12846
13450
|
}
|
|
12847
13451
|
if (typeof summaryOutputPath === "string" && summaryOutputPath !== "") {
|
|
12848
|
-
const resolvedSummaryPath =
|
|
12849
|
-
|
|
12850
|
-
|
|
13452
|
+
const resolvedSummaryPath = path8.resolve(summaryOutputPath);
|
|
13453
|
+
fs5.mkdirSync(path8.dirname(resolvedSummaryPath), { recursive: true });
|
|
13454
|
+
fs5.writeFileSync(resolvedSummaryPath, JSON.stringify(mutable, null, 2), "utf8");
|
|
12851
13455
|
mutable.summary_output_path = resolvedSummaryPath;
|
|
12852
13456
|
}
|
|
12853
13457
|
if (!includeComparisonIndex) {
|
|
@@ -12909,7 +13513,7 @@ var RobloxStudioTools = class {
|
|
|
12909
13513
|
return record.placeId !== void 0 && instance.placeId === record.placeId;
|
|
12910
13514
|
}
|
|
12911
13515
|
if ((record.source === "baseplate" || record.source === "local_file") && record.localPlaceFile) {
|
|
12912
|
-
const expectedName =
|
|
13516
|
+
const expectedName = path8.basename(record.localPlaceFile);
|
|
12913
13517
|
return instance.placeName === expectedName || instance.dataModelName === expectedName;
|
|
12914
13518
|
}
|
|
12915
13519
|
return true;
|
|
@@ -12922,7 +13526,7 @@ var RobloxStudioTools = class {
|
|
|
12922
13526
|
await instanceManager.attachInstanceId(candidate, instance.instanceId);
|
|
12923
13527
|
}
|
|
12924
13528
|
async _deriveUniverseId(placeId) {
|
|
12925
|
-
const response = await fetch(`https://apis.roblox.com/universes/v1/places/${placeId}/universe
|
|
13529
|
+
const response = await fetch(`https://apis.roblox.com/universes/v1/places/${placeId}/universe`, { signal: AbortSignal.timeout(3e4) });
|
|
12926
13530
|
if (!response.ok) {
|
|
12927
13531
|
const body = await response.text().catch(() => "");
|
|
12928
13532
|
throw new Error(`Could not resolve the universe for place_id ${placeId} (${response.status}): ${body}`);
|
|
@@ -14240,11 +14844,11 @@ var RobloxStudioTools = class {
|
|
|
14240
14844
|
return this.resolveUploadedReferenceImageId(decalId, instance_id);
|
|
14241
14845
|
}
|
|
14242
14846
|
async uploadAsset(filePath, assetType, displayName, description, userId, groupId) {
|
|
14243
|
-
if (!
|
|
14847
|
+
if (!fs5.existsSync(filePath)) {
|
|
14244
14848
|
throw new Error(`File not found: ${filePath}`);
|
|
14245
14849
|
}
|
|
14246
|
-
const fileContent =
|
|
14247
|
-
const fileName =
|
|
14850
|
+
const fileContent = fs5.readFileSync(filePath);
|
|
14851
|
+
const fileName = path8.basename(filePath);
|
|
14248
14852
|
const resolvedGroupId = groupId || process.env.ROBLOX_CREATOR_GROUP_ID;
|
|
14249
14853
|
const resolvedUserId = userId || process.env.ROBLOX_CREATOR_USER_ID;
|
|
14250
14854
|
if (assetType === "Decal" && this.cookieClient.hasCookie()) {
|
|
@@ -14460,10 +15064,10 @@ var RobloxStudioTools = class {
|
|
|
14460
15064
|
return { content: [{ type: "text", text: JSON.stringify({ error: "plugin returned no base64 payload" }) }] };
|
|
14461
15065
|
}
|
|
14462
15066
|
const bytes = Buffer.from(response.base64, "base64");
|
|
14463
|
-
const resolved =
|
|
15067
|
+
const resolved = path8.resolve(outputPath);
|
|
14464
15068
|
try {
|
|
14465
|
-
|
|
14466
|
-
|
|
15069
|
+
fs5.mkdirSync(path8.dirname(resolved), { recursive: true });
|
|
15070
|
+
fs5.writeFileSync(resolved, bytes);
|
|
14467
15071
|
} catch (err2) {
|
|
14468
15072
|
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err2.message}` }) }] };
|
|
14469
15073
|
}
|
|
@@ -14478,7 +15082,7 @@ var RobloxStudioTools = class {
|
|
|
14478
15082
|
}]
|
|
14479
15083
|
};
|
|
14480
15084
|
}
|
|
14481
|
-
async importRbxm(source, parentPath, target, instance_id) {
|
|
15085
|
+
async importRbxm(source, parentPath, target, instance_id, signal) {
|
|
14482
15086
|
if (!source || typeof source !== "object") {
|
|
14483
15087
|
throw new Error("source is required for import_rbxm");
|
|
14484
15088
|
}
|
|
@@ -14493,59 +15097,18 @@ var RobloxStudioTools = class {
|
|
|
14493
15097
|
if (modes.length !== 1) {
|
|
14494
15098
|
throw new Error(`source must contain exactly one of { path, url, base64 } (got: ${modes.join(", ") || "none"})`);
|
|
14495
15099
|
}
|
|
14496
|
-
let
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
bytes = fs4.readFileSync(resolved);
|
|
14502
|
-
} catch (err2) {
|
|
14503
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to read ${resolved}: ${err2.message}` }) }] };
|
|
14504
|
-
}
|
|
14505
|
-
sourceLabel = resolved;
|
|
14506
|
-
} else if (source.url !== void 0) {
|
|
14507
|
-
let parsedUrl;
|
|
14508
|
-
try {
|
|
14509
|
-
parsedUrl = new URL(source.url);
|
|
14510
|
-
} catch {
|
|
14511
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: `import_rbxm url is not a valid URL: ${source.url}` }) }] };
|
|
14512
|
-
}
|
|
14513
|
-
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
14514
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: `import_rbxm url must use http(s); got ${parsedUrl.protocol}` }) }] };
|
|
14515
|
-
}
|
|
14516
|
-
const MAX_IMPORT_BYTES = 50 * 1024 * 1024;
|
|
14517
|
-
try {
|
|
14518
|
-
const res = await fetch(source.url);
|
|
14519
|
-
if (!res.ok) {
|
|
14520
|
-
const snippet = (await res.text()).slice(0, 500);
|
|
14521
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url} returned ${res.status}: ${snippet}` }) }] };
|
|
14522
|
-
}
|
|
14523
|
-
const claimed = Number(res.headers.get("content-length") ?? "0");
|
|
14524
|
-
if (claimed > MAX_IMPORT_BYTES) {
|
|
14525
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url}: content-length ${claimed} exceeds ${MAX_IMPORT_BYTES} byte cap` }) }] };
|
|
14526
|
-
}
|
|
14527
|
-
const arr = await res.arrayBuffer();
|
|
14528
|
-
if (arr.byteLength > MAX_IMPORT_BYTES) {
|
|
14529
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url}: downloaded ${arr.byteLength} bytes exceeds ${MAX_IMPORT_BYTES} byte cap` }) }] };
|
|
14530
|
-
}
|
|
14531
|
-
bytes = Buffer.from(arr);
|
|
14532
|
-
} catch (err2) {
|
|
14533
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url} failed: ${err2.message}` }) }] };
|
|
14534
|
-
}
|
|
14535
|
-
sourceLabel = source.url;
|
|
14536
|
-
} else {
|
|
14537
|
-
try {
|
|
14538
|
-
bytes = Buffer.from(source.base64, "base64");
|
|
14539
|
-
} catch (err2) {
|
|
14540
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: `base64 decode failed: ${err2.message}` }) }] };
|
|
14541
|
-
}
|
|
14542
|
-
sourceLabel = `base64(${bytes.length}B)`;
|
|
15100
|
+
let input;
|
|
15101
|
+
try {
|
|
15102
|
+
input = await readRbxmInput(source, parentPath, signal);
|
|
15103
|
+
} catch (error) {
|
|
15104
|
+
return this._textResult({ error: error instanceof Error ? error.message : String(error) });
|
|
14543
15105
|
}
|
|
15106
|
+
const { bytes, sourceLabel } = input;
|
|
14544
15107
|
const response = await this._callSingle("/api/import-rbxm", {
|
|
14545
15108
|
base64: bytes.toString("base64"),
|
|
14546
15109
|
parent_path: parentPath,
|
|
14547
15110
|
source_label: sourceLabel
|
|
14548
|
-
}, tgt, instance_id);
|
|
15111
|
+
}, tgt, instance_id, void 0, signal);
|
|
14549
15112
|
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
14550
15113
|
}
|
|
14551
15114
|
// Even native capture must wait while another call has fitted the simulator
|
|
@@ -14567,24 +15130,26 @@ var RobloxStudioTools = class {
|
|
|
14567
15130
|
const q = quality === void 0 ? 92 : Math.max(1, Math.min(100, Math.floor(quality)));
|
|
14568
15131
|
let response = await this._callSingle("/api/capture-studio", { encoding: fmt === "png" ? "png" : "rgba8" }, targetRole, instanceId);
|
|
14569
15132
|
const studioFastPathMissing = response.unavailable !== void 0 || (response.error?.includes("/api/capture-studio") ?? false);
|
|
15133
|
+
const studioFastPathUnavailable = studioFastPathMissing ? response.unavailable ?? response.error : void 0;
|
|
14570
15134
|
if (studioFastPathMissing) {
|
|
14571
15135
|
if (targetRole.startsWith("client-")) {
|
|
14572
15136
|
const begin = await this._callSingle("/api/capture-begin", {}, targetRole, instanceId);
|
|
14573
15137
|
if (begin.error) {
|
|
14574
|
-
|
|
14575
|
-
}
|
|
14576
|
-
|
|
14577
|
-
|
|
15138
|
+
response = { error: begin.error };
|
|
15139
|
+
} else if (!begin.contentId) {
|
|
15140
|
+
response = { error: "Screenshot capture failed: no content id returned from client." };
|
|
15141
|
+
} else {
|
|
15142
|
+
response = await this._callSingle("/api/capture-read", { contentId: begin.contentId }, "edit", instanceId);
|
|
14578
15143
|
}
|
|
14579
|
-
response = await this._callSingle("/api/capture-read", { contentId: begin.contentId }, "edit", instanceId);
|
|
14580
15144
|
} else {
|
|
14581
15145
|
response = await this._callSingle("/api/capture-screenshot", {}, "edit", instanceId);
|
|
14582
15146
|
}
|
|
15147
|
+
response.source ??= "CaptureService";
|
|
14583
15148
|
}
|
|
14584
15149
|
let hostNote = "";
|
|
14585
15150
|
let hostReason;
|
|
14586
15151
|
try {
|
|
14587
|
-
hostReason = response.error ? `Studio's capture failed (${response.error})` : this._isUniformCaptureResponse(response) ?
|
|
15152
|
+
hostReason = response.error ? `Studio's capture failed (${response.error})` : this._isUniformCaptureResponse(response) ? `Studio's ${response.source ?? "capture"} returned a blank (single-colour) frame` : void 0;
|
|
14588
15153
|
} catch (error) {
|
|
14589
15154
|
response = { ...response, error: `Could not decode Studio's screenshot: ${error instanceof Error ? error.message : String(error)}` };
|
|
14590
15155
|
hostReason = `Studio's capture failed (${response.error})`;
|
|
@@ -14605,7 +15170,7 @@ var RobloxStudioTools = class {
|
|
|
14605
15170
|
if (targetRole.startsWith("client-") && response.error.includes("Failed to load texture, unexpected format") && await this._isMultiplayerTestRunning(instanceId)) {
|
|
14606
15171
|
text = `Screenshot capture reached the multiplayer client, but Roblox returned a temporary screenshot texture that the edit peer cannot read in StudioTestService multiplayer sessions. Regular solo_playtest capture works because the temporary rbxtemp:// handle is readable from the edit process; multiplayer client handles appear to be scoped to the client process. Raw error: ${response.error}`;
|
|
14607
15172
|
}
|
|
14608
|
-
return { success: false, error: text };
|
|
15173
|
+
return { success: false, error: text, studioFastPathUnavailable };
|
|
14609
15174
|
}
|
|
14610
15175
|
const w = response.width;
|
|
14611
15176
|
const h = response.height;
|
|
@@ -14659,7 +15224,9 @@ var RobloxStudioTools = class {
|
|
|
14659
15224
|
note,
|
|
14660
15225
|
data: buffer.toString("base64"),
|
|
14661
15226
|
mimeType,
|
|
14662
|
-
message
|
|
15227
|
+
message,
|
|
15228
|
+
source: response.source,
|
|
15229
|
+
studioFastPathUnavailable
|
|
14663
15230
|
};
|
|
14664
15231
|
}
|
|
14665
15232
|
_isUniformCaptureResponse(response) {
|
|
@@ -14681,6 +15248,19 @@ var RobloxStudioTools = class {
|
|
|
14681
15248
|
}
|
|
14682
15249
|
// Always restore the simulator, including failures and cached window grabs.
|
|
14683
15250
|
async _captureViewportFromHostWindow(instanceId, targetRole) {
|
|
15251
|
+
if (isHostCaptureDisabled()) {
|
|
15252
|
+
return { success: false, error: "host window capture is disabled by ROBLOX_STUDIO_HOST_CAPTURE" };
|
|
15253
|
+
}
|
|
15254
|
+
if (!isHostCaptureSupported()) {
|
|
15255
|
+
return { success: false, error: hostCaptureUnsupportedReason() };
|
|
15256
|
+
}
|
|
15257
|
+
if (this.hostWindowCapture === captureStudioWindow) {
|
|
15258
|
+
try {
|
|
15259
|
+
await prepareHostWindowCapture();
|
|
15260
|
+
} catch (error) {
|
|
15261
|
+
return { success: false, error: `could not prepare host capture: ${error instanceof Error ? error.message : String(error)}` };
|
|
15262
|
+
}
|
|
15263
|
+
}
|
|
14684
15264
|
let prepared;
|
|
14685
15265
|
let result;
|
|
14686
15266
|
try {
|
|
@@ -14734,9 +15314,12 @@ var RobloxStudioTools = class {
|
|
|
14734
15314
|
const viewportWidth = prepared.viewportWidth ?? sizeInfo.viewportWidth;
|
|
14735
15315
|
const viewportHeight = prepared.viewportHeight ?? sizeInfo.viewportHeight;
|
|
14736
15316
|
if (cached && Date.now() - cached.cachedAt < HOST_VIEWPORT_RECT_TTL_MS && cached.viewportWidth === viewportWidth && cached.viewportHeight === viewportHeight) {
|
|
14737
|
-
const grab = await this.hostWindowCapture(titleHint);
|
|
15317
|
+
const grab = await this.hostWindowCapture(titleHint, cached.identity);
|
|
14738
15318
|
if (!grab.ok)
|
|
14739
15319
|
return { success: false, error: grab.error };
|
|
15320
|
+
if (cached.identity !== void 0 && !sameHostWindowIdentity(grab.capture.identity, cached.identity)) {
|
|
15321
|
+
return { success: false, error: "the Studio window identity changed since locating the viewport; retry the capture" };
|
|
15322
|
+
}
|
|
14740
15323
|
if (grab.capture.width === cached.windowWidth && grab.capture.height === cached.windowHeight) {
|
|
14741
15324
|
return {
|
|
14742
15325
|
success: true,
|
|
@@ -14772,21 +15355,25 @@ var RobloxStudioTools = class {
|
|
|
14772
15355
|
});
|
|
14773
15356
|
if ("error" in located)
|
|
14774
15357
|
return { success: false, error: located.error };
|
|
15358
|
+
const cleanGrab = await this.hostWindowCapture(titleHint, markerGrab.capture.identity);
|
|
15359
|
+
if (!cleanGrab.ok)
|
|
15360
|
+
return { success: false, error: cleanGrab.error };
|
|
15361
|
+
if (markerGrab.capture.identity !== void 0 && !sameHostWindowIdentity(cleanGrab.capture.identity, markerGrab.capture.identity)) {
|
|
15362
|
+
return { success: false, error: "the Studio window identity changed while locating the viewport; retry the capture" };
|
|
15363
|
+
}
|
|
15364
|
+
if (cleanGrab.capture.width !== markerGrab.capture.width || cleanGrab.capture.height !== markerGrab.capture.height) {
|
|
15365
|
+
this.hostViewportRects.delete(cacheKey);
|
|
15366
|
+
return { success: false, error: "the Studio window was resized while locating the viewport; retry the capture" };
|
|
15367
|
+
}
|
|
14775
15368
|
this.hostViewportRects.set(cacheKey, {
|
|
14776
15369
|
rect: located.rect,
|
|
15370
|
+
identity: markerGrab.capture.identity,
|
|
14777
15371
|
windowWidth: markerGrab.capture.width,
|
|
14778
15372
|
windowHeight: markerGrab.capture.height,
|
|
14779
15373
|
viewportWidth,
|
|
14780
15374
|
viewportHeight,
|
|
14781
15375
|
cachedAt: Date.now()
|
|
14782
15376
|
});
|
|
14783
|
-
const cleanGrab = await this.hostWindowCapture(titleHint);
|
|
14784
|
-
if (!cleanGrab.ok)
|
|
14785
|
-
return { success: false, error: cleanGrab.error };
|
|
14786
|
-
if (cleanGrab.capture.width !== markerGrab.capture.width || cleanGrab.capture.height !== markerGrab.capture.height) {
|
|
14787
|
-
this.hostViewportRects.delete(cacheKey);
|
|
14788
|
-
return { success: false, error: "the Studio window was resized while locating the viewport; retry the capture" };
|
|
14789
|
-
}
|
|
14790
15377
|
return {
|
|
14791
15378
|
success: true,
|
|
14792
15379
|
response: this._hostResponseFromCrop(cleanGrab.capture.rgba, cleanGrab.capture.width, cleanGrab.capture.height, located.rect, viewportWidth, viewportHeight)
|
|
@@ -14812,7 +15399,7 @@ var RobloxStudioTools = class {
|
|
|
14812
15399
|
const { instanceId, clientRole } = this._resolveRuntime(instance_id);
|
|
14813
15400
|
const capture = await this._captureViewportImage(instanceId, clientRole ?? "edit", format, quality);
|
|
14814
15401
|
if (!capture.success) {
|
|
14815
|
-
return this._textResult({ error: capture.error });
|
|
15402
|
+
return this._textResult({ error: capture.error, studioFastPathUnavailable: capture.studioFastPathUnavailable });
|
|
14816
15403
|
}
|
|
14817
15404
|
return {
|
|
14818
15405
|
content: [
|
|
@@ -14824,7 +15411,9 @@ var RobloxStudioTools = class {
|
|
|
14824
15411
|
format: capture.format,
|
|
14825
15412
|
mimeType: capture.mimeType,
|
|
14826
15413
|
...capture.quality === void 0 ? {} : { quality: capture.quality },
|
|
14827
|
-
message: capture.message
|
|
15414
|
+
message: capture.message,
|
|
15415
|
+
source: capture.source,
|
|
15416
|
+
studioFastPathUnavailable: capture.studioFastPathUnavailable
|
|
14828
15417
|
})
|
|
14829
15418
|
},
|
|
14830
15419
|
{
|
|
@@ -14974,6 +15563,7 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
|
|
|
14974
15563
|
const response = await fetch(`${this.primaryBaseUrl}/create-multiplayer-group`, {
|
|
14975
15564
|
method: "POST",
|
|
14976
15565
|
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
15566
|
+
signal: AbortSignal.timeout(5e3),
|
|
14977
15567
|
body: JSON.stringify({ groupId, controllerInstanceId })
|
|
14978
15568
|
});
|
|
14979
15569
|
if (!response.ok) {
|
|
@@ -15004,6 +15594,7 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
|
|
|
15004
15594
|
const response = await fetch(`${this.primaryBaseUrl}/remove-multiplayer-group`, {
|
|
15005
15595
|
method: "POST",
|
|
15006
15596
|
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
15597
|
+
signal: AbortSignal.timeout(5e3),
|
|
15007
15598
|
body: JSON.stringify({ groupId })
|
|
15008
15599
|
});
|
|
15009
15600
|
if (response.status === 409) {
|
|
@@ -15035,6 +15626,7 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
|
|
|
15035
15626
|
const response = await fetch(`${this.primaryBaseUrl}/unregister-instance-id`, {
|
|
15036
15627
|
method: "POST",
|
|
15037
15628
|
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
15629
|
+
signal: AbortSignal.timeout(5e3),
|
|
15038
15630
|
body: JSON.stringify({ instanceId })
|
|
15039
15631
|
});
|
|
15040
15632
|
if (!response.ok) {
|
|
@@ -15064,7 +15656,8 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
|
|
|
15064
15656
|
}
|
|
15065
15657
|
async getRequestStatusEverywhere(requestId) {
|
|
15066
15658
|
const response = await fetch(`${this.primaryBaseUrl}/request-status?requestId=${encodeURIComponent(requestId)}`, {
|
|
15067
|
-
headers: this.authHeaders()
|
|
15659
|
+
headers: this.authHeaders(),
|
|
15660
|
+
signal: AbortSignal.timeout(5e3)
|
|
15068
15661
|
});
|
|
15069
15662
|
if (!response.ok)
|
|
15070
15663
|
throw new Error(`Proxy request status failed (${response.status})`);
|
|
@@ -15369,8 +15962,8 @@ var BoundedStdioTransport = class {
|
|
|
15369
15962
|
this.onStreamError(error);
|
|
15370
15963
|
return Promise.reject(error);
|
|
15371
15964
|
}
|
|
15372
|
-
const { promise, resolve:
|
|
15373
|
-
const pending = { resolve:
|
|
15965
|
+
const { promise, resolve: resolve6, reject } = Promise.withResolvers();
|
|
15966
|
+
const pending = { resolve: resolve6, reject };
|
|
15374
15967
|
this.pendingSends.add(pending);
|
|
15375
15968
|
this.pendingWrites++;
|
|
15376
15969
|
try {
|
|
@@ -15382,7 +15975,7 @@ var BoundedStdioTransport = class {
|
|
|
15382
15975
|
});
|
|
15383
15976
|
if (writable) {
|
|
15384
15977
|
this.pendingSends.delete(pending);
|
|
15385
|
-
|
|
15978
|
+
resolve6();
|
|
15386
15979
|
} else if (!this.closed) {
|
|
15387
15980
|
this.outputBlocked = true;
|
|
15388
15981
|
this.input.pause();
|
|
@@ -15481,6 +16074,7 @@ var RobloxStudioMCPServer = class {
|
|
|
15481
16074
|
try {
|
|
15482
16075
|
const result = await listenWithRetry(candidateApp, host, basePort, 1);
|
|
15483
16076
|
const oldBridge = this.bridge;
|
|
16077
|
+
const oldTools = this.tools;
|
|
15484
16078
|
this.bridge = candidateBridge;
|
|
15485
16079
|
this.tools = candidateTools;
|
|
15486
16080
|
if (oldBridge instanceof ProxyBridgeService) {
|
|
@@ -15494,6 +16088,7 @@ var RobloxStudioMCPServer = class {
|
|
|
15494
16088
|
console.error(`Promoted from proxy to primary on port ${boundPort}`);
|
|
15495
16089
|
if (promotionInterval)
|
|
15496
16090
|
clearInterval(promotionInterval);
|
|
16091
|
+
await oldTools.dispose();
|
|
15497
16092
|
} catch {
|
|
15498
16093
|
await candidateApp.cleanup().catch(() => {
|
|
15499
16094
|
});
|
|
@@ -15558,6 +16153,7 @@ var RobloxStudioMCPServer = class {
|
|
|
15558
16153
|
});
|
|
15559
16154
|
if (httpHandle)
|
|
15560
16155
|
httpHandle.close();
|
|
16156
|
+
await this.tools.dispose();
|
|
15561
16157
|
process.exit(0);
|
|
15562
16158
|
};
|
|
15563
16159
|
process.on("SIGTERM", shutdown);
|
|
@@ -17219,7 +17815,7 @@ var TOOL_DEFINITIONS = [
|
|
|
17219
17815
|
properties: {
|
|
17220
17816
|
source: {
|
|
17221
17817
|
type: "object",
|
|
17222
|
-
description: "Exactly one path, URL, or base64 source;
|
|
17818
|
+
description: "Exactly one path, URL, or base64 source. All inputs are capped below 37.5 MiB to fit base64 plus routing metadata within transport limits; URL downloads have a 30-second deadline.",
|
|
17223
17819
|
properties: {
|
|
17224
17820
|
path: { type: "string", description: "Absolute local .rbxm path." },
|
|
17225
17821
|
url: { type: "string", description: "HTTP or HTTPS .rbxm URL." },
|
|
@@ -17348,10 +17944,10 @@ var getAllTools = () => [...TOOL_DEFINITIONS];
|
|
|
17348
17944
|
|
|
17349
17945
|
// ../core/dist/install-plugin-helpers.js
|
|
17350
17946
|
var import_saxes = __toESM(require_saxes(), 1);
|
|
17351
|
-
import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync4, readFileSync as
|
|
17947
|
+
import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync, rmSync as rmSync4, rmdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
17352
17948
|
import { execSync } from "child_process";
|
|
17353
17949
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
17354
|
-
import { basename as basename3, join as
|
|
17950
|
+
import { basename as basename3, join as join8 } from "path";
|
|
17355
17951
|
import { homedir as homedir4 } from "os";
|
|
17356
17952
|
import { isUtf8 } from "buffer";
|
|
17357
17953
|
var DEFAULT_PLUGIN_PORT = 58741;
|
|
@@ -17403,15 +17999,15 @@ function getPluginsFolder() {
|
|
|
17403
17999
|
if (process.env.MCP_PLUGINS_DIR)
|
|
17404
18000
|
return process.env.MCP_PLUGINS_DIR;
|
|
17405
18001
|
if (process.platform === "win32") {
|
|
17406
|
-
return
|
|
18002
|
+
return join8(process.env.LOCALAPPDATA || join8(homedir4(), "AppData", "Local"), "Roblox", "Plugins");
|
|
17407
18003
|
}
|
|
17408
18004
|
if (isWSL()) {
|
|
17409
18005
|
const localAppData = getWindowsUserLocalAppData();
|
|
17410
18006
|
if (localAppData)
|
|
17411
|
-
return
|
|
18007
|
+
return join8(localAppData.linuxPath, "Roblox", "Plugins");
|
|
17412
18008
|
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.");
|
|
17413
18009
|
}
|
|
17414
|
-
return
|
|
18010
|
+
return join8(homedir4(), "Documents", "Roblox", "Plugins");
|
|
17415
18011
|
}
|
|
17416
18012
|
var STALE_TEST_PLUGINS_DIRECTORY = "RsmcpIsolatedPlugins";
|
|
17417
18013
|
function pluginDirectorySettingPattern() {
|
|
@@ -17426,7 +18022,7 @@ function repairStaleStudioPluginDirectorySetting({ settingsPath, studioPluginsDi
|
|
|
17426
18022
|
if (studioPluginsDirectory === "") {
|
|
17427
18023
|
throw new Error("studioPluginsDirectory must be a non-empty path.");
|
|
17428
18024
|
}
|
|
17429
|
-
const contents =
|
|
18025
|
+
const contents = readFileSync8(settingsPath, "utf8");
|
|
17430
18026
|
const matches = [...contents.matchAll(pluginDirectorySettingPattern())];
|
|
17431
18027
|
if (matches.length !== 1) {
|
|
17432
18028
|
throw new Error(`Expected exactly one Studio.PluginsDir QDir in ${settingsPath}; found ${matches.length}.`);
|
|
@@ -17434,16 +18030,16 @@ function repairStaleStudioPluginDirectorySetting({ settingsPath, studioPluginsDi
|
|
|
17434
18030
|
if (matches[0][2] !== STALE_TEST_PLUGINS_DIRECTORY)
|
|
17435
18031
|
return false;
|
|
17436
18032
|
const studioPath = escapeXmlText(studioPluginsDirectory);
|
|
17437
|
-
const updated = contents.replace(pluginDirectorySettingPattern(), (_match,
|
|
18033
|
+
const updated = contents.replace(pluginDirectorySettingPattern(), (_match, open4, _value, close) => `${open4}${studioPath}${close}`);
|
|
17438
18034
|
const temporaryPath = `${settingsPath}.rsmcp-install-${process.pid}-${randomUUID5()}.tmp`;
|
|
17439
18035
|
try {
|
|
17440
|
-
|
|
18036
|
+
writeFileSync4(temporaryPath, updated, {
|
|
17441
18037
|
encoding: "utf8",
|
|
17442
18038
|
mode: statSync3(settingsPath).mode
|
|
17443
18039
|
});
|
|
17444
18040
|
renameSync(temporaryPath, settingsPath);
|
|
17445
18041
|
} finally {
|
|
17446
|
-
|
|
18042
|
+
rmSync4(temporaryPath, { force: true });
|
|
17447
18043
|
}
|
|
17448
18044
|
return true;
|
|
17449
18045
|
}
|
|
@@ -17454,16 +18050,16 @@ function repairDefaultStudioPluginDirectorySetting(pluginsFolder, log) {
|
|
|
17454
18050
|
let defaultPluginsFolder;
|
|
17455
18051
|
let studioPluginsDirectory;
|
|
17456
18052
|
if (process.platform === "win32") {
|
|
17457
|
-
const localAppData = process.env.LOCALAPPDATA ||
|
|
17458
|
-
defaultPluginsFolder =
|
|
17459
|
-
settingsPath =
|
|
18053
|
+
const localAppData = process.env.LOCALAPPDATA || join8(homedir4(), "AppData", "Local");
|
|
18054
|
+
defaultPluginsFolder = join8(localAppData, "Roblox", "Plugins");
|
|
18055
|
+
settingsPath = join8(localAppData, "Roblox", "GlobalSettings_13.xml");
|
|
17460
18056
|
studioPluginsDirectory = defaultPluginsFolder.replaceAll("\\", "/");
|
|
17461
18057
|
} else if (isWSL()) {
|
|
17462
18058
|
const localAppData = getWindowsUserLocalAppData();
|
|
17463
18059
|
if (!localAppData)
|
|
17464
18060
|
return;
|
|
17465
|
-
defaultPluginsFolder =
|
|
17466
|
-
settingsPath =
|
|
18061
|
+
defaultPluginsFolder = join8(localAppData.linuxPath, "Roblox", "Plugins");
|
|
18062
|
+
settingsPath = join8(localAppData.linuxPath, "Roblox", "GlobalSettings_13.xml");
|
|
17467
18063
|
studioPluginsDirectory = [
|
|
17468
18064
|
localAppData.windowsPath.replace(/[\\/]+$/u, ""),
|
|
17469
18065
|
"Roblox",
|
|
@@ -17479,12 +18075,12 @@ function repairDefaultStudioPluginDirectorySetting(pluginsFolder, log) {
|
|
|
17479
18075
|
}
|
|
17480
18076
|
}
|
|
17481
18077
|
function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
|
|
17482
|
-
const otherDest =
|
|
18078
|
+
const otherDest = join8(pluginsFolder, otherAssetName);
|
|
17483
18079
|
if (!existsSync7(otherDest))
|
|
17484
18080
|
return;
|
|
17485
18081
|
if (replace) {
|
|
17486
18082
|
try {
|
|
17487
|
-
|
|
18083
|
+
unlinkSync2(otherDest);
|
|
17488
18084
|
log(`Removed conflicting ${otherAssetName}.`);
|
|
17489
18085
|
} catch (err2) {
|
|
17490
18086
|
warn(`[install-plugin] Could not remove ${otherDest}: ${err2}. Continuing.`);
|
|
@@ -17505,11 +18101,11 @@ function pluginInstallLockError(lockPath, reason) {
|
|
|
17505
18101
|
return new Error(`${reason}: ${lockPath}. If no installer or lock recovery is running, remove that lock directory and retry.`);
|
|
17506
18102
|
}
|
|
17507
18103
|
function readPluginInstallLockOwner(lockPath) {
|
|
17508
|
-
const ownerPath =
|
|
17509
|
-
if (!
|
|
18104
|
+
const ownerPath = join8(lockPath, "owner.json");
|
|
18105
|
+
if (!lstatSync2(lockPath).isDirectory() || !lstatSync2(ownerPath).isFile()) {
|
|
17510
18106
|
throw new Error("Lock and owner metadata must be a directory and regular file");
|
|
17511
18107
|
}
|
|
17512
|
-
const owner = JSON.parse(
|
|
18108
|
+
const owner = JSON.parse(readFileSync8(ownerPath, "utf8"));
|
|
17513
18109
|
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) {
|
|
17514
18110
|
throw new Error("Invalid lock owner metadata");
|
|
17515
18111
|
}
|
|
@@ -17535,7 +18131,7 @@ function recoverPluginInstallLock(lockPath, warn) {
|
|
|
17535
18131
|
throw pluginInstallLockError(lockPath, `Cannot safely read plugin installation lock owner (${error})`);
|
|
17536
18132
|
}
|
|
17537
18133
|
assertPluginInstallOwnerExited(lockPath, owner);
|
|
17538
|
-
const claimPath =
|
|
18134
|
+
const claimPath = join8(lockPath, `recovery-${owner.token}`);
|
|
17539
18135
|
try {
|
|
17540
18136
|
mkdirSync4(claimPath);
|
|
17541
18137
|
} catch (error) {
|
|
@@ -17568,14 +18164,14 @@ function recoverPluginInstallLock(lockPath, warn) {
|
|
|
17568
18164
|
}
|
|
17569
18165
|
}
|
|
17570
18166
|
try {
|
|
17571
|
-
|
|
18167
|
+
rmSync4(abandonedPath, { recursive: true, force: true });
|
|
17572
18168
|
} catch (error) {
|
|
17573
18169
|
warn(`[install-plugin] Could not clean abandoned install lock ${abandonedPath}: ${error}`);
|
|
17574
18170
|
}
|
|
17575
18171
|
}
|
|
17576
18172
|
function acquirePluginInstallLock(pluginsFolder, warn) {
|
|
17577
|
-
const lockPath =
|
|
17578
|
-
const ownerPath =
|
|
18173
|
+
const lockPath = join8(pluginsFolder, PLUGIN_INSTALL_LOCK_NAME);
|
|
18174
|
+
const ownerPath = join8(lockPath, "owner.json");
|
|
17579
18175
|
const owner = {
|
|
17580
18176
|
pid: process.pid,
|
|
17581
18177
|
token: randomUUID5(),
|
|
@@ -17596,14 +18192,14 @@ function acquirePluginInstallLock(pluginsFolder, warn) {
|
|
|
17596
18192
|
}
|
|
17597
18193
|
}
|
|
17598
18194
|
try {
|
|
17599
|
-
|
|
18195
|
+
writeFileSync4(ownerPath, `${JSON.stringify(owner)}
|
|
17600
18196
|
`, {
|
|
17601
18197
|
encoding: "utf8",
|
|
17602
18198
|
flag: "wx"
|
|
17603
18199
|
});
|
|
17604
18200
|
} catch (error) {
|
|
17605
18201
|
try {
|
|
17606
|
-
|
|
18202
|
+
rmSync4(lockPath, { recursive: true, force: true });
|
|
17607
18203
|
} catch (cleanupError) {
|
|
17608
18204
|
warn(`[install-plugin] Could not clean failed install lock ${lockPath}: ${cleanupError}`);
|
|
17609
18205
|
}
|
|
@@ -17613,7 +18209,7 @@ function acquirePluginInstallLock(pluginsFolder, warn) {
|
|
|
17613
18209
|
try {
|
|
17614
18210
|
const currentOwner = readPluginInstallLockOwner(lockPath);
|
|
17615
18211
|
if (currentOwner.pid === owner.pid && currentOwner.token === owner.token) {
|
|
17616
|
-
|
|
18212
|
+
rmSync4(lockPath, { recursive: true, force: true });
|
|
17617
18213
|
}
|
|
17618
18214
|
} catch (error) {
|
|
17619
18215
|
if (errorCode(error) !== "ENOENT") {
|
|
@@ -17707,9 +18303,9 @@ function assertPluginAssetIdentity(source, assetName, expectedVersion, expectedV
|
|
|
17707
18303
|
function installedFileMatches(source, destination) {
|
|
17708
18304
|
if (!existsSync7(destination))
|
|
17709
18305
|
return false;
|
|
17710
|
-
if (!
|
|
18306
|
+
if (!lstatSync2(destination).isFile())
|
|
17711
18307
|
return false;
|
|
17712
|
-
const installed =
|
|
18308
|
+
const installed = readFileSync8(destination);
|
|
17713
18309
|
return installed.length === source.length && installed.equals(source);
|
|
17714
18310
|
}
|
|
17715
18311
|
function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source, expectedVersion, expectedVariant, rawPort, replaceVariant = true, log = console.log, warn = console.warn }) {
|
|
@@ -17721,19 +18317,19 @@ function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source,
|
|
|
17721
18317
|
const configured = configurePluginAssetForPort(source, rawPort);
|
|
17722
18318
|
assertPluginAssetIdentity(configured, assetName, expectedVersion, expectedVariant);
|
|
17723
18319
|
mkdirSync4(pluginsFolder, { recursive: true });
|
|
17724
|
-
const destination =
|
|
18320
|
+
const destination = join8(pluginsFolder, assetName);
|
|
17725
18321
|
const releaseLock = acquirePluginInstallLock(pluginsFolder, warn);
|
|
17726
18322
|
try {
|
|
17727
18323
|
let installed = false;
|
|
17728
18324
|
if (!installedFileMatches(configured, destination)) {
|
|
17729
|
-
const staged =
|
|
18325
|
+
const staged = join8(pluginsFolder, `.${assetName}.${process.pid}.${randomUUID5()}.tmp`);
|
|
17730
18326
|
try {
|
|
17731
|
-
|
|
18327
|
+
writeFileSync4(staged, configured, { flag: "wx" });
|
|
17732
18328
|
renameSync(staged, destination);
|
|
17733
18329
|
installed = true;
|
|
17734
18330
|
} finally {
|
|
17735
18331
|
try {
|
|
17736
|
-
|
|
18332
|
+
unlinkSync2(staged);
|
|
17737
18333
|
} catch (error) {
|
|
17738
18334
|
if (errorCode(error) !== "ENOENT") {
|
|
17739
18335
|
warn(`[install-plugin] Could not remove staging file ${staged}: ${error}`);
|
|
@@ -17755,175 +18351,156 @@ function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source,
|
|
|
17755
18351
|
}
|
|
17756
18352
|
}
|
|
17757
18353
|
|
|
17758
|
-
//
|
|
17759
|
-
import {
|
|
17760
|
-
|
|
17761
|
-
// src/install-plugin.ts
|
|
17762
|
-
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
17763
|
-
import { dirname as dirname5, join as join7 } from "path";
|
|
18354
|
+
// ../core/dist/plugin-installer.js
|
|
18355
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
|
|
18356
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
17764
18357
|
import { fileURLToPath } from "url";
|
|
17765
18358
|
import { get } from "https";
|
|
17766
18359
|
var REPO = "chrrxs/robloxstudio-mcp";
|
|
17767
|
-
var ASSET_NAME = "MCPPlugin.rbxmx";
|
|
17768
|
-
var OTHER_VARIANT = "MCPInspectorPlugin.rbxmx";
|
|
17769
18360
|
var TIMEOUT_MS = 3e4;
|
|
17770
18361
|
var MAX_REDIRECTS = 5;
|
|
17771
18362
|
var MAX_ASSET_BYTES = 64 * 1024 * 1024;
|
|
17772
|
-
function
|
|
17773
|
-
|
|
17774
|
-
|
|
17775
|
-
|
|
17776
|
-
|
|
17777
|
-
|
|
18363
|
+
function createPluginInstaller(moduleUrl, variant) {
|
|
18364
|
+
const currentDir = dirname5(fileURLToPath(moduleUrl));
|
|
18365
|
+
const ASSET_NAME = variant === "main" ? "MCPPlugin.rbxmx" : "MCPInspectorPlugin.rbxmx";
|
|
18366
|
+
const OTHER_VARIANT = variant === "main" ? "MCPInspectorPlugin.rbxmx" : "MCPPlugin.rbxmx";
|
|
18367
|
+
const userAgent = variant === "main" ? "robloxstudio-mcp" : "robloxstudio-mcp-inspector";
|
|
18368
|
+
const buildCommand = variant === "main" ? "build:plugin" : "build:plugin:inspector";
|
|
18369
|
+
function httpsGet(url) {
|
|
18370
|
+
return new Promise((resolve6, reject) => {
|
|
18371
|
+
const req = get(url, { headers: { "User-Agent": userAgent } }, resolve6);
|
|
18372
|
+
req.on("error", reject);
|
|
18373
|
+
req.setTimeout(TIMEOUT_MS, () => {
|
|
18374
|
+
req.destroy(new Error(`Request timed out after ${TIMEOUT_MS}ms`));
|
|
18375
|
+
});
|
|
17778
18376
|
});
|
|
17779
|
-
});
|
|
17780
|
-
}
|
|
17781
|
-
async function download(url, redirects = 0) {
|
|
17782
|
-
const res = await httpsGet(url);
|
|
17783
|
-
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
17784
|
-
if (redirects >= MAX_REDIRECTS) throw new Error(`Too many redirects (max ${MAX_REDIRECTS})`);
|
|
17785
|
-
const location = res.headers.location;
|
|
17786
|
-
if (!location) throw new Error("Redirect with no location header");
|
|
17787
|
-
for await (const _chunk of res) {
|
|
17788
|
-
}
|
|
17789
|
-
return download(location, redirects + 1);
|
|
17790
18377
|
}
|
|
17791
|
-
|
|
17792
|
-
|
|
17793
|
-
|
|
17794
|
-
|
|
17795
|
-
|
|
17796
|
-
|
|
17797
|
-
|
|
17798
|
-
|
|
17799
|
-
|
|
17800
|
-
|
|
17801
|
-
|
|
17802
|
-
|
|
17803
|
-
|
|
18378
|
+
async function download(url, redirects = 0) {
|
|
18379
|
+
const res = await httpsGet(url);
|
|
18380
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
18381
|
+
if (redirects >= MAX_REDIRECTS)
|
|
18382
|
+
throw new Error(`Too many redirects (max ${MAX_REDIRECTS})`);
|
|
18383
|
+
const location = res.headers.location;
|
|
18384
|
+
if (!location)
|
|
18385
|
+
throw new Error("Redirect with no location header");
|
|
18386
|
+
for await (const chunk of res) {
|
|
18387
|
+
void chunk;
|
|
18388
|
+
}
|
|
18389
|
+
return download(location, redirects + 1);
|
|
18390
|
+
}
|
|
18391
|
+
if (res.statusCode !== 200) {
|
|
18392
|
+
throw new Error(`Download failed: HTTP ${res.statusCode}`);
|
|
17804
18393
|
}
|
|
17805
|
-
chunks
|
|
18394
|
+
const chunks = [];
|
|
18395
|
+
let totalBytes = 0;
|
|
18396
|
+
for await (const chunk of res) {
|
|
18397
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
18398
|
+
totalBytes += bytes.length;
|
|
18399
|
+
if (totalBytes > MAX_ASSET_BYTES) {
|
|
18400
|
+
res.destroy();
|
|
18401
|
+
throw new Error(`${ASSET_NAME} download exceeds the ${MAX_ASSET_BYTES}-byte limit.`);
|
|
18402
|
+
}
|
|
18403
|
+
chunks.push(bytes);
|
|
18404
|
+
}
|
|
18405
|
+
return Buffer.concat(chunks, totalBytes);
|
|
17806
18406
|
}
|
|
17807
|
-
|
|
17808
|
-
|
|
17809
|
-
|
|
17810
|
-
|
|
17811
|
-
|
|
17812
|
-
|
|
18407
|
+
async function fetchJson(url) {
|
|
18408
|
+
const res = await httpsGet(url);
|
|
18409
|
+
if (res.statusCode !== 200) {
|
|
18410
|
+
throw new Error(`GitHub API returned HTTP ${res.statusCode}`);
|
|
18411
|
+
}
|
|
18412
|
+
const chunks = [];
|
|
18413
|
+
for await (const chunk of res) {
|
|
18414
|
+
chunks.push(chunk);
|
|
18415
|
+
}
|
|
18416
|
+
return JSON.parse(Buffer.concat(chunks).toString());
|
|
17813
18417
|
}
|
|
17814
|
-
|
|
17815
|
-
|
|
17816
|
-
|
|
18418
|
+
function bundledAssetPath() {
|
|
18419
|
+
const candidates = [
|
|
18420
|
+
join9(currentDir, "..", "studio-plugin", ASSET_NAME),
|
|
18421
|
+
join9(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
|
|
18422
|
+
];
|
|
18423
|
+
return candidates.find((candidate) => existsSync8(candidate)) ?? null;
|
|
17817
18424
|
}
|
|
17818
|
-
|
|
17819
|
-
|
|
17820
|
-
|
|
17821
|
-
|
|
17822
|
-
const prerelease = releases.find(
|
|
17823
|
-
(r) => r.prerelease && r.assets.some((a) => a.name === ASSET_NAME)
|
|
17824
|
-
);
|
|
17825
|
-
if (!prerelease) {
|
|
17826
|
-
throw new Error(`No prerelease found with ${ASSET_NAME}`);
|
|
17827
|
-
}
|
|
17828
|
-
return prerelease;
|
|
17829
|
-
}
|
|
17830
|
-
function bundledAssetPath() {
|
|
17831
|
-
const currentDir = dirname5(fileURLToPath(import.meta.url));
|
|
17832
|
-
const candidates = [
|
|
17833
|
-
join7(currentDir, "..", "studio-plugin", ASSET_NAME),
|
|
17834
|
-
join7(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
|
|
17835
|
-
];
|
|
17836
|
-
return candidates.find((candidate) => existsSync8(candidate)) ?? null;
|
|
17837
|
-
}
|
|
17838
|
-
function resolvePluginAssetPath(sourcePath) {
|
|
17839
|
-
if (sourcePath === void 0) return bundledAssetPath();
|
|
17840
|
-
return existsSync8(sourcePath) ? sourcePath : null;
|
|
17841
|
-
}
|
|
17842
|
-
function packageVersion() {
|
|
17843
|
-
const currentDir = dirname5(fileURLToPath(import.meta.url));
|
|
17844
|
-
const pkg = JSON.parse(readFileSync8(join7(currentDir, "..", "package.json"), "utf8"));
|
|
17845
|
-
if (!pkg.version) {
|
|
17846
|
-
throw new Error("Package version not found");
|
|
18425
|
+
function resolvePluginAssetPath(sourcePath) {
|
|
18426
|
+
if (sourcePath === void 0)
|
|
18427
|
+
return bundledAssetPath();
|
|
18428
|
+
return existsSync8(sourcePath) ? sourcePath : null;
|
|
17847
18429
|
}
|
|
17848
|
-
|
|
17849
|
-
|
|
17850
|
-
|
|
17851
|
-
|
|
17852
|
-
|
|
17853
|
-
|
|
17854
|
-
const sourcePath = resolvePluginAssetPath(options.sourcePath);
|
|
17855
|
-
if (!sourcePath) {
|
|
17856
|
-
throw new Error(
|
|
17857
|
-
`Bundled ${ASSET_NAME} not found. Run npm run build:plugin in this worktree first.`
|
|
17858
|
-
);
|
|
17859
|
-
}
|
|
17860
|
-
const result = await installPluginAsset({
|
|
17861
|
-
pluginsFolder: getPluginsFolder(),
|
|
17862
|
-
assetName: ASSET_NAME,
|
|
17863
|
-
otherAssetName: OTHER_VARIANT,
|
|
17864
|
-
source: readFileSync8(sourcePath),
|
|
17865
|
-
expectedVersion: packageVersion(),
|
|
17866
|
-
expectedVariant: "main",
|
|
17867
|
-
replaceVariant,
|
|
17868
|
-
log,
|
|
17869
|
-
warn
|
|
17870
|
-
});
|
|
17871
|
-
if (result.installed) {
|
|
17872
|
-
log(`Installed ${ASSET_NAME} to ${result.destination}`);
|
|
18430
|
+
function packageVersion() {
|
|
18431
|
+
const pkg = JSON.parse(readFileSync9(join9(currentDir, "..", "package.json"), "utf8"));
|
|
18432
|
+
if (!pkg.version) {
|
|
18433
|
+
throw new Error("Package version not found");
|
|
18434
|
+
}
|
|
18435
|
+
return pkg.version;
|
|
17873
18436
|
}
|
|
17874
|
-
|
|
17875
|
-
|
|
17876
|
-
const dev = options.dev ?? process.argv.includes("--dev");
|
|
17877
|
-
const replaceVariant = options.replaceVariant ?? true;
|
|
17878
|
-
const log = options.log ?? console.log;
|
|
17879
|
-
const warn = options.warn ?? console.warn;
|
|
17880
|
-
const bundled = resolvePluginAssetPath(options.sourcePath);
|
|
17881
|
-
if (options.sourcePath !== void 0 && !bundled) {
|
|
17882
|
-
throw new Error(`Plugin asset not found at explicit path ${options.sourcePath}.`);
|
|
17883
|
-
}
|
|
17884
|
-
if (bundled) {
|
|
17885
|
-
const result2 = await installPluginAsset({
|
|
18437
|
+
function installSource(source, options) {
|
|
18438
|
+
return installPluginAsset({
|
|
17886
18439
|
pluginsFolder: getPluginsFolder(),
|
|
17887
18440
|
assetName: ASSET_NAME,
|
|
17888
18441
|
otherAssetName: OTHER_VARIANT,
|
|
17889
|
-
source
|
|
18442
|
+
source,
|
|
17890
18443
|
expectedVersion: packageVersion(),
|
|
17891
|
-
expectedVariant:
|
|
17892
|
-
replaceVariant,
|
|
17893
|
-
log,
|
|
17894
|
-
warn
|
|
18444
|
+
expectedVariant: variant,
|
|
18445
|
+
replaceVariant: options.replaceVariant ?? true,
|
|
18446
|
+
log: options.log ?? console.log,
|
|
18447
|
+
warn: options.warn ?? console.warn
|
|
17895
18448
|
});
|
|
17896
|
-
|
|
17897
|
-
|
|
18449
|
+
}
|
|
18450
|
+
async function installBundledPlugin2(options = {}) {
|
|
18451
|
+
const log = options.log ?? console.log;
|
|
18452
|
+
const sourcePath = resolvePluginAssetPath(options.sourcePath);
|
|
18453
|
+
if (!sourcePath) {
|
|
18454
|
+
throw new Error(`Bundled ${ASSET_NAME} not found. Run npm run ${buildCommand} in this worktree first.`);
|
|
18455
|
+
}
|
|
18456
|
+
const result = installSource(readFileSync9(sourcePath), options);
|
|
18457
|
+
if (result.installed) {
|
|
18458
|
+
log(`Installed ${ASSET_NAME} to ${result.destination}`);
|
|
18459
|
+
}
|
|
18460
|
+
}
|
|
18461
|
+
async function installPlugin2(options = {}, dev = false) {
|
|
18462
|
+
const log = options.log ?? console.log;
|
|
18463
|
+
const bundled = resolvePluginAssetPath(options.sourcePath);
|
|
18464
|
+
if (options.sourcePath !== void 0 && !bundled) {
|
|
18465
|
+
throw new Error(`Plugin asset not found at explicit path ${options.sourcePath}.`);
|
|
18466
|
+
}
|
|
18467
|
+
if (bundled) {
|
|
18468
|
+
const result2 = installSource(readFileSync9(bundled), options);
|
|
18469
|
+
if (result2.installed) {
|
|
18470
|
+
log(`Installed bundled ${ASSET_NAME} to ${result2.destination}`);
|
|
18471
|
+
} else {
|
|
18472
|
+
log(`${ASSET_NAME} already installed.`);
|
|
18473
|
+
}
|
|
18474
|
+
return;
|
|
18475
|
+
}
|
|
18476
|
+
log(dev ? "Fetching matching dev release..." : "Fetching matching release...");
|
|
18477
|
+
const release = await fetchJson(`https://api.github.com/repos/${REPO}/releases/tags/v${encodeURIComponent(packageVersion())}`);
|
|
18478
|
+
const asset = release.assets?.find((a) => a.name === ASSET_NAME);
|
|
18479
|
+
if (!asset) {
|
|
18480
|
+
throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
|
|
18481
|
+
}
|
|
18482
|
+
log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
|
|
18483
|
+
const downloaded = await download(asset.browser_download_url);
|
|
18484
|
+
const result = installSource(downloaded, options);
|
|
18485
|
+
if (result.installed) {
|
|
18486
|
+
log(`Installed to ${result.destination}`);
|
|
17898
18487
|
} else {
|
|
17899
18488
|
log(`${ASSET_NAME} already installed.`);
|
|
17900
18489
|
}
|
|
17901
|
-
return;
|
|
17902
|
-
}
|
|
17903
|
-
log(dev ? "Fetching latest dev prerelease..." : "Fetching latest release...");
|
|
17904
|
-
const release = dev ? await findDevRelease() : await fetchJson(`https://api.github.com/repos/${REPO}/releases/latest`);
|
|
17905
|
-
const asset = release.assets?.find((a) => a.name === ASSET_NAME);
|
|
17906
|
-
if (!asset) {
|
|
17907
|
-
throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
|
|
17908
|
-
}
|
|
17909
|
-
log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
|
|
17910
|
-
const downloaded = await download(asset.browser_download_url);
|
|
17911
|
-
const result = await installPluginAsset({
|
|
17912
|
-
pluginsFolder: getPluginsFolder(),
|
|
17913
|
-
assetName: ASSET_NAME,
|
|
17914
|
-
otherAssetName: OTHER_VARIANT,
|
|
17915
|
-
source: downloaded,
|
|
17916
|
-
expectedVersion: release.tag_name.replace(/^v/, ""),
|
|
17917
|
-
expectedVariant: "main",
|
|
17918
|
-
replaceVariant,
|
|
17919
|
-
log,
|
|
17920
|
-
warn
|
|
17921
|
-
});
|
|
17922
|
-
if (result.installed) {
|
|
17923
|
-
log(`Installed to ${result.destination}`);
|
|
17924
|
-
} else {
|
|
17925
|
-
log(`${ASSET_NAME} already installed.`);
|
|
17926
18490
|
}
|
|
18491
|
+
return { installBundledPlugin: installBundledPlugin2, installPlugin: installPlugin2 };
|
|
18492
|
+
}
|
|
18493
|
+
|
|
18494
|
+
// src/index.ts
|
|
18495
|
+
import { createRequire } from "module";
|
|
18496
|
+
|
|
18497
|
+
// src/install-plugin.ts
|
|
18498
|
+
var installer = createPluginInstaller(import.meta.url, "main");
|
|
18499
|
+
async function installBundledPlugin(options = {}) {
|
|
18500
|
+
await installer.installBundledPlugin(options);
|
|
18501
|
+
}
|
|
18502
|
+
async function installPlugin(options = {}) {
|
|
18503
|
+
await installer.installPlugin(options, options.dev ?? process.argv.includes("--dev"));
|
|
17927
18504
|
}
|
|
17928
18505
|
|
|
17929
18506
|
// src/index.ts
|