@el4cteo/rbx-studio-mcp 0.6.5 → 0.6.8

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.
Files changed (46) hide show
  1. package/README.md +28 -2
  2. package/dist/bridge/console.js +182 -0
  3. package/dist/bridge/console.js.map +1 -1
  4. package/dist/index.js +4 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/cloudassets.js +233 -0
  7. package/dist/lib/cloudassets.js.map +1 -0
  8. package/dist/lib/credentials.js +180 -0
  9. package/dist/lib/credentials.js.map +1 -0
  10. package/dist/lib/livedata.js +325 -0
  11. package/dist/lib/livedata.js.map +1 -0
  12. package/dist/lib/liveluau.js +83 -0
  13. package/dist/lib/liveluau.js.map +1 -0
  14. package/dist/lib/liveops.js +358 -0
  15. package/dist/lib/liveops.js.map +1 -0
  16. package/dist/lib/opencloud.js +290 -0
  17. package/dist/lib/opencloud.js.map +1 -0
  18. package/dist/tools/audio.js +96 -0
  19. package/dist/tools/audio.js.map +1 -0
  20. package/dist/tools/data.js +133 -3
  21. package/dist/tools/data.js.map +1 -1
  22. package/dist/tools/exec.js +56 -1
  23. package/dist/tools/exec.js.map +1 -1
  24. package/dist/tools/scripts.js +124 -4
  25. package/dist/tools/scripts.js.map +1 -1
  26. package/dist/tools/spatial.js +135 -0
  27. package/dist/tools/spatial.js.map +1 -0
  28. package/dist/tools/universe.js +182 -0
  29. package/dist/tools/universe.js.map +1 -0
  30. package/dist/tools/upload.js +294 -0
  31. package/dist/tools/upload.js.map +1 -0
  32. package/dist/tools/world.js +361 -10
  33. package/dist/tools/world.js.map +1 -1
  34. package/package.json +74 -74
  35. package/plugin/src/Commands.luau +646 -622
  36. package/plugin/src/Config.luau +65 -65
  37. package/plugin/src/Phrase.luau +816 -766
  38. package/plugin/src/Prompt.luau +965 -961
  39. package/plugin/src/Secret.luau +86 -0
  40. package/plugin/src/Serialize.luau +759 -499
  41. package/plugin/src/handlers/Assets.luau +636 -587
  42. package/plugin/src/handlers/Audio.luau +411 -0
  43. package/plugin/src/handlers/Geometry.luau +722 -577
  44. package/plugin/src/handlers/Instances.luau +84 -4
  45. package/plugin/src/handlers/Spatial.luau +334 -0
  46. package/plugin/src/init.server.luau +883 -879
@@ -0,0 +1,180 @@
1
+ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ /**
5
+ * Where the Open Cloud key lives, and the rules about handling it.
6
+ *
7
+ * A Roblox API key with `assets:write` can publish assets under the user's own
8
+ * name, and Roblox counts those against a real monthly quota. So it is treated
9
+ * as a password everywhere in this file: it is never returned to a caller, never
10
+ * logged, never put in a tool result, and never written anywhere that travels.
11
+ *
12
+ * Not `plugin:SetSetting`, which was the obvious place to put it. Plugin
13
+ * settings are stored per place and move with the .rbxl: into version control,
14
+ * into a Team Create session, to whoever the place is shared with. A file under
15
+ * the user's own home directory does not move at all.
16
+ *
17
+ * The environment still wins over the file. Someone running this in CI, or
18
+ * sharing one machine, wants the key to come from the process they started and
19
+ * to leave nothing behind — so `ROBLOX_API_KEY` is checked first and the file is
20
+ * only consulted when it is unset.
21
+ */
22
+ const DIRECTORY = join(homedir(), ".rbx-studio-mcp");
23
+ const FILE = join(DIRECTORY, "credentials.json");
24
+ async function readStored() {
25
+ try {
26
+ return JSON.parse(await readFile(FILE, "utf8"));
27
+ }
28
+ catch {
29
+ // Absent, unreadable or corrupt all mean the same thing to every caller:
30
+ // nothing is stored. A parse error is not worth failing a tool over.
31
+ return {};
32
+ }
33
+ }
34
+ async function writeStored(next) {
35
+ await mkdir(DIRECTORY, { recursive: true, mode: 0o700 });
36
+ /**
37
+ * Written, then narrowed.
38
+ *
39
+ * `mode` on writeFile is masked by the process umask, so it is not on its own
40
+ * a guarantee; chmod after the fact is. On Windows this is close to a no-op —
41
+ * Node maps it to the read-only flag — and the protection there comes from
42
+ * the file sitting inside the user's profile, which is ACL'd to that user.
43
+ * Worth doing anyway: this server runs on macOS and Linux too, where a
44
+ * world-readable key in a home directory is a real exposure.
45
+ */
46
+ await writeFile(FILE, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
47
+ await chmod(FILE, 0o600).catch(() => {
48
+ // Best effort: a filesystem that does not do permissions is not a reason to
49
+ // refuse to save.
50
+ });
51
+ }
52
+ /**
53
+ * The credentials to upload with, or null when the setup is incomplete.
54
+ *
55
+ * A key with no creator is incomplete, not usable: Roblox rejects the upload
56
+ * with a permissions error that names neither, which is a much worse thing to
57
+ * debug than being told up front.
58
+ */
59
+ export async function loadCredentials() {
60
+ const stored = await readStored();
61
+ const envKey = process.env["ROBLOX_API_KEY"]?.trim();
62
+ const envUser = process.env["ROBLOX_USER_ID"]?.trim();
63
+ const envGroup = process.env["ROBLOX_GROUP_ID"]?.trim();
64
+ /**
65
+ * The universe and place fall back to the file even when the key comes from
66
+ * the environment.
67
+ *
68
+ * They are not secrets and they are not part of the credential: someone who
69
+ * exports a key in CI has still told the panel which game they are working
70
+ * on, and making them re-export two public ids to keep that would be a
71
+ * papercut with nothing behind it.
72
+ */
73
+ const universeId = process.env["ROBLOX_UNIVERSE_ID"]?.trim() || stored.universeId;
74
+ const placeId = process.env["ROBLOX_PLACE_ID"]?.trim() || stored.placeId;
75
+ const apiKey = envKey || stored.apiKey;
76
+ if (!apiKey)
77
+ return null;
78
+ const source = envKey ? "environment" : "file";
79
+ const groupId = envKey ? envGroup : stored.groupId;
80
+ const userId = envKey ? envUser : stored.userId;
81
+ if (!groupId && !userId)
82
+ return null;
83
+ return {
84
+ apiKey,
85
+ creatorId: (groupId || userId),
86
+ creatorField: groupId ? "groupId" : "userId",
87
+ universeId,
88
+ placeId,
89
+ source,
90
+ };
91
+ }
92
+ /** What is configured, without the key itself. */
93
+ export async function credentialStatus() {
94
+ const envKey = process.env["ROBLOX_API_KEY"]?.trim();
95
+ const envUser = process.env["ROBLOX_USER_ID"]?.trim();
96
+ const envGroup = process.env["ROBLOX_GROUP_ID"]?.trim();
97
+ const stored = await readStored();
98
+ const apiKey = envKey || stored.apiKey;
99
+ const source = envKey ? "environment" : stored.apiKey ? "file" : null;
100
+ const groupId = envKey ? envGroup : stored.groupId;
101
+ const userId = envKey ? envUser : stored.userId;
102
+ return {
103
+ hasKey: Boolean(apiKey),
104
+ keyHint: apiKey ? apiKey.slice(-4) : null,
105
+ creatorId: groupId || userId || null,
106
+ creatorField: groupId ? "groupId" : userId ? "userId" : null,
107
+ universeId: process.env["ROBLOX_UNIVERSE_ID"]?.trim() || stored.universeId || null,
108
+ placeId: process.env["ROBLOX_PLACE_ID"]?.trim() || stored.placeId || null,
109
+ source,
110
+ path: FILE,
111
+ };
112
+ }
113
+ /** Remembers which game the universe-scoped calls act on. */
114
+ export async function saveTarget(field, id) {
115
+ const stored = await readStored();
116
+ stored[field] = id;
117
+ await writeStored(stored);
118
+ }
119
+ export async function saveApiKey(apiKey) {
120
+ const stored = await readStored();
121
+ stored.apiKey = apiKey;
122
+ await writeStored(stored);
123
+ }
124
+ /**
125
+ * Sets the creator, clearing the other kind.
126
+ *
127
+ * Both at once is not a state worth allowing: Roblox takes one creator per
128
+ * upload, and a leftover userId sitting under a groupId is the kind of thing
129
+ * that silently publishes to the wrong account months later.
130
+ */
131
+ export async function saveCreator(field, id) {
132
+ const stored = await readStored();
133
+ delete stored.userId;
134
+ delete stored.groupId;
135
+ stored[field] = id;
136
+ await writeStored(stored);
137
+ }
138
+ /** Deletes the file outright. Nothing is left behind to un-delete. */
139
+ export async function forgetCredentials() {
140
+ try {
141
+ await rm(FILE);
142
+ return true;
143
+ }
144
+ catch {
145
+ return false;
146
+ }
147
+ }
148
+ /**
149
+ * Asks Roblox whether the key works, without uploading anything.
150
+ *
151
+ * There is no "who am I" endpoint for an API key, so this calls the operations
152
+ * endpoint with an id that cannot exist. A working key gets 404 (or 400); a bad
153
+ * one gets 401 or 403 before Roblox ever looks at the id. That distinction is
154
+ * the whole test, and it costs no quota.
155
+ */
156
+ export async function testCredentials(apiKey) {
157
+ let response;
158
+ try {
159
+ response = await fetch("https://apis.roblox.com/assets/v1/operations/0", {
160
+ headers: { "x-api-key": apiKey },
161
+ });
162
+ }
163
+ catch (cause) {
164
+ return { ok: false, detail: `could not reach Roblox: ${cause.message}` };
165
+ }
166
+ if (response.status === 401)
167
+ return { ok: false, detail: "Roblox rejected the key (401)" };
168
+ if (response.status === 403) {
169
+ return {
170
+ ok: false,
171
+ detail: "Roblox refused the key (403) — usually a missing `assets` permission, " +
172
+ "or your IP is not in the key's allowed list",
173
+ };
174
+ }
175
+ if (response.status === 429)
176
+ return { ok: true, detail: "the key works (rate limited right now)" };
177
+ return { ok: true, detail: "the key works" };
178
+ }
179
+ export { FILE as CREDENTIALS_PATH };
180
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/lib/credentials.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,325 @@
1
+ import { ToolError } from "./errors.js";
2
+ import { call, paged } from "./opencloud.js";
3
+ /**
4
+ * Data stores of the PUBLISHED game, over Open Cloud.
5
+ *
6
+ * The `datastore` tool already reads data stores — through Studio, which reads
7
+ * whatever the open place is connected to. That is the right thing while
8
+ * building and the wrong thing when a player reports lost progress, because
9
+ * Studio's view depends on which place is open and on "Enable Studio Access to
10
+ * API Services" being ticked. This path talks to Roblox directly and always
11
+ * sees exactly what the live servers see.
12
+ *
13
+ * Kept behind the same tool and the same five verbs rather than a new one. The
14
+ * question "what did this player save" does not change because the answer comes
15
+ * over HTTP, and a second tool would mean an agent picking between two spellings
16
+ * of one idea.
17
+ *
18
+ * Two differences from the engine API are load-bearing and are surfaced rather
19
+ * than hidden:
20
+ *
21
+ * 1. Creating and updating are separate permissions. `SetAsync` creates or
22
+ * overwrites; here a write to a missing key is refused unless it is asked
23
+ * for, which is what makes a support tool safe to hand out.
24
+ * 2. Values are JSON, not Luau. Roblox does no serialising for us, and a value
25
+ * that was a Luau table arrives as a JSON object.
26
+ */
27
+ /** Percent-encodes one path segment. Key names routinely contain slashes. */
28
+ function segment(value) {
29
+ return encodeURIComponent(value);
30
+ }
31
+ /**
32
+ * Builds the entries path, with or without a scope.
33
+ *
34
+ * Open Cloud has two shapes rather than a scope parameter: omitting the scope
35
+ * is not "the default scope", it means "all scopes". A game that uses scopes
36
+ * and a game that does not therefore need genuinely different URLs, and getting
37
+ * this wrong reads as the store being empty.
38
+ */
39
+ function entriesPath(universe, store, scope) {
40
+ const base = `/cloud/v2/universes/${segment(universe)}/data-stores/${segment(store)}`;
41
+ return scope && scope !== ""
42
+ ? `${base}/scopes/${segment(scope)}/entries`
43
+ : `${base}/entries`;
44
+ }
45
+ function orderedPath(universe, store, scope) {
46
+ return (`/cloud/v2/universes/${segment(universe)}` +
47
+ `/ordered-data-stores/${segment(store)}/scopes/${segment(scope)}/entries`);
48
+ }
49
+ /** The trailing id of a resource path, which is the only part a human wants. */
50
+ function idOf(path) {
51
+ if (!path)
52
+ return "";
53
+ const parts = path.split("/");
54
+ return decodeURIComponent(parts[parts.length - 1] ?? "");
55
+ }
56
+ function parseValue(raw) {
57
+ if (raw === undefined) {
58
+ throw new ToolError("BAD_PARAMS", "set needs a `value`.");
59
+ }
60
+ try {
61
+ return JSON.parse(raw);
62
+ }
63
+ catch {
64
+ /**
65
+ * A bare string is valid JSON only when quoted, and people write
66
+ * `value: "hello"` meaning the string. Accepting it is friendlier than a
67
+ * parse error — but only after real JSON has been tried, so `{"a":1}` is
68
+ * never stored as the eleven characters that spell it.
69
+ */
70
+ return raw;
71
+ }
72
+ }
73
+ export async function liveDataStore(credentials, args) {
74
+ const { universe, op, kind } = args;
75
+ if (kind === "ordered")
76
+ return orderedStore(credentials, args);
77
+ if (op === "list" && !args.store) {
78
+ const { items, truncated } = await paged(credentials, {
79
+ path: `/cloud/v2/universes/${segment(universe)}/data-stores`,
80
+ scope: "universe-datastores.control:list",
81
+ field: "dataStores",
82
+ limit: args.limit,
83
+ query: { pageToken: args.cursor },
84
+ });
85
+ return {
86
+ target: "live",
87
+ universe,
88
+ items: items.map((store) => ({
89
+ store: idOf(store.path),
90
+ created: store.createTime,
91
+ state: store.state,
92
+ })),
93
+ count: items.length,
94
+ truncated,
95
+ };
96
+ }
97
+ if (!args.store) {
98
+ throw new ToolError("BAD_PARAMS", `live ${op} needs a \`store\`.`);
99
+ }
100
+ const base = entriesPath(universe, args.store, args.scope);
101
+ if (op === "list") {
102
+ const { items, truncated } = await paged(credentials, {
103
+ path: base,
104
+ scope: "universe-datastores.objects:list",
105
+ field: "dataStoreEntries",
106
+ limit: args.limit,
107
+ query: { pageToken: args.cursor },
108
+ });
109
+ return {
110
+ target: "live",
111
+ store: args.store,
112
+ items: items.map((entry) => ({ key: idOf(entry.path), state: entry.state })),
113
+ count: items.length,
114
+ truncated,
115
+ };
116
+ }
117
+ if (!args.key)
118
+ throw new ToolError("BAD_PARAMS", `live ${op} needs a \`key\`.`);
119
+ const entry = `${base}/${segment(args.key)}`;
120
+ if (op === "versions") {
121
+ const { items, truncated } = await paged(credentials, {
122
+ path: `${entry}:listRevisions`,
123
+ scope: "universe-datastores.versions:list",
124
+ field: "dataStoreEntries",
125
+ limit: args.limit,
126
+ query: { pageToken: args.cursor },
127
+ });
128
+ return {
129
+ target: "live",
130
+ store: args.store,
131
+ key: args.key,
132
+ items: items.map((revision) => ({
133
+ version: revision.revisionId,
134
+ created: revision.revisionCreateTime,
135
+ deleted: revision.state === "DELETED",
136
+ })),
137
+ count: items.length,
138
+ truncated,
139
+ };
140
+ }
141
+ if (op === "get") {
142
+ const found = await call(credentials, {
143
+ path: entry,
144
+ scope: "universe-datastores.objects:read",
145
+ });
146
+ return {
147
+ target: "live",
148
+ store: args.store,
149
+ key: args.key,
150
+ exists: true,
151
+ value: found.value,
152
+ version: found.revisionId,
153
+ updated: found.revisionCreateTime,
154
+ etag: found.etag,
155
+ };
156
+ }
157
+ if (op === "remove") {
158
+ await call(credentials, {
159
+ method: "DELETE",
160
+ path: entry,
161
+ scope: "universe-datastores.objects:delete",
162
+ });
163
+ return { target: "live", store: args.store, key: args.key, removed: true };
164
+ }
165
+ if (op === "increment") {
166
+ if (typeof args.amount !== "number") {
167
+ throw new ToolError("BAD_PARAMS", "increment needs an `amount`.");
168
+ }
169
+ const result = await call(credentials, {
170
+ method: "POST",
171
+ path: `${entry}:increment`,
172
+ body: { amount: args.amount },
173
+ scope: "universe-datastores.objects:update",
174
+ });
175
+ return {
176
+ target: "live",
177
+ store: args.store,
178
+ key: args.key,
179
+ value: result.value,
180
+ version: result.revisionId,
181
+ };
182
+ }
183
+ /**
184
+ * `allowMissing` is the create/update split made visible.
185
+ *
186
+ * Off by default so a typo in a key name fails instead of quietly creating a
187
+ * second, empty save beside the real one — the failure mode that makes people
188
+ * think a player's data "reset".
189
+ */
190
+ const written = await call(credentials, {
191
+ method: "PATCH",
192
+ path: entry,
193
+ query: { allowMissing: args.create ? "true" : undefined },
194
+ body: { value: parseValue(args.value) },
195
+ scope: "universe-datastores.objects:update",
196
+ });
197
+ return {
198
+ target: "live",
199
+ store: args.store,
200
+ key: args.key,
201
+ written: true,
202
+ version: written.revisionId,
203
+ };
204
+ }
205
+ /**
206
+ * Ordered data stores: the leaderboard backend.
207
+ *
208
+ * A separate service with a separate scope, not a flavour of the one above —
209
+ * values are numbers only, entries are always sorted, and there is no version
210
+ * history. It lives here rather than in its own file because from the caller's
211
+ * side it is the same five verbs against the same game.
212
+ */
213
+ async function orderedStore(credentials, args) {
214
+ if (!args.store)
215
+ throw new ToolError("BAD_PARAMS", "ordered stores need a `store`.");
216
+ // Roblox requires a scope in the URL and every SDK defaults it to "global";
217
+ // an ordered store written by an engine script with no scope lands there.
218
+ const scope = args.scope && args.scope !== "" ? args.scope : "global";
219
+ const base = orderedPath(args.universe, args.store, scope);
220
+ if (args.op === "list") {
221
+ const { items, truncated } = await paged(credentials, {
222
+ path: base,
223
+ scope: "universe.ordered-data-store.scope.entry:read",
224
+ field: "orderedDataStoreEntries",
225
+ limit: args.limit,
226
+ // Descending: a leaderboard is read from the top, and asking for the
227
+ // bottom of one is the rarer case.
228
+ query: { orderBy: "value desc", pageToken: args.cursor },
229
+ });
230
+ return {
231
+ target: "live",
232
+ kind: "ordered",
233
+ store: args.store,
234
+ scope,
235
+ items: items.map((entry, index) => ({
236
+ rank: index + 1,
237
+ key: entry.id ?? idOf(entry.path),
238
+ value: entry.value,
239
+ })),
240
+ count: items.length,
241
+ truncated,
242
+ };
243
+ }
244
+ if (!args.key)
245
+ throw new ToolError("BAD_PARAMS", `ordered ${args.op} needs a \`key\`.`);
246
+ const entry = `${base}/${segment(args.key)}`;
247
+ if (args.op === "get") {
248
+ const found = await call(credentials, {
249
+ path: entry,
250
+ scope: "universe.ordered-data-store.scope.entry:read",
251
+ });
252
+ return { target: "live", kind: "ordered", store: args.store, scope, key: args.key, value: found.value };
253
+ }
254
+ if (args.op === "remove") {
255
+ await call(credentials, {
256
+ method: "DELETE",
257
+ path: entry,
258
+ scope: "universe.ordered-data-store.scope.entry:write",
259
+ });
260
+ return { target: "live", kind: "ordered", store: args.store, key: args.key, removed: true };
261
+ }
262
+ if (args.op === "increment") {
263
+ if (typeof args.amount !== "number") {
264
+ throw new ToolError("BAD_PARAMS", "increment needs an `amount`.");
265
+ }
266
+ const result = await call(credentials, {
267
+ method: "POST",
268
+ path: `${entry}:increment`,
269
+ body: { amount: args.amount },
270
+ scope: "universe.ordered-data-store.scope.entry:write",
271
+ });
272
+ return { target: "live", kind: "ordered", store: args.store, key: args.key, value: result.value };
273
+ }
274
+ if (args.op === "versions") {
275
+ throw new ToolError("UNSUPPORTED", "Ordered data stores keep no version history.", 'Use kind="data" for anything that needs to be recoverable.');
276
+ }
277
+ const numeric = Number(args.value);
278
+ if (!Number.isFinite(numeric)) {
279
+ throw new ToolError("BAD_PARAMS", `An ordered data store holds numbers; "${args.value}" is not one.`);
280
+ }
281
+ /**
282
+ * Update first, create on 404.
283
+ *
284
+ * Open Cloud splits the two and the caller usually does not know which they
285
+ * need — setting a player's score works the same whether or not they have one
286
+ * yet. Trying the safer verb first keeps the create path from being the
287
+ * default, which is what stops a typo'd key becoming a new leaderboard row.
288
+ */
289
+ try {
290
+ const patched = await call(credentials, {
291
+ method: "PATCH",
292
+ path: entry,
293
+ body: { value: numeric },
294
+ scope: "universe.ordered-data-store.scope.entry:write",
295
+ });
296
+ return {
297
+ target: "live",
298
+ kind: "ordered",
299
+ store: args.store,
300
+ key: args.key,
301
+ value: patched.value,
302
+ created: false,
303
+ };
304
+ }
305
+ catch (cause) {
306
+ if (!(cause instanceof ToolError) || cause.code !== "NOT_FOUND")
307
+ throw cause;
308
+ const made = await call(credentials, {
309
+ method: "POST",
310
+ path: base,
311
+ query: { id: args.key },
312
+ body: { value: numeric },
313
+ scope: "universe.ordered-data-store.scope.entry:write",
314
+ });
315
+ return {
316
+ target: "live",
317
+ kind: "ordered",
318
+ store: args.store,
319
+ key: args.key,
320
+ value: made.value,
321
+ created: true,
322
+ };
323
+ }
324
+ }
325
+ //# sourceMappingURL=livedata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"livedata.js","sourceRoot":"","sources":["../../src/lib/livedata.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ import { ToolError } from "./errors.js";
2
+ import { call } from "./opencloud.js";
3
+ /**
4
+ * Running Luau against the PUBLISHED game, over Open Cloud.
5
+ *
6
+ * `execute_luau` already runs code — inside Studio, against the place someone
7
+ * has open. This runs it on Roblox's own servers against the published place,
8
+ * with no Studio involved at all. That is a different question with the same
9
+ * shape: "what does this code do here" versus "what does this code do in
10
+ * production", and it is the only way to answer the second one.
11
+ *
12
+ * Roblox models it as a task rather than a request: you create one, it queues,
13
+ * a server picks it up, and the result appears later. So this submits and then
14
+ * polls, and reports the state it ended in rather than pretending it was
15
+ * synchronous.
16
+ *
17
+ * It is genuinely dangerous in a way the Studio path is not. Studio has an undo
18
+ * stack and a place file nobody is playing; this touches live data stores and
19
+ * live players, and nothing here can put any of it back. The tool gates it.
20
+ */
21
+ const POLL_INTERVAL_MS = 2_000;
22
+ export async function runLiveLuau(credentials, args) {
23
+ const started = await call(credentials, {
24
+ method: "POST",
25
+ path: `/cloud/v2/universes/${encodeURIComponent(args.universeId)}` +
26
+ `/places/${encodeURIComponent(args.placeId)}/luau-execution-session-tasks`,
27
+ body: { script: args.source, timeout: `${args.timeoutSeconds}s` },
28
+ scope: "universe.place.luau-execution-session:write",
29
+ });
30
+ if (!started.path) {
31
+ throw new ToolError("NO_TASK", "Roblox accepted the script but did not say where to track it.");
32
+ }
33
+ /**
34
+ * Waited on for the script's own timeout plus a margin.
35
+ *
36
+ * The margin is for the queue: a task is not running the moment it is
37
+ * created, and giving up at exactly the script timeout would report a
38
+ * failure for a script that had not started yet.
39
+ */
40
+ const deadline = Date.now() + (args.timeoutSeconds + 30) * 1000;
41
+ let task = started;
42
+ while (task.state === "QUEUED" || task.state === "PROCESSING" || task.state === undefined) {
43
+ if (Date.now() >= deadline) {
44
+ return {
45
+ target: "live",
46
+ state: task.state ?? "UNKNOWN",
47
+ task: started.path,
48
+ note: "Gave up waiting. The task may still be running on Roblox's side — " +
49
+ "it was not cancelled.",
50
+ };
51
+ }
52
+ await new Promise((done) => setTimeout(done, POLL_INTERVAL_MS));
53
+ task = await call(credentials, {
54
+ path: `/cloud/v2/${started.path}`,
55
+ query: { view: "FULL" },
56
+ scope: "universe.place.luau-execution-session:write",
57
+ });
58
+ }
59
+ // Logs are a separate resource from the result, and they hold everything the
60
+ // script printed — which for a diagnostic script is the whole point.
61
+ let logs = [];
62
+ try {
63
+ const page = await call(credentials, {
64
+ path: `/cloud/v2/${started.path}/logs`,
65
+ query: { maxPageSize: 100 },
66
+ scope: "universe.place.luau-execution-session:write",
67
+ });
68
+ logs = (page.luauExecutionSessionTaskLogs ?? []).flatMap((entry) => entry.messages ?? []);
69
+ }
70
+ catch {
71
+ // A missing log page is not worth failing a successful run over.
72
+ }
73
+ return {
74
+ target: "live",
75
+ universeId: args.universeId,
76
+ placeId: args.placeId,
77
+ state: task.state,
78
+ results: task.output?.results,
79
+ logs,
80
+ error: task.error ? `${task.error.code ?? "error"}: ${task.error.message ?? ""}` : undefined,
81
+ };
82
+ }
83
+ //# sourceMappingURL=liveluau.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"liveluau.js","sourceRoot":"","sources":["../../src/lib/liveluau.ts"],"names":[],"mappings":"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"}