@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/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 (operation.name === "reward-review.show" && operation.snapshotDir) {
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
- // Build XP/Coin reward approvals are the administrator's own decision and
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
- // Offer an edited copy of the snapshot as the condition of approval:
1643
- // the creator accepts (which publishes it) or declines (which rejects).
1644
- const reviewId = parseRequiredInteger(target, "Reward review ID", 1);
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
- // AI Card crafting recipes (cardcraft.json). Same reviewer as rewards and,
1708
- // like rewards, never part of a delegated daily run.
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
- return readOperation(
1711
- "cardcraft-review.list",
1712
- withQuery("/cli/admin/cardcraft-reviews", {
1713
- status: parseChoice(
1714
- options.adminStatus || "pending",
1715
- "--status",
1716
- REWARD_REVIEW_STATUSES,
1717
- ),
1718
- beforeId: options.adminCursor
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 reviewId = parseRequiredInteger(target, "Card crafting review ID", 1);
1923
+ const user = parseStorageTargetUser(target);
1727
1924
  return readOperation(
1728
- "cardcraft-review.show",
1729
- `/cli/admin/cardcraft-reviews/${reviewId}`,
1730
- { requiresRun: false, reviewId },
1925
+ "storage.show",
1926
+ `/cli/admin/storage-limits/users/${encodeURIComponent(user)}`,
1927
+ { requiresRun: false },
1731
1928
  );
1732
1929
  }
1733
- if (REWARD_REVIEW_DECISIONS.includes(action)) {
1734
- const reviewId = parseRequiredInteger(target, "Card crafting review ID", 1);
1735
- const reason = String(options.adminReason || "").trim();
1736
- if (action !== "approve" && !reason) {
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
- `lumine admin cardcraft-review ${action} <id> needs --reason <text> the creator will read.`,
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
- "cardcraft-review.decide",
1746
- "POST",
1747
- `/cli/admin/cardcraft-reviews/${reviewId}`,
1748
- { decision: action, reason },
1749
- { requiresRun: false, reviewId, decision: action },
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(