@el4cteo/rbx-studio-mcp 0.6.5 → 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.
- package/README.md +28 -2
- package/dist/bridge/console.js +182 -0
- package/dist/bridge/console.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/cloudassets.js +233 -0
- package/dist/lib/cloudassets.js.map +1 -0
- package/dist/lib/credentials.js +180 -0
- package/dist/lib/credentials.js.map +1 -0
- package/dist/lib/livedata.js +325 -0
- package/dist/lib/livedata.js.map +1 -0
- package/dist/lib/liveluau.js +83 -0
- package/dist/lib/liveluau.js.map +1 -0
- package/dist/lib/liveops.js +358 -0
- package/dist/lib/liveops.js.map +1 -0
- package/dist/lib/opencloud.js +235 -0
- package/dist/lib/opencloud.js.map +1 -0
- package/dist/tools/audio.js +96 -0
- package/dist/tools/audio.js.map +1 -0
- package/dist/tools/data.js +122 -3
- package/dist/tools/data.js.map +1 -1
- package/dist/tools/exec.js +48 -1
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/scripts.js +112 -4
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/spatial.js +135 -0
- package/dist/tools/spatial.js.map +1 -0
- package/dist/tools/universe.js +177 -0
- package/dist/tools/universe.js.map +1 -0
- package/dist/tools/upload.js +294 -0
- package/dist/tools/upload.js.map +1 -0
- package/dist/tools/world.js +361 -10
- package/dist/tools/world.js.map +1 -1
- package/package.json +74 -74
- package/plugin/src/Commands.luau +646 -622
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Phrase.luau +816 -766
- package/plugin/src/Prompt.luau +965 -961
- package/plugin/src/Secret.luau +86 -0
- package/plugin/src/Serialize.luau +759 -499
- package/plugin/src/handlers/Assets.luau +636 -587
- package/plugin/src/handlers/Audio.luau +411 -0
- package/plugin/src/handlers/Geometry.luau +722 -577
- package/plugin/src/handlers/Instances.luau +84 -4
- package/plugin/src/handlers/Spatial.luau +334 -0
- 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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"}
|