@stage5/lumine 0.2.92 → 0.2.94
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 +31 -0
- package/lib/admin.js +519 -126
- package/lib/api.js +58 -0
- package/lib/app-mcp/server.js +135 -23
- package/lib/assets.js +93 -2
- package/lib/commands.js +10 -0
- package/lib/constants.js +5 -0
- package/lib/util.js +28 -1
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +6 -4
- package/sdk/LUMINE_ADMIN.md +28 -0
package/lib/admin.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { assertAuthScope, resolveAuth } from "./auth.js";
|
|
14
14
|
import { requestJson } from "./http.js";
|
|
15
|
+
import { formatBytes, parseStorageSizeBytes } from "./util.js";
|
|
15
16
|
import {
|
|
16
17
|
extractNewsClaim,
|
|
17
18
|
readAdminJsonFile,
|
|
@@ -141,6 +142,238 @@ const REWARD_REVIEW_STATUSES = ["pending", "approved", "all"];
|
|
|
141
142
|
const CHAT_REPORT_STATUSES = ["open", "reviewing", "resolved", "dismissed"];
|
|
142
143
|
const CHAT_REPORT_LIST_STATUSES = ["pending", ...CHAT_REPORT_STATUSES, "all"];
|
|
143
144
|
const REWARD_REVIEW_DECISIONS = ["approve", "reject", "revoke"];
|
|
145
|
+
// `lumine admin review`: one queue for every Build unlock (XP/Coin rewards,
|
|
146
|
+
// project room, Lumine file storage, AI Card crafting). The per-type commands
|
|
147
|
+
// (reward-review, cardcraft-review, storage) are thin aliases of it.
|
|
148
|
+
const REVIEW_REQUEST_TYPES = [
|
|
149
|
+
"rewards",
|
|
150
|
+
"project-limit",
|
|
151
|
+
"storage-limit",
|
|
152
|
+
"cardcraft",
|
|
153
|
+
];
|
|
154
|
+
const REVIEW_REQUEST_TYPE_ALIASES = {
|
|
155
|
+
rewards: "rewards",
|
|
156
|
+
reward: "rewards",
|
|
157
|
+
"reward-review": "rewards",
|
|
158
|
+
"project-limit": "project-limit",
|
|
159
|
+
project: "project-limit",
|
|
160
|
+
room: "project-limit",
|
|
161
|
+
"storage-limit": "storage-limit",
|
|
162
|
+
storage: "storage-limit",
|
|
163
|
+
cardcraft: "cardcraft",
|
|
164
|
+
"card-craft": "cardcraft",
|
|
165
|
+
crafting: "cardcraft",
|
|
166
|
+
};
|
|
167
|
+
const REVIEW_REQUEST_LIST_STATUSES = [
|
|
168
|
+
"queue",
|
|
169
|
+
"pending",
|
|
170
|
+
"open",
|
|
171
|
+
"approved",
|
|
172
|
+
"rejected",
|
|
173
|
+
"all",
|
|
174
|
+
];
|
|
175
|
+
const REVIEW_REQUEST_LABELS = {
|
|
176
|
+
rewards: "XP & Coin rewards",
|
|
177
|
+
"project-limit": "Project room",
|
|
178
|
+
"storage-limit": "Lumine file storage",
|
|
179
|
+
cardcraft: "AI Card crafting",
|
|
180
|
+
};
|
|
181
|
+
const REVIEW_REQUEST_USAGE =
|
|
182
|
+
"Usage: lumine admin review list [--type rewards|project|storage|cardcraft] [--status queue|pending|approved|rejected|all] [--cursor <c>] | show <type:id> [--dir <path>] | approve <type:id> [--config <rules.json>] [--size <500MB|1GB|2GB|n MB>] [--reason <text>] | reject <type:id> [--reason <text>] | revoke <type:id> --reason <text> | propose rewards:<id> --dir <edited-snapshot> --config <rules.json> [--reason <text>]. A bare id works with --type.";
|
|
183
|
+
|
|
184
|
+
function parseReviewRequestType(value) {
|
|
185
|
+
const key = String(value || "")
|
|
186
|
+
.trim()
|
|
187
|
+
.toLowerCase();
|
|
188
|
+
const type = REVIEW_REQUEST_TYPE_ALIASES[key];
|
|
189
|
+
if (!type) {
|
|
190
|
+
throw cliValidationError(
|
|
191
|
+
`Unknown request type "${key}". Use rewards, project, storage or cardcraft.`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return type;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// "storage:12", "rewards:70", or a bare id with --type (or a fixed type).
|
|
198
|
+
function parseReviewRequestRef(target, { type: fixedType, typeOption } = {}) {
|
|
199
|
+
const raw = String(target || "").trim();
|
|
200
|
+
const match = /^([a-z-]+):(\d+)$/i.exec(raw);
|
|
201
|
+
if (match) {
|
|
202
|
+
const type = parseReviewRequestType(match[1]);
|
|
203
|
+
if (fixedType && type !== fixedType) {
|
|
204
|
+
throw cliValidationError(
|
|
205
|
+
`This command handles ${REVIEW_REQUEST_LABELS[fixedType]} requests only.`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
type,
|
|
210
|
+
id: parseRequiredInteger(match[2], "Request ID", 1),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const type = fixedType || (typeOption ? parseReviewRequestType(typeOption) : "");
|
|
214
|
+
if (!type) {
|
|
215
|
+
throw cliValidationError(
|
|
216
|
+
"Name the request as <type>:<id> (e.g. storage:12) or pass --type.",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return { type, id: parseRequiredInteger(raw, "Request ID", 1) };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Builds the operation for list/show/approve/reject/revoke. `fixedType` is set
|
|
223
|
+
// by the per-type aliases; `listStatuses` keeps their own --status choices.
|
|
224
|
+
function buildReviewRequestOperation({
|
|
225
|
+
action,
|
|
226
|
+
target,
|
|
227
|
+
options,
|
|
228
|
+
fixedType = "",
|
|
229
|
+
command = "lumine admin review",
|
|
230
|
+
listStatuses = REVIEW_REQUEST_LIST_STATUSES,
|
|
231
|
+
}) {
|
|
232
|
+
const reason = String(options.adminReason || "").trim();
|
|
233
|
+
if (reason.length > 1000) {
|
|
234
|
+
throw cliValidationError("--reason must be at most 1000 characters.");
|
|
235
|
+
}
|
|
236
|
+
if (!action || action === "list") {
|
|
237
|
+
return readOperation(
|
|
238
|
+
"review.list",
|
|
239
|
+
withQuery("/cli/admin/reviews", {
|
|
240
|
+
status: parseChoice(
|
|
241
|
+
options.adminStatus || "pending",
|
|
242
|
+
"--status",
|
|
243
|
+
listStatuses,
|
|
244
|
+
),
|
|
245
|
+
type:
|
|
246
|
+
fixedType ||
|
|
247
|
+
(options.adminType
|
|
248
|
+
? String(options.adminType)
|
|
249
|
+
.split(",")
|
|
250
|
+
.map((value) => parseReviewRequestType(value))
|
|
251
|
+
.join(",")
|
|
252
|
+
: ""),
|
|
253
|
+
cursor: options.adminCursor
|
|
254
|
+
? /^\d+(\.\d\.\d+)?$/.test(String(options.adminCursor))
|
|
255
|
+
? String(options.adminCursor)
|
|
256
|
+
: (() => {
|
|
257
|
+
throw cliValidationError(
|
|
258
|
+
"--cursor must be the value a previous list printed.",
|
|
259
|
+
);
|
|
260
|
+
})()
|
|
261
|
+
: "",
|
|
262
|
+
}),
|
|
263
|
+
{ requiresRun: false, reviewType: fixedType || null },
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const ref = parseReviewRequestRef(target, {
|
|
267
|
+
type: fixedType,
|
|
268
|
+
typeOption: options.adminType,
|
|
269
|
+
});
|
|
270
|
+
if (action === "show" || action === "get") {
|
|
271
|
+
const snapshotDir = String(options.dir || "").trim();
|
|
272
|
+
if (options.dir && !snapshotDir) {
|
|
273
|
+
throw cliValidationError("Pass a new or empty directory with --dir <path>.");
|
|
274
|
+
}
|
|
275
|
+
if (snapshotDir && ref.type !== "rewards") {
|
|
276
|
+
throw cliValidationError("--dir writes a reward request's frozen source.");
|
|
277
|
+
}
|
|
278
|
+
return readOperation(
|
|
279
|
+
"review.show",
|
|
280
|
+
withQuery(`/cli/admin/reviews/${ref.type}/${ref.id}`, {
|
|
281
|
+
// Reward source is listed with sizes only unless --dir asks for it.
|
|
282
|
+
files: snapshotDir ? "1" : "0",
|
|
283
|
+
}),
|
|
284
|
+
{
|
|
285
|
+
requiresRun: false,
|
|
286
|
+
reviewType: ref.type,
|
|
287
|
+
reviewId: ref.id,
|
|
288
|
+
snapshotDir,
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
if (!REWARD_REVIEW_DECISIONS.includes(action)) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
if (action === "revoke" && !["rewards", "cardcraft"].includes(ref.type)) {
|
|
296
|
+
throw cliValidationError(
|
|
297
|
+
`${REVIEW_REQUEST_LABELS[ref.type]} approvals are not revoked; change the limit instead${ref.type === "storage-limit" ? " (lumine admin storage grant)" : ""}.`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (
|
|
301
|
+
["rewards", "cardcraft"].includes(ref.type) &&
|
|
302
|
+
action !== "approve" &&
|
|
303
|
+
!reason
|
|
304
|
+
) {
|
|
305
|
+
throw cliValidationError(
|
|
306
|
+
`${command} ${action} <id> needs --reason <text> the creator will read.`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
const body = { decision: action, reason };
|
|
310
|
+
if (options.adminConfigFile) {
|
|
311
|
+
if (ref.type !== "rewards") {
|
|
312
|
+
throw cliValidationError(
|
|
313
|
+
"--config (earning rules) is only used with XP & Coin reward requests.",
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
if (action !== "approve") {
|
|
317
|
+
throw cliValidationError(
|
|
318
|
+
"--config is only used with approve; rejections and revocations take --reason.",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
// Without --config the app's own proposal (rewards.json + sheet, frozen
|
|
322
|
+
// in the request) is approved as it stands; --config replaces it.
|
|
323
|
+
body.config = readRewardConfigFile(options.adminConfigFile);
|
|
324
|
+
}
|
|
325
|
+
if (options.storageSize) {
|
|
326
|
+
if (ref.type !== "storage-limit") {
|
|
327
|
+
throw cliValidationError(
|
|
328
|
+
"--size is only used with Lumine file storage requests.",
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (action !== "approve") {
|
|
332
|
+
throw cliValidationError("--size is only used with approve.");
|
|
333
|
+
}
|
|
334
|
+
body.sizeBytes = parseAdminStorageSize(options.storageSize);
|
|
335
|
+
}
|
|
336
|
+
return writeOperation(
|
|
337
|
+
"review.decide",
|
|
338
|
+
"POST",
|
|
339
|
+
`/cli/admin/reviews/${ref.type}/${ref.id}`,
|
|
340
|
+
body,
|
|
341
|
+
{
|
|
342
|
+
requiresRun: false,
|
|
343
|
+
reviewType: ref.type,
|
|
344
|
+
reviewId: ref.id,
|
|
345
|
+
decision: action,
|
|
346
|
+
},
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// A reviewer counter-proposal for an XP & Coin reward request: the edited copy
|
|
351
|
+
// of the snapshot (from `show --dir`) plus the rules the creator's acceptance
|
|
352
|
+
// publishes.
|
|
353
|
+
function buildRewardProposalOperation(reviewId, options) {
|
|
354
|
+
const reason = String(options.adminReason || "").trim();
|
|
355
|
+
if (reason.length > 1000) {
|
|
356
|
+
throw cliValidationError("--reason must be at most 1000 characters.");
|
|
357
|
+
}
|
|
358
|
+
if (!options.adminConfigFile) {
|
|
359
|
+
throw cliValidationError(
|
|
360
|
+
"lumine admin reward-review propose <id> needs --config <rules.json> (the rules the creator's acceptance publishes) and --dir <edited snapshot>.",
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
const files = readRewardProposalDirectory(options.dir);
|
|
364
|
+
return writeOperation(
|
|
365
|
+
"reward-review.propose",
|
|
366
|
+
"POST",
|
|
367
|
+
`/cli/admin/reward-reviews/${reviewId}/propose`,
|
|
368
|
+
{
|
|
369
|
+
files,
|
|
370
|
+
config: readRewardConfigFile(options.adminConfigFile),
|
|
371
|
+
reason,
|
|
372
|
+
},
|
|
373
|
+
{ requiresRun: false, reviewId, fileCount: files.length },
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
144
377
|
const REWARD_PROPOSAL_MAX_FILES = 500;
|
|
145
378
|
const REWARD_PROPOSAL_MAX_BYTES = 5 * 1024 * 1024;
|
|
146
379
|
const REWARD_PROPOSAL_SKIPPED_DIRS = new Set([
|
|
@@ -633,7 +866,11 @@ export async function adminCommand(options) {
|
|
|
633
866
|
if (operation.name === "daily-run.start") {
|
|
634
867
|
assertAdminTodoHandoffResult(result, operation.body.scope);
|
|
635
868
|
}
|
|
636
|
-
if (
|
|
869
|
+
if (
|
|
870
|
+
(operation.name === "reward-review.show" ||
|
|
871
|
+
operation.name === "review.show") &&
|
|
872
|
+
operation.snapshotDir
|
|
873
|
+
) {
|
|
637
874
|
// The snapshot leaves the JSON result and lands on disk, where the agent
|
|
638
875
|
// reads it like any pulled workspace; the result keeps sizes and paths.
|
|
639
876
|
const review = result?.data?.review || {};
|
|
@@ -1597,103 +1834,46 @@ export function parseAdminOperation(options) {
|
|
|
1597
1834
|
}
|
|
1598
1835
|
}
|
|
1599
1836
|
|
|
1837
|
+
if (namespace === "review" || namespace === "reviews") {
|
|
1838
|
+
// Every Build unlock in one queue. Mikey's own decisions: any time, never
|
|
1839
|
+
// part of a delegated Zero/Ciel daily run.
|
|
1840
|
+
if (action === "propose") {
|
|
1841
|
+
const ref = parseReviewRequestRef(target, {
|
|
1842
|
+
typeOption: options.adminType,
|
|
1843
|
+
});
|
|
1844
|
+
if (ref.type !== "rewards") {
|
|
1845
|
+
throw cliValidationError(
|
|
1846
|
+
"Only XP & Coin reward requests take a proposed copy.",
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
return buildRewardProposalOperation(ref.id, options);
|
|
1850
|
+
}
|
|
1851
|
+
const operation = buildReviewRequestOperation({ action, target, options });
|
|
1852
|
+
if (operation) return operation;
|
|
1853
|
+
throw cliValidationError(REVIEW_REQUEST_USAGE);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1600
1856
|
if (
|
|
1601
1857
|
namespace === "reward-review" ||
|
|
1602
1858
|
namespace === "reward-reviews" ||
|
|
1603
1859
|
namespace === "rewards"
|
|
1604
1860
|
) {
|
|
1605
|
-
//
|
|
1606
|
-
// never part of a delegated Zero/Ciel daily run: they can happen any time.
|
|
1607
|
-
const snapshotDir = String(options.dir || "").trim();
|
|
1608
|
-
if (!action || action === "list") {
|
|
1609
|
-
return readOperation(
|
|
1610
|
-
"reward-review.list",
|
|
1611
|
-
withQuery("/cli/admin/reward-reviews", {
|
|
1612
|
-
status: parseChoice(
|
|
1613
|
-
options.adminStatus || "pending",
|
|
1614
|
-
"--status",
|
|
1615
|
-
REWARD_REVIEW_STATUSES,
|
|
1616
|
-
),
|
|
1617
|
-
beforeId: options.adminCursor
|
|
1618
|
-
? parseRequiredInteger(options.adminCursor, "--cursor", 1)
|
|
1619
|
-
: "",
|
|
1620
|
-
}),
|
|
1621
|
-
{ requiresRun: false },
|
|
1622
|
-
);
|
|
1623
|
-
}
|
|
1624
|
-
if (action === "show" || action === "get") {
|
|
1625
|
-
const reviewId = parseRequiredInteger(target, "Reward review ID", 1);
|
|
1626
|
-
if (options.dir && !snapshotDir) {
|
|
1627
|
-
throw cliValidationError(
|
|
1628
|
-
"Pass a new or empty directory with --dir <path>.",
|
|
1629
|
-
);
|
|
1630
|
-
}
|
|
1631
|
-
return readOperation(
|
|
1632
|
-
"reward-review.show",
|
|
1633
|
-
withQuery(`/cli/admin/reward-reviews/${reviewId}`, {
|
|
1634
|
-
// Without --dir the source is listed with sizes only; with --dir the
|
|
1635
|
-
// full snapshot is fetched and written locally for reading.
|
|
1636
|
-
files: snapshotDir ? "1" : "0",
|
|
1637
|
-
}),
|
|
1638
|
-
{ requiresRun: false, reviewId, snapshotDir },
|
|
1639
|
-
);
|
|
1640
|
-
}
|
|
1861
|
+
// Alias of `lumine admin review ... rewards:<id>`; propose stays here.
|
|
1641
1862
|
if (action === "propose") {
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
const reason = String(options.adminReason || "").trim();
|
|
1646
|
-
if (reason.length > 1000) {
|
|
1647
|
-
throw cliValidationError("--reason must be at most 1000 characters.");
|
|
1648
|
-
}
|
|
1649
|
-
if (!options.adminConfigFile) {
|
|
1650
|
-
throw cliValidationError(
|
|
1651
|
-
"lumine admin reward-review propose <id> needs --config <rules.json> (the rules the creator's acceptance publishes) and --dir <edited snapshot>.",
|
|
1652
|
-
);
|
|
1653
|
-
}
|
|
1654
|
-
const files = readRewardProposalDirectory(options.dir);
|
|
1655
|
-
return writeOperation(
|
|
1656
|
-
"reward-review.propose",
|
|
1657
|
-
"POST",
|
|
1658
|
-
`/cli/admin/reward-reviews/${reviewId}/propose`,
|
|
1659
|
-
{
|
|
1660
|
-
files,
|
|
1661
|
-
config: readRewardConfigFile(options.adminConfigFile),
|
|
1662
|
-
reason,
|
|
1663
|
-
},
|
|
1664
|
-
{ requiresRun: false, reviewId, fileCount: files.length },
|
|
1665
|
-
);
|
|
1666
|
-
}
|
|
1667
|
-
if (REWARD_REVIEW_DECISIONS.includes(action)) {
|
|
1668
|
-
const reviewId = parseRequiredInteger(target, "Reward review ID", 1);
|
|
1669
|
-
const reason = String(options.adminReason || "").trim();
|
|
1670
|
-
if (action !== "approve" && !reason) {
|
|
1671
|
-
throw cliValidationError(
|
|
1672
|
-
`lumine admin reward-review ${action} <id> needs --reason <text> the creator will read.`,
|
|
1673
|
-
);
|
|
1674
|
-
}
|
|
1675
|
-
if (reason.length > 1000) {
|
|
1676
|
-
throw cliValidationError("--reason must be at most 1000 characters.");
|
|
1677
|
-
}
|
|
1678
|
-
const body = { decision: action, reason };
|
|
1679
|
-
if (action === "approve") {
|
|
1680
|
-
// Without --config the app's own proposal (rewards.json + sheet, frozen
|
|
1681
|
-
// in the request) is approved as it stands; --config replaces it.
|
|
1682
|
-
if (options.adminConfigFile)
|
|
1683
|
-
body.config = readRewardConfigFile(options.adminConfigFile);
|
|
1684
|
-
} else if (options.adminConfigFile) {
|
|
1685
|
-
throw cliValidationError(
|
|
1686
|
-
"--config is only used with approve; rejections and revocations take --reason.",
|
|
1687
|
-
);
|
|
1688
|
-
}
|
|
1689
|
-
return writeOperation(
|
|
1690
|
-
"reward-review.decide",
|
|
1691
|
-
"POST",
|
|
1692
|
-
`/cli/admin/reward-reviews/${reviewId}`,
|
|
1693
|
-
body,
|
|
1694
|
-
{ requiresRun: false, reviewId, decision: action },
|
|
1863
|
+
return buildRewardProposalOperation(
|
|
1864
|
+
parseRequiredInteger(target, "Reward review ID", 1),
|
|
1865
|
+
options,
|
|
1695
1866
|
);
|
|
1696
1867
|
}
|
|
1868
|
+
const operation = buildReviewRequestOperation({
|
|
1869
|
+
action,
|
|
1870
|
+
target,
|
|
1871
|
+
options,
|
|
1872
|
+
fixedType: "rewards",
|
|
1873
|
+
command: "lumine admin reward-review",
|
|
1874
|
+
listStatuses: REWARD_REVIEW_STATUSES,
|
|
1875
|
+
});
|
|
1876
|
+
if (operation) return operation;
|
|
1697
1877
|
throw cliValidationError(
|
|
1698
1878
|
"Usage: lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] | show <id> [--dir <path>] | approve <id> [--config <rules.json>] [--reason <text>] | propose <id> --dir <edited-snapshot> --config <rules.json> [--reason <text>] | reject <id> --reason <text> | revoke <id> --reason <text>.",
|
|
1699
1879
|
);
|
|
@@ -1704,54 +1884,75 @@ export function parseAdminOperation(options) {
|
|
|
1704
1884
|
namespace === "cardcraft-reviews" ||
|
|
1705
1885
|
namespace === "cardcraft"
|
|
1706
1886
|
) {
|
|
1707
|
-
//
|
|
1708
|
-
|
|
1887
|
+
// Alias of `lumine admin review ... cardcraft:<id>`.
|
|
1888
|
+
const operation = buildReviewRequestOperation({
|
|
1889
|
+
action,
|
|
1890
|
+
target,
|
|
1891
|
+
options,
|
|
1892
|
+
fixedType: "cardcraft",
|
|
1893
|
+
command: "lumine admin cardcraft-review",
|
|
1894
|
+
listStatuses: REWARD_REVIEW_STATUSES,
|
|
1895
|
+
});
|
|
1896
|
+
if (operation) return operation;
|
|
1897
|
+
throw cliValidationError(
|
|
1898
|
+
"Usage: lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve <id> [--reason <text>] | reject <id> --reason <text> | revoke <id> --reason <text>.",
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
if (STORAGE_LIMIT_NAMESPACES.has(namespace)) {
|
|
1903
|
+
// Lumine file storage (Twinkle.files) unlocks: per-creator approvals past
|
|
1904
|
+
// the default quota. Mikey's own decision, any time, never in a daily run.
|
|
1905
|
+
const usage =
|
|
1906
|
+
"Usage: lumine admin storage list [--status pending|approved|rejected|all] [--cursor <id>] | show <user-id-or-username> | approve <request-id> [--size <500MB|1GB|2GB|n MB>] [--reason <text>] | reject <request-id> [--reason <text>] | grant <user-id-or-username> --size <500MB|1GB|2GB|n MB> [--reason <text>].";
|
|
1907
|
+
const reason = String(options.adminReason || "").trim();
|
|
1908
|
+
if (reason.length > 1000) {
|
|
1909
|
+
throw cliValidationError("--reason must be at most 1000 characters.");
|
|
1910
|
+
}
|
|
1709
1911
|
if (!action || action === "list") {
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
? parseRequiredInteger(options.adminCursor, "--cursor", 1)
|
|
1720
|
-
: "",
|
|
1721
|
-
}),
|
|
1722
|
-
{ requiresRun: false },
|
|
1723
|
-
);
|
|
1912
|
+
// Alias of `lumine admin review list --type storage`.
|
|
1913
|
+
return buildReviewRequestOperation({
|
|
1914
|
+
action,
|
|
1915
|
+
target,
|
|
1916
|
+
options,
|
|
1917
|
+
fixedType: "storage-limit",
|
|
1918
|
+
command: "lumine admin storage",
|
|
1919
|
+
listStatuses: ["pending", "approved", "rejected", "all"],
|
|
1920
|
+
});
|
|
1724
1921
|
}
|
|
1725
1922
|
if (action === "show" || action === "get") {
|
|
1726
|
-
const
|
|
1923
|
+
const user = parseStorageTargetUser(target);
|
|
1727
1924
|
return readOperation(
|
|
1728
|
-
"
|
|
1729
|
-
`/cli/admin/
|
|
1730
|
-
{ requiresRun: false
|
|
1925
|
+
"storage.show",
|
|
1926
|
+
`/cli/admin/storage-limits/users/${encodeURIComponent(user)}`,
|
|
1927
|
+
{ requiresRun: false },
|
|
1731
1928
|
);
|
|
1732
1929
|
}
|
|
1733
|
-
if (
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1930
|
+
if (action === "approve" || action === "reject") {
|
|
1931
|
+
// Alias of `lumine admin review approve|reject storage:<id>`.
|
|
1932
|
+
return buildReviewRequestOperation({
|
|
1933
|
+
action,
|
|
1934
|
+
target,
|
|
1935
|
+
options,
|
|
1936
|
+
fixedType: "storage-limit",
|
|
1937
|
+
command: "lumine admin storage",
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
if (action === "grant" || action === "set") {
|
|
1941
|
+
const user = parseStorageTargetUser(target);
|
|
1942
|
+
if (!options.storageSize) {
|
|
1737
1943
|
throw cliValidationError(
|
|
1738
|
-
|
|
1944
|
+
"lumine admin storage grant <user> needs --size <500MB|1GB|2GB|n MB>.",
|
|
1739
1945
|
);
|
|
1740
1946
|
}
|
|
1741
|
-
if (reason.length > 1000) {
|
|
1742
|
-
throw cliValidationError("--reason must be at most 1000 characters.");
|
|
1743
|
-
}
|
|
1744
1947
|
return writeOperation(
|
|
1745
|
-
"
|
|
1746
|
-
"
|
|
1747
|
-
`/cli/admin/
|
|
1748
|
-
{
|
|
1749
|
-
{ requiresRun: false
|
|
1948
|
+
"storage.grant",
|
|
1949
|
+
"PUT",
|
|
1950
|
+
`/cli/admin/storage-limits/users/${encodeURIComponent(user)}`,
|
|
1951
|
+
{ sizeBytes: parseAdminStorageSize(options.storageSize), reason },
|
|
1952
|
+
{ requiresRun: false },
|
|
1750
1953
|
);
|
|
1751
1954
|
}
|
|
1752
|
-
throw cliValidationError(
|
|
1753
|
-
"Usage: lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve <id> [--reason <text>] | reject <id> --reason <text> | revoke <id> --reason <text>.",
|
|
1754
|
-
);
|
|
1955
|
+
throw cliValidationError(usage);
|
|
1755
1956
|
}
|
|
1756
1957
|
|
|
1757
1958
|
if (namespace === "chat-reports" || namespace === "chat-report") {
|
|
@@ -4458,6 +4659,190 @@ function printCardCraftReviewResult({ operation, data }) {
|
|
|
4458
4659
|
);
|
|
4459
4660
|
}
|
|
4460
4661
|
|
|
4662
|
+
const STORAGE_LIMIT_NAMESPACES = new Set([
|
|
4663
|
+
"storage",
|
|
4664
|
+
"storage-limit",
|
|
4665
|
+
"storage-limits",
|
|
4666
|
+
"storage-request",
|
|
4667
|
+
"storage-requests",
|
|
4668
|
+
]);
|
|
4669
|
+
|
|
4670
|
+
function parseStorageTargetUser(value) {
|
|
4671
|
+
const user = String(value || "")
|
|
4672
|
+
.trim()
|
|
4673
|
+
.replace(/^@/, "");
|
|
4674
|
+
if (!user || !/^[\w.-]+$/.test(user)) {
|
|
4675
|
+
throw cliValidationError("Pass a user id or username.");
|
|
4676
|
+
}
|
|
4677
|
+
return user;
|
|
4678
|
+
}
|
|
4679
|
+
|
|
4680
|
+
function parseAdminStorageSize(value) {
|
|
4681
|
+
try {
|
|
4682
|
+
return parseStorageSizeBytes(value);
|
|
4683
|
+
} catch (error) {
|
|
4684
|
+
throw cliValidationError(error.message);
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
|
|
4688
|
+
function formatStorageRequestLine(request) {
|
|
4689
|
+
const approved = request.approvedMaxRuntimeFileStorageBytes
|
|
4690
|
+
? ` → ${formatBytes(request.approvedMaxRuntimeFileStorageBytes)}`
|
|
4691
|
+
: "";
|
|
4692
|
+
return `#${request.requestId} · ${request.status} · ${request.username || `user ${request.userId}`} (${request.userId}) asks ${formatBytes(request.requestedMaxRuntimeFileStorageBytes)}${approved} · using ${formatBytes(request.currentUsageBytes)} at request${request.reason ? ` · "${request.reason}"` : ""}${request.reviewReason ? ` · review: ${request.reviewReason}` : ""}`;
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4695
|
+
function printStorageLimitResult({ operation, data, result }) {
|
|
4696
|
+
if (operation.name === "storage.list") {
|
|
4697
|
+
const requests = Array.isArray(data.requests) ? data.requests : [];
|
|
4698
|
+
console.log(
|
|
4699
|
+
`${requests.length} storage request(s) (${data.filter || "pending"}):`,
|
|
4700
|
+
);
|
|
4701
|
+
for (const request of requests)
|
|
4702
|
+
console.log(` ${formatStorageRequestLine(request)}`);
|
|
4703
|
+
if (data.nextCursor) console.log(`More: --cursor ${data.nextCursor}`);
|
|
4704
|
+
return;
|
|
4705
|
+
}
|
|
4706
|
+
if (operation.name === "storage.show") {
|
|
4707
|
+
const storage = data.storage || {};
|
|
4708
|
+
console.log(
|
|
4709
|
+
`${data.user?.username || "user"} (${data.user?.id}): ${formatBytes(storage.runtimeFileStorageBytes)} used of ${formatBytes(storage.maxRuntimeFileStorageBytes)} across ${Number(storage.runtimeFileCount || 0)} file(s) (default ${formatBytes(storage.defaultMaxRuntimeFileStorageBytes)}, cap ${formatBytes(storage.maxApprovableRuntimeFileStorageBytes)}).`,
|
|
4710
|
+
);
|
|
4711
|
+
if (data.override) {
|
|
4712
|
+
console.log(
|
|
4713
|
+
`Approved override: ${formatBytes(data.override.maxRuntimeFileStorageBytes)} by user ${data.override.approvedByUserId}${data.override.reason ? ` · ${data.override.reason}` : ""}`,
|
|
4714
|
+
);
|
|
4715
|
+
}
|
|
4716
|
+
for (const request of data.recentRequests || [])
|
|
4717
|
+
console.log(` ${formatStorageRequestLine(request)}`);
|
|
4718
|
+
return;
|
|
4719
|
+
}
|
|
4720
|
+
const limit = formatBytes(data.maxRuntimeFileStorageBytes);
|
|
4721
|
+
if (operation.name === "storage.decide") {
|
|
4722
|
+
const request = data.request || {};
|
|
4723
|
+
console.log(
|
|
4724
|
+
result?.changed === false
|
|
4725
|
+
? `Storage request #${request.requestId} was already ${request.status}. ${request.username || "The creator"}'s limit is ${limit}.`
|
|
4726
|
+
: `Storage request #${request.requestId} ${request.status}. ${request.username || "The creator"}'s Lumine file storage limit is now ${limit}.`,
|
|
4727
|
+
);
|
|
4728
|
+
return;
|
|
4729
|
+
}
|
|
4730
|
+
if (operation.name === "storage.grant") {
|
|
4731
|
+
console.log(
|
|
4732
|
+
result?.changed === false
|
|
4733
|
+
? `${data.user?.username || "The user"} (${data.user?.id}) already has ${limit} of Lumine file storage.`
|
|
4734
|
+
: `${data.user?.username || "The user"} (${data.user?.id}) now has ${limit} of Lumine file storage.${data.request ? ` Closed pending request #${data.request.requestId} as approved.` : ""}`,
|
|
4735
|
+
);
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
|
|
4739
|
+
function formatReviewAge(seconds) {
|
|
4740
|
+
const at = Number(seconds || 0);
|
|
4741
|
+
if (!at) return "";
|
|
4742
|
+
const minutes = Math.max(0, Math.round((Date.now() / 1000 - at) / 60));
|
|
4743
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
4744
|
+
const hours = Math.round(minutes / 60);
|
|
4745
|
+
if (hours < 48) return `${hours}h ago`;
|
|
4746
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
4747
|
+
}
|
|
4748
|
+
|
|
4749
|
+
function formatReviewItemLine(item) {
|
|
4750
|
+
const where =
|
|
4751
|
+
item.appTitle || (item.type === "storage-limit" ? "all their Builds" : "");
|
|
4752
|
+
return [
|
|
4753
|
+
`${item.ref} · ${item.status}${item.details?.isLive ? " (live)" : ""}`,
|
|
4754
|
+
REVIEW_REQUEST_LABELS[item.type] || item.type,
|
|
4755
|
+
`${where ? `${where} · ` : ""}by ${item.requesterUsername || "user"} (${item.requesterId})`,
|
|
4756
|
+
item.summary,
|
|
4757
|
+
item.reason ? `"${item.reason}"` : "",
|
|
4758
|
+
item.reviewReason && item.status !== "pending" ? `note: ${item.reviewReason}` : "",
|
|
4759
|
+
formatReviewAge(item.createdAt),
|
|
4760
|
+
]
|
|
4761
|
+
.filter(Boolean)
|
|
4762
|
+
.join(" · ");
|
|
4763
|
+
}
|
|
4764
|
+
|
|
4765
|
+
function printReviewEvents(events) {
|
|
4766
|
+
for (const event of events || [])
|
|
4767
|
+
console.log(
|
|
4768
|
+
` ${new Date(Number(event.createdAt || 0) * 1000).toISOString()} ${event.action} by ${event.actorId}${event.reason ? `: ${event.reason}` : ""}`,
|
|
4769
|
+
);
|
|
4770
|
+
}
|
|
4771
|
+
|
|
4772
|
+
function printReviewRequestResult({ operation, data, result }) {
|
|
4773
|
+
if (operation.name === "review.list") {
|
|
4774
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
4775
|
+
const scope = operation.reviewType
|
|
4776
|
+
? REVIEW_REQUEST_LABELS[operation.reviewType]
|
|
4777
|
+
: "Build";
|
|
4778
|
+
console.log(`${items.length} ${scope} request(s) (${data.filter || "pending"}):`);
|
|
4779
|
+
for (const item of items) console.log(` ${formatReviewItemLine(item)}`);
|
|
4780
|
+
if (data.nextCursor) console.log(`More: --cursor ${data.nextCursor}`);
|
|
4781
|
+
if (items.some((item) => item.decisions?.includes("approve")))
|
|
4782
|
+
console.log(
|
|
4783
|
+
"Decide: lumine admin review approve|reject <ref> (storage: --size; rewards: --config; cardcraft/rewards reject needs --reason).",
|
|
4784
|
+
);
|
|
4785
|
+
return;
|
|
4786
|
+
}
|
|
4787
|
+
const type = operation.reviewType;
|
|
4788
|
+
// Rewards and card crafting keep their detailed printers.
|
|
4789
|
+
if (type === "rewards") {
|
|
4790
|
+
printRewardReviewResult({
|
|
4791
|
+
operation: {
|
|
4792
|
+
...operation,
|
|
4793
|
+
name:
|
|
4794
|
+
operation.name === "review.decide"
|
|
4795
|
+
? "reward-review.decide"
|
|
4796
|
+
: "reward-review.show",
|
|
4797
|
+
},
|
|
4798
|
+
data,
|
|
4799
|
+
});
|
|
4800
|
+
return;
|
|
4801
|
+
}
|
|
4802
|
+
if (type === "cardcraft") {
|
|
4803
|
+
printCardCraftReviewResult({
|
|
4804
|
+
operation: {
|
|
4805
|
+
...operation,
|
|
4806
|
+
name:
|
|
4807
|
+
operation.name === "review.decide"
|
|
4808
|
+
? "cardcraft-review.decide"
|
|
4809
|
+
: "cardcraft-review.show",
|
|
4810
|
+
},
|
|
4811
|
+
data,
|
|
4812
|
+
});
|
|
4813
|
+
return;
|
|
4814
|
+
}
|
|
4815
|
+
if (operation.name === "review.decide" && type === "storage-limit") {
|
|
4816
|
+
printStorageLimitResult({
|
|
4817
|
+
operation: { ...operation, name: "storage.decide" },
|
|
4818
|
+
data,
|
|
4819
|
+
result,
|
|
4820
|
+
});
|
|
4821
|
+
return;
|
|
4822
|
+
}
|
|
4823
|
+
if (data.item) console.log(formatReviewItemLine(data.item));
|
|
4824
|
+
if (operation.name === "review.decide") {
|
|
4825
|
+
console.log(
|
|
4826
|
+
result?.changed === false
|
|
4827
|
+
? `Request ${data.item?.ref || ""} was already ${data.item?.status || data.request?.status}.`
|
|
4828
|
+
: `Decision recorded: ${operation.decision}.${type === "project-limit" && operation.decision === "approve" ? " Main and all its branches have the room now." : ""}`,
|
|
4829
|
+
);
|
|
4830
|
+
return;
|
|
4831
|
+
}
|
|
4832
|
+
if (type === "storage-limit" && data.storage) {
|
|
4833
|
+
const storage = data.storage;
|
|
4834
|
+
console.log(
|
|
4835
|
+
`Now: ${formatBytes(storage.runtimeFileStorageBytes)} used of ${formatBytes(storage.maxRuntimeFileStorageBytes)} across ${Number(storage.runtimeFileCount || 0)} file(s) (default ${formatBytes(storage.defaultMaxRuntimeFileStorageBytes)}, cap ${formatBytes(storage.maxApprovableRuntimeFileStorageBytes)}).`,
|
|
4836
|
+
);
|
|
4837
|
+
}
|
|
4838
|
+
if (type === "project-limit" && data.currentLimits) {
|
|
4839
|
+
console.log(
|
|
4840
|
+
`Now: ${data.currentLimits.maxFilesPerProject} files, ${formatBytes(data.currentLimits.maxProjectBytes)} (Main; every branch inherits it).`,
|
|
4841
|
+
);
|
|
4842
|
+
}
|
|
4843
|
+
printReviewEvents(data.events);
|
|
4844
|
+
}
|
|
4845
|
+
|
|
4461
4846
|
function printAdminResult({ operation, result }) {
|
|
4462
4847
|
const data = result?.data || {};
|
|
4463
4848
|
if (printChatSafetyResult({ operation, data })) return;
|
|
@@ -4465,6 +4850,10 @@ function printAdminResult({ operation, result }) {
|
|
|
4465
4850
|
printRewardActivity(data);
|
|
4466
4851
|
return;
|
|
4467
4852
|
}
|
|
4853
|
+
if (operation.name.startsWith("review.")) {
|
|
4854
|
+
printReviewRequestResult({ operation, data, result });
|
|
4855
|
+
return;
|
|
4856
|
+
}
|
|
4468
4857
|
if (operation.name.startsWith("reward-review.")) {
|
|
4469
4858
|
printRewardReviewResult({ operation, data });
|
|
4470
4859
|
return;
|
|
@@ -4473,6 +4862,10 @@ function printAdminResult({ operation, result }) {
|
|
|
4473
4862
|
printCardCraftReviewResult({ operation, data });
|
|
4474
4863
|
return;
|
|
4475
4864
|
}
|
|
4865
|
+
if (operation.name.startsWith("storage.")) {
|
|
4866
|
+
printStorageLimitResult({ operation, data, result });
|
|
4867
|
+
return;
|
|
4868
|
+
}
|
|
4476
4869
|
if (operation.name === "runtime.evidence") {
|
|
4477
4870
|
const evidence = data.evidence || {};
|
|
4478
4871
|
console.log(
|