@el4cteo/rbx-studio-mcp 0.6.1 → 0.6.7

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 (75) 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 +8 -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 +235 -0
  17. package/dist/lib/opencloud.js.map +1 -0
  18. package/dist/tools/anim.js +159 -0
  19. package/dist/tools/anim.js.map +1 -0
  20. package/dist/tools/audio.js +96 -0
  21. package/dist/tools/audio.js.map +1 -0
  22. package/dist/tools/character.js +95 -5
  23. package/dist/tools/character.js.map +1 -1
  24. package/dist/tools/data.js +292 -0
  25. package/dist/tools/data.js.map +1 -0
  26. package/dist/tools/device.js +77 -7
  27. package/dist/tools/device.js.map +1 -1
  28. package/dist/tools/discover.js +80 -4
  29. package/dist/tools/discover.js.map +1 -1
  30. package/dist/tools/exec.js +96 -2
  31. package/dist/tools/exec.js.map +1 -1
  32. package/dist/tools/input.js +35 -9
  33. package/dist/tools/input.js.map +1 -1
  34. package/dist/tools/perf.js +74 -7
  35. package/dist/tools/perf.js.map +1 -1
  36. package/dist/tools/scripts.js +162 -6
  37. package/dist/tools/scripts.js.map +1 -1
  38. package/dist/tools/spatial.js +135 -0
  39. package/dist/tools/spatial.js.map +1 -0
  40. package/dist/tools/universe.js +177 -0
  41. package/dist/tools/universe.js.map +1 -0
  42. package/dist/tools/upload.js +294 -0
  43. package/dist/tools/upload.js.map +1 -0
  44. package/dist/tools/world.js +675 -51
  45. package/dist/tools/world.js.map +1 -1
  46. package/package.json +2 -2
  47. package/plugin/src/Commands.luau +31 -7
  48. package/plugin/src/Config.luau +65 -65
  49. package/plugin/src/Console.luau +1909 -1843
  50. package/plugin/src/Emulation.luau +172 -0
  51. package/plugin/src/Phrase.luau +816 -618
  52. package/plugin/src/Png.luau +8 -4
  53. package/plugin/src/Prompt.luau +965 -961
  54. package/plugin/src/Secret.luau +86 -0
  55. package/plugin/src/Serialize.luau +440 -8
  56. package/plugin/src/Undo.luau +94 -6
  57. package/plugin/src/handlers/Anim.luau +897 -0
  58. package/plugin/src/handlers/Assets.luau +286 -2
  59. package/plugin/src/handlers/Audio.luau +411 -0
  60. package/plugin/src/handlers/Capture.luau +155 -20
  61. package/plugin/src/handlers/Character.luau +823 -361
  62. package/plugin/src/handlers/Data.luau +539 -0
  63. package/plugin/src/handlers/Device.luau +394 -139
  64. package/plugin/src/handlers/Discover.luau +685 -363
  65. package/plugin/src/handlers/Geometry.luau +722 -450
  66. package/plugin/src/handlers/Instances.luau +84 -4
  67. package/plugin/src/handlers/Perf.luau +227 -0
  68. package/plugin/src/handlers/Scripts.luau +673 -539
  69. package/plugin/src/handlers/Session.luau +3 -0
  70. package/plugin/src/handlers/Spatial.luau +334 -0
  71. package/plugin/src/handlers/Viewport.luau +268 -0
  72. package/plugin/src/handlers/World.luau +89 -15
  73. package/plugin/src/init.server.luau +9 -1
  74. package/scripts/build-plugin.mjs +20 -0
  75. package/scripts/check-plugin.mjs +171 -124
@@ -0,0 +1,358 @@
1
+ import { ToolError } from "./errors.js";
2
+ import { call, paged } from "./opencloud.js";
3
+ /**
4
+ * Operating the published experience, over Open Cloud.
5
+ *
6
+ * Everything here acts on the game as it exists on Roblox rather than on the
7
+ * place someone has open in Studio: its scripts, its servers, its players, its
8
+ * saved data. That is a different job from building, and the failure modes are
9
+ * different too — there is no undo stack and the audience is real.
10
+ *
11
+ * Two things in here are long-running operations rather than requests. Roblox
12
+ * answers the Instance calls with an `Operation` to poll, because reaching into
13
+ * a place file is not instant. `awaitOperation` hides that; nothing else should
14
+ * have to know.
15
+ */
16
+ const OPERATION_INTERVAL_MS = 1_000;
17
+ const OPERATION_TIMEOUT_MS = 60_000;
18
+ /**
19
+ * Polls an Operation to completion and returns its payload.
20
+ *
21
+ * The error case is worth naming rather than passing through: a 422 here means
22
+ * the response exceeded Roblox's 500,000-byte cap, which is a real and
23
+ * recoverable situation (ask for fewer children, or a smaller script) and reads
24
+ * as nothing at all in the raw body.
25
+ */
26
+ async function awaitOperation(credentials, started, scope) {
27
+ let operation = started;
28
+ const deadline = Date.now() + OPERATION_TIMEOUT_MS;
29
+ while (!operation.done) {
30
+ if (!operation.path) {
31
+ throw new ToolError("NO_OPERATION", "Roblox did not say where to track this request.");
32
+ }
33
+ if (Date.now() >= deadline) {
34
+ throw new ToolError("TIMEOUT", "Roblox did not finish this request in time.");
35
+ }
36
+ await new Promise((done) => setTimeout(done, OPERATION_INTERVAL_MS));
37
+ operation = await call(credentials, {
38
+ path: `/cloud/v2/${operation.path}`,
39
+ scope,
40
+ });
41
+ }
42
+ if (operation.error) {
43
+ if (operation.error.code === 422) {
44
+ throw new ToolError("TOO_LARGE", "The response was over Roblox's 500,000-byte limit for this API.", "Ask for a smaller piece: fewer children at a time, or one script rather " +
45
+ "than a folder.");
46
+ }
47
+ throw new ToolError("OPERATION_FAILED", `Roblox refused: ${operation.error.message ?? operation.error.code ?? "no reason given"}`);
48
+ }
49
+ return operation.response ?? {};
50
+ }
51
+ /**
52
+ * The Instance API: scripts in the PUBLISHED place, without Studio.
53
+ *
54
+ * Narrower than it first looks, and the narrowness is the important part.
55
+ * Roblox's `InstanceDetails` covers exactly four classes — Folder, Script,
56
+ * LocalScript and ModuleScript — so this is not a live DataModel browser. It is
57
+ * a way to read and edit the code of a published place from outside Studio,
58
+ * which is why it belongs beside `script_read` and `script_edit` rather than
59
+ * beside `tree` and `modify`.
60
+ *
61
+ * It also acts on the saved place, not on a running server. Editing here
62
+ * changes what the next server to start will run; it does not change what a
63
+ * player in a server right now is running. `restartServers` is the other half
64
+ * of that, and they are meant to be used together.
65
+ */
66
+ const INSTANCE_SCOPE = "universe.place.instance:read";
67
+ const INSTANCE_WRITE = "universe.place.instance:write";
68
+ function instancePath(universe, place, instance) {
69
+ return (`/cloud/v2/universes/${encodeURIComponent(universe)}` +
70
+ `/places/${encodeURIComponent(place)}/instances/${encodeURIComponent(instance)}`);
71
+ }
72
+ /** Flattens one instance into the shape the script tools already speak. */
73
+ function describe(entry) {
74
+ const engine = entry.engineInstance ?? {};
75
+ const details = engine.Details ?? {};
76
+ const className = Object.keys(details)[0] ?? "Instance";
77
+ const body = details[className] ?? {};
78
+ return {
79
+ id: engine.Id,
80
+ name: engine.Name,
81
+ className,
82
+ hasChildren: entry.hasChildren ?? false,
83
+ ...(body.Source === undefined ? {} : { source: body.Source }),
84
+ ...(body.Enabled === undefined ? {} : { enabled: body.Enabled }),
85
+ ...(body.RunContext === undefined ? {} : { runContext: body.RunContext }),
86
+ };
87
+ }
88
+ export async function liveInstance(credentials, args) {
89
+ const started = await call(credentials, {
90
+ path: instancePath(args.universeId, args.placeId, args.instanceId),
91
+ scope: INSTANCE_SCOPE,
92
+ });
93
+ const done = await awaitOperation(credentials, started, INSTANCE_SCOPE);
94
+ return describe(done);
95
+ }
96
+ export async function liveChildren(credentials, args) {
97
+ const started = await call(credentials, {
98
+ path: `${instancePath(args.universeId, args.placeId, args.instanceId)}:listChildren`,
99
+ query: { maxPageSize: Math.min(args.limit, 100) },
100
+ scope: INSTANCE_SCOPE,
101
+ });
102
+ const done = await awaitOperation(credentials, started, INSTANCE_SCOPE);
103
+ const items = done["instances"] ?? [];
104
+ return {
105
+ parent: args.instanceId,
106
+ items: items.slice(0, args.limit).map(describe),
107
+ count: Math.min(items.length, args.limit),
108
+ };
109
+ }
110
+ /**
111
+ * Resolves a dot path to a cloud instance id by walking down from the root.
112
+ *
113
+ * The Instance API addresses things by GUID, not by name, and offers no search.
114
+ * So a path everyone already writes -- "ServerScriptService.Systems.Combat" --
115
+ * costs one listChildren per segment, and each of those is a long-running
116
+ * operation. Three segments is a few seconds.
117
+ *
118
+ * Worth paying rather than making callers hunt for GUIDs: every other tool here
119
+ * takes a path, and a live read that needed a different kind of address would
120
+ * be a different tool wearing this one's name.
121
+ */
122
+ export async function resolveLivePath(credentials, args) {
123
+ const segments = args.path.split(".").filter((part) => part !== "");
124
+ if (segments.length === 0) {
125
+ throw new ToolError("BAD_PARAMS", "A live script path cannot be empty.");
126
+ }
127
+ let current = "root";
128
+ let found = null;
129
+ for (const segment of segments) {
130
+ const children = await liveChildren(credentials, {
131
+ universeId: args.universeId,
132
+ placeId: args.placeId,
133
+ instanceId: current,
134
+ limit: 100,
135
+ });
136
+ const items = children["items"];
137
+ const match = items.find((entry) => entry["name"] === segment);
138
+ if (!match) {
139
+ const names = items
140
+ .slice(0, 12)
141
+ .map((entry) => String(entry["name"]))
142
+ .join(", ");
143
+ throw new ToolError("NOT_FOUND", `The published place has no "${segment}" under ${current === "root" ? "the root" : current}.`, names === ""
144
+ ? "Nothing is there. Note the Instance API only sees Folders and " +
145
+ "scripts, so a path through any other class cannot be walked."
146
+ : `What is there: ${names}. The Instance API only sees Folders and ` +
147
+ "scripts, so a path through a Model or a Part cannot be walked.");
148
+ }
149
+ current = String(match["id"]);
150
+ found = match;
151
+ }
152
+ return {
153
+ id: current,
154
+ className: String(found?.["className"] ?? "Instance"),
155
+ name: String(found?.["name"] ?? segments[segments.length - 1]),
156
+ };
157
+ }
158
+ export async function liveScriptWrite(credentials, args) {
159
+ // Roblox caps the source at 200,000 bytes AFTER UTF-8 encoding, so the check
160
+ // is on bytes rather than on characters — a script of emoji or non-Latin
161
+ // comments is several times longer than its length suggests.
162
+ const bytes = Buffer.byteLength(args.source, "utf8");
163
+ if (bytes > 200_000) {
164
+ throw new ToolError("TOO_LARGE", `That source is ${bytes} bytes; Roblox accepts 200,000 for a cloud script edit.`);
165
+ }
166
+ const started = await call(credentials, {
167
+ method: "PATCH",
168
+ path: instancePath(args.universeId, args.placeId, args.instanceId),
169
+ body: {
170
+ engineInstance: {
171
+ Details: { [args.className]: { Source: args.source } },
172
+ },
173
+ },
174
+ scope: INSTANCE_WRITE,
175
+ });
176
+ const done = await awaitOperation(credentials, started, INSTANCE_WRITE);
177
+ return {
178
+ ...describe(done),
179
+ written: true,
180
+ bytes,
181
+ note: "This changed the SAVED place. Servers already running still have the old " +
182
+ "code — restart them with `assets op=\"publish\" restart:true`, or wait for " +
183
+ "them to cycle.",
184
+ };
185
+ }
186
+ /**
187
+ * Rolls live servers onto the newly published version.
188
+ *
189
+ * Publishing alone changes nothing for anyone already playing: they stay on the
190
+ * server they are on, running the old code, until it empties. This is the step
191
+ * people forget, which is why it sits on the publish call rather than on its
192
+ * own.
193
+ *
194
+ * `bleedOff` is the humane default and is not the API's. Roblox defaults to
195
+ * shutting servers down immediately, which teleports players mid-game; bleeding
196
+ * off stops matchmaking and lets them finish.
197
+ */
198
+ export async function restartServers(credentials, args) {
199
+ const bleed = args.bleedOffMinutes ?? 10;
200
+ await call(credentials, {
201
+ method: "POST",
202
+ path: `/cloud/v2/universes/${encodeURIComponent(args.universeId)}:restartServers`,
203
+ body: {
204
+ placeIds: args.placeIds,
205
+ bleedOffServers: bleed > 0,
206
+ bleedOffDurationMinutes: bleed > 0 ? bleed : undefined,
207
+ },
208
+ scope: "universe:write",
209
+ });
210
+ return {
211
+ restarted: true,
212
+ universeId: args.universeId,
213
+ bleedOffMinutes: bleed,
214
+ note: bleed > 0
215
+ ? `Matchmaking to old servers has stopped; they shut down over the next ${bleed} ` +
216
+ "minutes so players can finish."
217
+ : "Servers are shutting down now. Anyone playing is being moved immediately.",
218
+ };
219
+ }
220
+ /** Sends a MessagingService message to every live server. */
221
+ export async function publishMessage(credentials, args) {
222
+ await call(credentials, {
223
+ method: "POST",
224
+ path: `/cloud/v2/universes/${encodeURIComponent(args.universeId)}:publishMessage`,
225
+ body: { topic: args.topic, message: args.message },
226
+ scope: "universe-messaging-service:publish",
227
+ });
228
+ return {
229
+ published: true,
230
+ topic: args.topic,
231
+ note: "Delivered only to servers with a SubscribeAsync listener on this exact " +
232
+ "topic. Nothing reports whether anything was listening, so a silent " +
233
+ "success does not mean it was received.",
234
+ };
235
+ }
236
+ /**
237
+ * Takes a data store snapshot for the whole experience.
238
+ *
239
+ * The safety net for every live write. Roblox allows one snapshot per
240
+ * experience per UTC day, and says in the response whether this call actually
241
+ * took one — which matters, because a second call on the same day reports
242
+ * success while doing nothing.
243
+ */
244
+ export async function snapshotDataStores(credentials, universeId) {
245
+ const result = await call(credentials, {
246
+ method: "POST",
247
+ path: `/cloud/v2/universes/${encodeURIComponent(universeId)}/data-stores:snapshot`,
248
+ body: {},
249
+ scope: "universe-datastores.control:snapshot",
250
+ });
251
+ return {
252
+ newSnapshotTaken: result.newSnapshotTaken === true,
253
+ latestSnapshotTime: result.latestSnapshotTime,
254
+ note: result.newSnapshotTaken === true
255
+ ? "Snapshot taken. Safe to make the change."
256
+ : "NO new snapshot was taken — Roblox allows one per experience per UTC " +
257
+ `day and one already exists (${result.latestSnapshotTime}). Anything ` +
258
+ "written since then is NOT covered.",
259
+ };
260
+ }
261
+ /** Bans or unbans a player, across the experience or one place. */
262
+ export async function setRestriction(credentials, args) {
263
+ const base = args.placeId
264
+ ? `/cloud/v2/universes/${encodeURIComponent(args.universeId)}/places/${encodeURIComponent(args.placeId)}`
265
+ : `/cloud/v2/universes/${encodeURIComponent(args.universeId)}`;
266
+ const restriction = { active: args.active };
267
+ if (args.active) {
268
+ // Omitted duration means permanent, which is the engine's rule and is worth
269
+ // making the caller state rather than defaulting to.
270
+ if (args.durationSeconds)
271
+ restriction["duration"] = `${args.durationSeconds}s`;
272
+ restriction["displayReason"] = args.displayReason ?? "";
273
+ restriction["privateReason"] = args.privateReason ?? "";
274
+ restriction["excludeAltAccounts"] = args.excludeAltAccounts ?? false;
275
+ }
276
+ const result = await call(credentials, {
277
+ method: "PATCH",
278
+ path: `${base}/user-restrictions/${encodeURIComponent(args.userId)}`,
279
+ body: { gameJoinRestriction: restriction },
280
+ scope: "universe.user-restriction:write",
281
+ });
282
+ return {
283
+ userId: args.userId,
284
+ banned: args.active,
285
+ scope: args.placeId ? `place ${args.placeId}` : `universe ${args.universeId}`,
286
+ duration: args.durationSeconds ? `${args.durationSeconds}s` : args.active ? "permanent" : null,
287
+ restriction: result.gameJoinRestriction,
288
+ };
289
+ }
290
+ export async function listRestrictions(credentials, args) {
291
+ const { items } = await paged(credentials, {
292
+ path: `/cloud/v2/universes/${encodeURIComponent(args.universeId)}/user-restrictions`,
293
+ scope: "universe.user-restriction:read",
294
+ field: "userRestrictions",
295
+ limit: args.limit,
296
+ });
297
+ return {
298
+ items: items.map((entry) => ({
299
+ user: entry.user?.replace("users/", ""),
300
+ active: entry.gameJoinRestriction?.active ?? false,
301
+ duration: entry.gameJoinRestriction?.duration ?? "permanent",
302
+ reason: entry.gameJoinRestriction?.displayReason,
303
+ updated: entry.updateTime,
304
+ })),
305
+ count: items.length,
306
+ };
307
+ }
308
+ /** Looks up a user: the id-to-name step almost every other call needs. */
309
+ export async function getUser(credentials, userId) {
310
+ return call(credentials, {
311
+ path: `/cloud/v2/users/${encodeURIComponent(userId)}`,
312
+ scope: "user.advanced:read",
313
+ });
314
+ }
315
+ /** What a user owns: passes, badges, assets, private servers. */
316
+ export async function getInventory(credentials, args) {
317
+ const { items } = await paged(credentials, {
318
+ path: `/cloud/v2/users/${encodeURIComponent(args.userId)}/inventory-items`,
319
+ query: { filter: args.filter },
320
+ scope: "user.inventory-item:read",
321
+ field: "inventoryItems",
322
+ limit: args.limit,
323
+ });
324
+ return { userId: args.userId, items, count: items.length };
325
+ }
326
+ /**
327
+ * How many uploads are left before Roblox starts refusing them.
328
+ *
329
+ * Audio is the one that bites: ten a month unverified. Worth checking before a
330
+ * batch rather than discovering it on the eleventh.
331
+ */
332
+ export async function assetQuotas(credentials) {
333
+ const { items } = await paged(credentials, {
334
+ path: `/cloud/v2/users/${encodeURIComponent(credentials.creatorId)}/asset-quotas`,
335
+ scope: "assets:write",
336
+ field: "assetQuotas",
337
+ limit: 50,
338
+ });
339
+ return {
340
+ items: items
341
+ .filter((entry) => entry.quotaType === "RATE_LIMIT_UPLOAD")
342
+ .map((entry) => {
343
+ // Roblox sends capacity as a STRING ("2000"). Subtracting from it works
344
+ // by coercion and would stop working the day they send a number, so it
345
+ // is converted rather than relied upon.
346
+ const capacity = entry.capacity === undefined ? undefined : Number(entry.capacity);
347
+ const used = Number(entry.usage ?? 0);
348
+ return {
349
+ assetType: entry.assetType,
350
+ used,
351
+ capacity,
352
+ remaining: capacity === undefined ? undefined : capacity - used,
353
+ resets: entry.expirationTime,
354
+ };
355
+ }),
356
+ };
357
+ }
358
+ //# sourceMappingURL=liveops.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"liveops.js","sourceRoot":"","sources":["../../src/lib/liveops.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,235 @@
1
+ import { loadCredentials } from "./credentials.js";
2
+ import { ToolError } from "./errors.js";
3
+ /**
4
+ * One HTTP client for every Open Cloud call this server makes.
5
+ *
6
+ * Six features now talk to apis.roblox.com — uploads, asset grants, live data
7
+ * stores, ordered data stores, place publishing and remote Luau. They share an
8
+ * auth header, a set of failure modes and a pagination scheme, and the failure
9
+ * modes are the reason this is one file rather than six copies: Roblox answers
10
+ * a missing scope, a stale IP allowlist and a revoked key with the same 403 and
11
+ * a body that names none of them. Guessing wrong there costs an hour, so the
12
+ * guess is made once, here, with the scope that was actually needed in hand.
13
+ */
14
+ const BASE = "https://apis.roblox.com";
15
+ /**
16
+ * The credentials, or a ToolError explaining exactly what is missing.
17
+ *
18
+ * Thrown rather than returned. Every caller does the same thing with an
19
+ * incomplete setup — stop and say so — and an early return per call site is how
20
+ * one of them ends up forgetting.
21
+ */
22
+ export async function requireCredentials() {
23
+ const found = await loadCredentials();
24
+ if (found !== null)
25
+ return found;
26
+ throw new ToolError("NO_CREDENTIALS", "No Open Cloud API key is set up, so this cannot run.", "The user sets it once by typing `cloud` in the Studio panel — it walks " +
27
+ "through creating the key and storing it safely. Never ask them to paste " +
28
+ "a key into this conversation.");
29
+ }
30
+ /**
31
+ * The universe id, which every universe-scoped call needs and none can guess.
32
+ *
33
+ * Open Cloud is stateless: unlike the engine API it has no idea which game it
34
+ * is talking about, and the Studio session cannot supply it either — a place
35
+ * open in Studio knows its placeId but not the universe that contains it.
36
+ */
37
+ /**
38
+ * Turns a place id into the universe that contains it.
39
+ *
40
+ * Not part of Open Cloud and not in Roblox's OpenAPI document, but it is the
41
+ * endpoint Studio and every deploy tool uses, it needs no key, and it is the
42
+ * only way to answer the question at all: a place knows nothing about its own
43
+ * universe, and every universe-scoped call needs one. Verified against a live
44
+ * place rather than taken from a forum post.
45
+ *
46
+ * Treated as best-effort. If Roblox ever moves it, the caller falls back to
47
+ * asking the user, which is where they were before.
48
+ */
49
+ export async function universeForPlace(placeId) {
50
+ try {
51
+ const response = await fetch(`${BASE}/universes/v1/places/${encodeURIComponent(placeId)}/universe`);
52
+ if (!response.ok)
53
+ return null;
54
+ const body = (await response.json());
55
+ return body.universeId === undefined ? null : String(body.universeId);
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ export async function requireUniverse(explicit) {
62
+ if (explicit && explicit.trim() !== "")
63
+ return explicit.trim();
64
+ const found = await loadCredentials();
65
+ if (found?.universeId)
66
+ return found.universeId;
67
+ /**
68
+ * Derived from the place rather than demanded from the user.
69
+ *
70
+ * Someone who has told the panel which place they are working on has already
71
+ * said which game they mean; making them go and copy a second id off a web
72
+ * page to say it again is a papercut with nothing behind it.
73
+ */
74
+ if (found?.placeId) {
75
+ const derived = await universeForPlace(found.placeId);
76
+ if (derived)
77
+ return derived;
78
+ }
79
+ throw new ToolError("NO_UNIVERSE", "This needs a universe id and none is set.", "Ask the user to run `cloud universe <id>` in the Studio panel. They find " +
80
+ "it on the Creator Dashboard: hover the game's thumbnail, click the ⋯ " +
81
+ "button, Copy Universe ID. Or pass `universeId` to this call.");
82
+ }
83
+ export async function requirePlace(explicit) {
84
+ if (explicit && explicit.trim() !== "")
85
+ return explicit.trim();
86
+ const found = await loadCredentials();
87
+ if (found?.placeId)
88
+ return found.placeId;
89
+ throw new ToolError("NO_PLACE", "This needs a place id and none is set.", "Ask the user to run `cloud place <id>` in the Studio panel, or pass " +
90
+ "`placeId`. `studio_status` reports the placeId of the open place.");
91
+ }
92
+ /** Pulls something readable out of Roblox's several error body shapes. */
93
+ function detailOf(body) {
94
+ try {
95
+ const parsed = JSON.parse(body);
96
+ if (typeof parsed.error === "string")
97
+ return parsed.error;
98
+ return (parsed.message ??
99
+ (typeof parsed.error === "object" ? parsed.error?.message : undefined) ??
100
+ parsed.errors?.[0]?.message ??
101
+ body.slice(0, 300));
102
+ }
103
+ catch {
104
+ return body.slice(0, 300);
105
+ }
106
+ }
107
+ /**
108
+ * Turns an HTTP failure into an error a reader can act on.
109
+ *
110
+ * The 403 branch carries the whole value of this module. Roblox returns it for
111
+ * a key missing a scope, a key whose IP allowlist no longer matches, and a key
112
+ * with no access to that particular universe — three different fixes behind one
113
+ * status and a body that distinguishes none of them. Naming the scope the call
114
+ * needed turns "Forbidden" into a checklist.
115
+ */
116
+ function failure(status, body, scope) {
117
+ const detail = detailOf(body);
118
+ if (status === 401) {
119
+ return new ToolError("BAD_KEY", `Roblox rejected the API key (401): ${detail}`, "The key is wrong or was revoked. `cloud test` in the Studio panel " +
120
+ "re-checks it.");
121
+ }
122
+ if (status === 403) {
123
+ return new ToolError("FORBIDDEN", `Roblox refused the key (403): ${detail}`, `This call needs the \`${scope}\` permission. Three things cause a 403 ` +
124
+ "and the message names none of them: the key is missing that scope, " +
125
+ "the key's IP allowlist no longer includes this machine, or the key " +
126
+ "was not granted access to this experience. Check them in that order " +
127
+ "on the Creator Dashboard.");
128
+ }
129
+ if (status === 404) {
130
+ return new ToolError("NOT_FOUND", `Roblox has no such thing (404): ${detail}`, "Check the universe id, place id, and any data store or key name — " +
131
+ "Open Cloud does not create parents implicitly.");
132
+ }
133
+ if (status === 429) {
134
+ return new ToolError("RATE_LIMITED", `Rate limited by Roblox: ${detail}`, "Wait and retry.");
135
+ }
136
+ if (status === 400) {
137
+ return new ToolError("BAD_REQUEST", `Roblox refused the request (400): ${detail}`);
138
+ }
139
+ return new ToolError("CLOUD_ERROR", `Roblox returned ${status}: ${detail}`);
140
+ }
141
+ /**
142
+ * One Open Cloud request.
143
+ *
144
+ * Returns parsed JSON, or `{}` for the endpoints that answer 200 with an empty
145
+ * body — a DELETE among them. A caller that needs the raw text should not be
146
+ * using this.
147
+ */
148
+ export async function call(credentials, options) {
149
+ const url = new URL(options.path, BASE);
150
+ for (const [key, value] of Object.entries(options.query ?? {})) {
151
+ if (value !== undefined)
152
+ url.searchParams.set(key, String(value));
153
+ }
154
+ const headers = { "x-api-key": credentials.apiKey };
155
+ let body;
156
+ if (options.raw) {
157
+ headers["Content-Type"] = options.raw.contentType;
158
+ body = options.raw.bytes;
159
+ }
160
+ else if (options.body instanceof FormData) {
161
+ // Left unset on purpose: fetch adds the multipart boundary, and setting it
162
+ // by hand produces a body Roblox cannot parse.
163
+ body = options.body;
164
+ }
165
+ else if (options.body !== undefined) {
166
+ headers["Content-Type"] = "application/json";
167
+ body = JSON.stringify(options.body);
168
+ }
169
+ const controller = new AbortController();
170
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 60_000);
171
+ let response;
172
+ try {
173
+ response = await fetch(url, {
174
+ method: options.method ?? "GET",
175
+ headers,
176
+ body: body,
177
+ signal: controller.signal,
178
+ });
179
+ }
180
+ catch (cause) {
181
+ const error = cause;
182
+ throw new ToolError("CLOUD_UNREACHABLE", error.name === "AbortError"
183
+ ? "Roblox did not answer in time."
184
+ : `Could not reach Roblox: ${error.message}`);
185
+ }
186
+ finally {
187
+ clearTimeout(timer);
188
+ }
189
+ const text = await response.text();
190
+ if (!response.ok)
191
+ throw failure(response.status, text, options.scope);
192
+ if (text.trim() === "")
193
+ return {};
194
+ try {
195
+ return JSON.parse(text);
196
+ }
197
+ catch {
198
+ return { raw: text };
199
+ }
200
+ }
201
+ /**
202
+ * Walks a paged collection and returns at most `limit` items.
203
+ *
204
+ * Open Cloud pages everything and the page size it picks is not the number a
205
+ * caller asked for, so "the first 20 entries" is a loop, not a request. Capped
206
+ * rather than exhaustive: a data store can hold millions of keys, and the tool
207
+ * that asks for a page wants a page.
208
+ */
209
+ export async function paged(credentials, options) {
210
+ const items = [];
211
+ let pageToken = options.query?.["pageToken"];
212
+ for (;;) {
213
+ const page = await call(credentials, {
214
+ ...options,
215
+ query: {
216
+ ...options.query,
217
+ maxPageSize: Math.min(options.limit - items.length, 100),
218
+ pageToken,
219
+ },
220
+ });
221
+ const batch = page[options.field] ?? [];
222
+ items.push(...batch);
223
+ pageToken = page["nextPageToken"];
224
+ // An empty page with a token still set would spin forever; Roblox does
225
+ // return that when a filter excludes everything on a page.
226
+ if (!pageToken || items.length >= options.limit || batch.length === 0) {
227
+ return {
228
+ items: items.slice(0, options.limit),
229
+ nextPageToken: pageToken,
230
+ truncated: Boolean(pageToken),
231
+ };
232
+ }
233
+ }
234
+ }
235
+ //# sourceMappingURL=opencloud.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencloud.js","sourceRoot":"","sources":["../../src/lib/opencloud.ts"],"names":[],"mappings":"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"}