@stage5/lumine 0.2.92 → 0.2.95

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,
@@ -31,6 +32,11 @@ import {
31
32
  runManagedBuildReview,
32
33
  } from "./build-review.js";
33
34
  import { runAdminRuntimeLogWorkflow } from "./admin-runtime-logs.js";
35
+ import {
36
+ dailyRunGather,
37
+ featuredCandidates,
38
+ reportServe,
39
+ } from "./admin-daily.js";
34
40
  import {
35
41
  readApprovedFeaturedPlan,
36
42
  runFeaturedWorkflow,
@@ -141,6 +147,238 @@ const REWARD_REVIEW_STATUSES = ["pending", "approved", "all"];
141
147
  const CHAT_REPORT_STATUSES = ["open", "reviewing", "resolved", "dismissed"];
142
148
  const CHAT_REPORT_LIST_STATUSES = ["pending", ...CHAT_REPORT_STATUSES, "all"];
143
149
  const REWARD_REVIEW_DECISIONS = ["approve", "reject", "revoke"];
150
+ // `lumine admin review`: one queue for every Build unlock (XP/Coin rewards,
151
+ // project room, Lumine file storage, AI Card crafting). The per-type commands
152
+ // (reward-review, cardcraft-review, storage) are thin aliases of it.
153
+ const REVIEW_REQUEST_TYPES = [
154
+ "rewards",
155
+ "project-limit",
156
+ "storage-limit",
157
+ "cardcraft",
158
+ ];
159
+ const REVIEW_REQUEST_TYPE_ALIASES = {
160
+ rewards: "rewards",
161
+ reward: "rewards",
162
+ "reward-review": "rewards",
163
+ "project-limit": "project-limit",
164
+ project: "project-limit",
165
+ room: "project-limit",
166
+ "storage-limit": "storage-limit",
167
+ storage: "storage-limit",
168
+ cardcraft: "cardcraft",
169
+ "card-craft": "cardcraft",
170
+ crafting: "cardcraft",
171
+ };
172
+ const REVIEW_REQUEST_LIST_STATUSES = [
173
+ "queue",
174
+ "pending",
175
+ "open",
176
+ "approved",
177
+ "rejected",
178
+ "all",
179
+ ];
180
+ const REVIEW_REQUEST_LABELS = {
181
+ rewards: "XP & Coin rewards",
182
+ "project-limit": "Project room",
183
+ "storage-limit": "Lumine file storage",
184
+ cardcraft: "AI Card crafting",
185
+ };
186
+ const REVIEW_REQUEST_USAGE =
187
+ "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.";
188
+
189
+ function parseReviewRequestType(value) {
190
+ const key = String(value || "")
191
+ .trim()
192
+ .toLowerCase();
193
+ const type = REVIEW_REQUEST_TYPE_ALIASES[key];
194
+ if (!type) {
195
+ throw cliValidationError(
196
+ `Unknown request type "${key}". Use rewards, project, storage or cardcraft.`,
197
+ );
198
+ }
199
+ return type;
200
+ }
201
+
202
+ // "storage:12", "rewards:70", or a bare id with --type (or a fixed type).
203
+ function parseReviewRequestRef(target, { type: fixedType, typeOption } = {}) {
204
+ const raw = String(target || "").trim();
205
+ const match = /^([a-z-]+):(\d+)$/i.exec(raw);
206
+ if (match) {
207
+ const type = parseReviewRequestType(match[1]);
208
+ if (fixedType && type !== fixedType) {
209
+ throw cliValidationError(
210
+ `This command handles ${REVIEW_REQUEST_LABELS[fixedType]} requests only.`,
211
+ );
212
+ }
213
+ return {
214
+ type,
215
+ id: parseRequiredInteger(match[2], "Request ID", 1),
216
+ };
217
+ }
218
+ const type = fixedType || (typeOption ? parseReviewRequestType(typeOption) : "");
219
+ if (!type) {
220
+ throw cliValidationError(
221
+ "Name the request as <type>:<id> (e.g. storage:12) or pass --type.",
222
+ );
223
+ }
224
+ return { type, id: parseRequiredInteger(raw, "Request ID", 1) };
225
+ }
226
+
227
+ // Builds the operation for list/show/approve/reject/revoke. `fixedType` is set
228
+ // by the per-type aliases; `listStatuses` keeps their own --status choices.
229
+ function buildReviewRequestOperation({
230
+ action,
231
+ target,
232
+ options,
233
+ fixedType = "",
234
+ command = "lumine admin review",
235
+ listStatuses = REVIEW_REQUEST_LIST_STATUSES,
236
+ }) {
237
+ const reason = String(options.adminReason || "").trim();
238
+ if (reason.length > 1000) {
239
+ throw cliValidationError("--reason must be at most 1000 characters.");
240
+ }
241
+ if (!action || action === "list") {
242
+ return readOperation(
243
+ "review.list",
244
+ withQuery("/cli/admin/reviews", {
245
+ status: parseChoice(
246
+ options.adminStatus || "pending",
247
+ "--status",
248
+ listStatuses,
249
+ ),
250
+ type:
251
+ fixedType ||
252
+ (options.adminType
253
+ ? String(options.adminType)
254
+ .split(",")
255
+ .map((value) => parseReviewRequestType(value))
256
+ .join(",")
257
+ : ""),
258
+ cursor: options.adminCursor
259
+ ? /^\d+(\.\d\.\d+)?$/.test(String(options.adminCursor))
260
+ ? String(options.adminCursor)
261
+ : (() => {
262
+ throw cliValidationError(
263
+ "--cursor must be the value a previous list printed.",
264
+ );
265
+ })()
266
+ : "",
267
+ }),
268
+ { requiresRun: false, reviewType: fixedType || null },
269
+ );
270
+ }
271
+ const ref = parseReviewRequestRef(target, {
272
+ type: fixedType,
273
+ typeOption: options.adminType,
274
+ });
275
+ if (action === "show" || action === "get") {
276
+ const snapshotDir = String(options.dir || "").trim();
277
+ if (options.dir && !snapshotDir) {
278
+ throw cliValidationError("Pass a new or empty directory with --dir <path>.");
279
+ }
280
+ if (snapshotDir && ref.type !== "rewards") {
281
+ throw cliValidationError("--dir writes a reward request's frozen source.");
282
+ }
283
+ return readOperation(
284
+ "review.show",
285
+ withQuery(`/cli/admin/reviews/${ref.type}/${ref.id}`, {
286
+ // Reward source is listed with sizes only unless --dir asks for it.
287
+ files: snapshotDir ? "1" : "0",
288
+ }),
289
+ {
290
+ requiresRun: false,
291
+ reviewType: ref.type,
292
+ reviewId: ref.id,
293
+ snapshotDir,
294
+ },
295
+ );
296
+ }
297
+ if (!REWARD_REVIEW_DECISIONS.includes(action)) {
298
+ return null;
299
+ }
300
+ if (action === "revoke" && !["rewards", "cardcraft"].includes(ref.type)) {
301
+ throw cliValidationError(
302
+ `${REVIEW_REQUEST_LABELS[ref.type]} approvals are not revoked; change the limit instead${ref.type === "storage-limit" ? " (lumine admin storage grant)" : ""}.`,
303
+ );
304
+ }
305
+ if (
306
+ ["rewards", "cardcraft"].includes(ref.type) &&
307
+ action !== "approve" &&
308
+ !reason
309
+ ) {
310
+ throw cliValidationError(
311
+ `${command} ${action} <id> needs --reason <text> the creator will read.`,
312
+ );
313
+ }
314
+ const body = { decision: action, reason };
315
+ if (options.adminConfigFile) {
316
+ if (ref.type !== "rewards") {
317
+ throw cliValidationError(
318
+ "--config (earning rules) is only used with XP & Coin reward requests.",
319
+ );
320
+ }
321
+ if (action !== "approve") {
322
+ throw cliValidationError(
323
+ "--config is only used with approve; rejections and revocations take --reason.",
324
+ );
325
+ }
326
+ // Without --config the app's own proposal (rewards.json + sheet, frozen
327
+ // in the request) is approved as it stands; --config replaces it.
328
+ body.config = readRewardConfigFile(options.adminConfigFile);
329
+ }
330
+ if (options.storageSize) {
331
+ if (ref.type !== "storage-limit") {
332
+ throw cliValidationError(
333
+ "--size is only used with Lumine file storage requests.",
334
+ );
335
+ }
336
+ if (action !== "approve") {
337
+ throw cliValidationError("--size is only used with approve.");
338
+ }
339
+ body.sizeBytes = parseAdminStorageSize(options.storageSize);
340
+ }
341
+ return writeOperation(
342
+ "review.decide",
343
+ "POST",
344
+ `/cli/admin/reviews/${ref.type}/${ref.id}`,
345
+ body,
346
+ {
347
+ requiresRun: false,
348
+ reviewType: ref.type,
349
+ reviewId: ref.id,
350
+ decision: action,
351
+ },
352
+ );
353
+ }
354
+
355
+ // A reviewer counter-proposal for an XP & Coin reward request: the edited copy
356
+ // of the snapshot (from `show --dir`) plus the rules the creator's acceptance
357
+ // publishes.
358
+ function buildRewardProposalOperation(reviewId, options) {
359
+ const reason = String(options.adminReason || "").trim();
360
+ if (reason.length > 1000) {
361
+ throw cliValidationError("--reason must be at most 1000 characters.");
362
+ }
363
+ if (!options.adminConfigFile) {
364
+ throw cliValidationError(
365
+ "lumine admin reward-review propose <id> needs --config <rules.json> (the rules the creator's acceptance publishes) and --dir <edited snapshot>.",
366
+ );
367
+ }
368
+ const files = readRewardProposalDirectory(options.dir);
369
+ return writeOperation(
370
+ "reward-review.propose",
371
+ "POST",
372
+ `/cli/admin/reward-reviews/${reviewId}/propose`,
373
+ {
374
+ files,
375
+ config: readRewardConfigFile(options.adminConfigFile),
376
+ reason,
377
+ },
378
+ { requiresRun: false, reviewId, fileCount: files.length },
379
+ );
380
+ }
381
+
144
382
  const REWARD_PROPOSAL_MAX_FILES = 500;
145
383
  const REWARD_PROPOSAL_MAX_BYTES = 5 * 1024 * 1024;
146
384
  const REWARD_PROPOSAL_SKIPPED_DIRS = new Set([
@@ -349,6 +587,26 @@ function readEditorialFile(filePath) {
349
587
  }
350
588
 
351
589
  export async function adminCommand(options) {
590
+ const [namespace, action] = options.positional || [];
591
+ const composite =
592
+ namespace === "daily-run" && action === "gather"
593
+ ? dailyRunGather
594
+ : namespace === "featured" && action === "candidates"
595
+ ? featuredCandidates
596
+ : namespace === "report" && action === "serve"
597
+ ? reportServe
598
+ : null;
599
+ if (composite) {
600
+ const result = await composite(options);
601
+ if (options.adminOutput) {
602
+ writeFileSync(options.adminOutput, JSON.stringify(result), {
603
+ mode: 0o600,
604
+ });
605
+ }
606
+ console.log(JSON.stringify(result, null, options.json ? 0 : 2));
607
+ if (result && result.ok === false) process.exitCode = 1;
608
+ return result;
609
+ }
352
610
  const operation = parseAdminOperation(options);
353
611
  if (operation.name === "news.validate") {
354
612
  const validation = validateNewsEditorial({
@@ -633,7 +891,11 @@ export async function adminCommand(options) {
633
891
  if (operation.name === "daily-run.start") {
634
892
  assertAdminTodoHandoffResult(result, operation.body.scope);
635
893
  }
636
- if (operation.name === "reward-review.show" && operation.snapshotDir) {
894
+ if (
895
+ (operation.name === "reward-review.show" ||
896
+ operation.name === "review.show") &&
897
+ operation.snapshotDir
898
+ ) {
637
899
  // The snapshot leaves the JSON result and lands on disk, where the agent
638
900
  // reads it like any pulled workspace; the result keeps sizes and paths.
639
901
  const review = result?.data?.review || {};
@@ -1597,103 +1859,46 @@ export function parseAdminOperation(options) {
1597
1859
  }
1598
1860
  }
1599
1861
 
1862
+ if (namespace === "review" || namespace === "reviews") {
1863
+ // Every Build unlock in one queue. Mikey's own decisions: any time, never
1864
+ // part of a delegated Zero/Ciel daily run.
1865
+ if (action === "propose") {
1866
+ const ref = parseReviewRequestRef(target, {
1867
+ typeOption: options.adminType,
1868
+ });
1869
+ if (ref.type !== "rewards") {
1870
+ throw cliValidationError(
1871
+ "Only XP & Coin reward requests take a proposed copy.",
1872
+ );
1873
+ }
1874
+ return buildRewardProposalOperation(ref.id, options);
1875
+ }
1876
+ const operation = buildReviewRequestOperation({ action, target, options });
1877
+ if (operation) return operation;
1878
+ throw cliValidationError(REVIEW_REQUEST_USAGE);
1879
+ }
1880
+
1600
1881
  if (
1601
1882
  namespace === "reward-review" ||
1602
1883
  namespace === "reward-reviews" ||
1603
1884
  namespace === "rewards"
1604
1885
  ) {
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
- }
1886
+ // Alias of `lumine admin review ... rewards:<id>`; propose stays here.
1641
1887
  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 },
1888
+ return buildRewardProposalOperation(
1889
+ parseRequiredInteger(target, "Reward review ID", 1),
1890
+ options,
1695
1891
  );
1696
1892
  }
1893
+ const operation = buildReviewRequestOperation({
1894
+ action,
1895
+ target,
1896
+ options,
1897
+ fixedType: "rewards",
1898
+ command: "lumine admin reward-review",
1899
+ listStatuses: REWARD_REVIEW_STATUSES,
1900
+ });
1901
+ if (operation) return operation;
1697
1902
  throw cliValidationError(
1698
1903
  "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
1904
  );
@@ -1704,54 +1909,75 @@ export function parseAdminOperation(options) {
1704
1909
  namespace === "cardcraft-reviews" ||
1705
1910
  namespace === "cardcraft"
1706
1911
  ) {
1707
- // AI Card crafting recipes (cardcraft.json). Same reviewer as rewards and,
1708
- // like rewards, never part of a delegated daily run.
1912
+ // Alias of `lumine admin review ... cardcraft:<id>`.
1913
+ const operation = buildReviewRequestOperation({
1914
+ action,
1915
+ target,
1916
+ options,
1917
+ fixedType: "cardcraft",
1918
+ command: "lumine admin cardcraft-review",
1919
+ listStatuses: REWARD_REVIEW_STATUSES,
1920
+ });
1921
+ if (operation) return operation;
1922
+ throw cliValidationError(
1923
+ "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>.",
1924
+ );
1925
+ }
1926
+
1927
+ if (STORAGE_LIMIT_NAMESPACES.has(namespace)) {
1928
+ // Lumine file storage (Twinkle.files) unlocks: per-creator approvals past
1929
+ // the default quota. Mikey's own decision, any time, never in a daily run.
1930
+ const usage =
1931
+ "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>].";
1932
+ const reason = String(options.adminReason || "").trim();
1933
+ if (reason.length > 1000) {
1934
+ throw cliValidationError("--reason must be at most 1000 characters.");
1935
+ }
1709
1936
  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
- );
1937
+ // Alias of `lumine admin review list --type storage`.
1938
+ return buildReviewRequestOperation({
1939
+ action,
1940
+ target,
1941
+ options,
1942
+ fixedType: "storage-limit",
1943
+ command: "lumine admin storage",
1944
+ listStatuses: ["pending", "approved", "rejected", "all"],
1945
+ });
1724
1946
  }
1725
1947
  if (action === "show" || action === "get") {
1726
- const reviewId = parseRequiredInteger(target, "Card crafting review ID", 1);
1948
+ const user = parseStorageTargetUser(target);
1727
1949
  return readOperation(
1728
- "cardcraft-review.show",
1729
- `/cli/admin/cardcraft-reviews/${reviewId}`,
1730
- { requiresRun: false, reviewId },
1950
+ "storage.show",
1951
+ `/cli/admin/storage-limits/users/${encodeURIComponent(user)}`,
1952
+ { requiresRun: false },
1731
1953
  );
1732
1954
  }
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) {
1955
+ if (action === "approve" || action === "reject") {
1956
+ // Alias of `lumine admin review approve|reject storage:<id>`.
1957
+ return buildReviewRequestOperation({
1958
+ action,
1959
+ target,
1960
+ options,
1961
+ fixedType: "storage-limit",
1962
+ command: "lumine admin storage",
1963
+ });
1964
+ }
1965
+ if (action === "grant" || action === "set") {
1966
+ const user = parseStorageTargetUser(target);
1967
+ if (!options.storageSize) {
1737
1968
  throw cliValidationError(
1738
- `lumine admin cardcraft-review ${action} <id> needs --reason <text> the creator will read.`,
1969
+ "lumine admin storage grant <user> needs --size <500MB|1GB|2GB|n MB>.",
1739
1970
  );
1740
1971
  }
1741
- if (reason.length > 1000) {
1742
- throw cliValidationError("--reason must be at most 1000 characters.");
1743
- }
1744
1972
  return writeOperation(
1745
- "cardcraft-review.decide",
1746
- "POST",
1747
- `/cli/admin/cardcraft-reviews/${reviewId}`,
1748
- { decision: action, reason },
1749
- { requiresRun: false, reviewId, decision: action },
1973
+ "storage.grant",
1974
+ "PUT",
1975
+ `/cli/admin/storage-limits/users/${encodeURIComponent(user)}`,
1976
+ { sizeBytes: parseAdminStorageSize(options.storageSize), reason },
1977
+ { requiresRun: false },
1750
1978
  );
1751
1979
  }
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
- );
1980
+ throw cliValidationError(usage);
1755
1981
  }
1756
1982
 
1757
1983
  if (namespace === "chat-reports" || namespace === "chat-report") {
@@ -4458,6 +4684,190 @@ function printCardCraftReviewResult({ operation, data }) {
4458
4684
  );
4459
4685
  }
4460
4686
 
4687
+ const STORAGE_LIMIT_NAMESPACES = new Set([
4688
+ "storage",
4689
+ "storage-limit",
4690
+ "storage-limits",
4691
+ "storage-request",
4692
+ "storage-requests",
4693
+ ]);
4694
+
4695
+ function parseStorageTargetUser(value) {
4696
+ const user = String(value || "")
4697
+ .trim()
4698
+ .replace(/^@/, "");
4699
+ if (!user || !/^[\w.-]+$/.test(user)) {
4700
+ throw cliValidationError("Pass a user id or username.");
4701
+ }
4702
+ return user;
4703
+ }
4704
+
4705
+ function parseAdminStorageSize(value) {
4706
+ try {
4707
+ return parseStorageSizeBytes(value);
4708
+ } catch (error) {
4709
+ throw cliValidationError(error.message);
4710
+ }
4711
+ }
4712
+
4713
+ function formatStorageRequestLine(request) {
4714
+ const approved = request.approvedMaxRuntimeFileStorageBytes
4715
+ ? ` → ${formatBytes(request.approvedMaxRuntimeFileStorageBytes)}`
4716
+ : "";
4717
+ 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}` : ""}`;
4718
+ }
4719
+
4720
+ function printStorageLimitResult({ operation, data, result }) {
4721
+ if (operation.name === "storage.list") {
4722
+ const requests = Array.isArray(data.requests) ? data.requests : [];
4723
+ console.log(
4724
+ `${requests.length} storage request(s) (${data.filter || "pending"}):`,
4725
+ );
4726
+ for (const request of requests)
4727
+ console.log(` ${formatStorageRequestLine(request)}`);
4728
+ if (data.nextCursor) console.log(`More: --cursor ${data.nextCursor}`);
4729
+ return;
4730
+ }
4731
+ if (operation.name === "storage.show") {
4732
+ const storage = data.storage || {};
4733
+ console.log(
4734
+ `${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)}).`,
4735
+ );
4736
+ if (data.override) {
4737
+ console.log(
4738
+ `Approved override: ${formatBytes(data.override.maxRuntimeFileStorageBytes)} by user ${data.override.approvedByUserId}${data.override.reason ? ` · ${data.override.reason}` : ""}`,
4739
+ );
4740
+ }
4741
+ for (const request of data.recentRequests || [])
4742
+ console.log(` ${formatStorageRequestLine(request)}`);
4743
+ return;
4744
+ }
4745
+ const limit = formatBytes(data.maxRuntimeFileStorageBytes);
4746
+ if (operation.name === "storage.decide") {
4747
+ const request = data.request || {};
4748
+ console.log(
4749
+ result?.changed === false
4750
+ ? `Storage request #${request.requestId} was already ${request.status}. ${request.username || "The creator"}'s limit is ${limit}.`
4751
+ : `Storage request #${request.requestId} ${request.status}. ${request.username || "The creator"}'s Lumine file storage limit is now ${limit}.`,
4752
+ );
4753
+ return;
4754
+ }
4755
+ if (operation.name === "storage.grant") {
4756
+ console.log(
4757
+ result?.changed === false
4758
+ ? `${data.user?.username || "The user"} (${data.user?.id}) already has ${limit} of Lumine file storage.`
4759
+ : `${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.` : ""}`,
4760
+ );
4761
+ }
4762
+ }
4763
+
4764
+ function formatReviewAge(seconds) {
4765
+ const at = Number(seconds || 0);
4766
+ if (!at) return "";
4767
+ const minutes = Math.max(0, Math.round((Date.now() / 1000 - at) / 60));
4768
+ if (minutes < 60) return `${minutes}m ago`;
4769
+ const hours = Math.round(minutes / 60);
4770
+ if (hours < 48) return `${hours}h ago`;
4771
+ return `${Math.round(hours / 24)}d ago`;
4772
+ }
4773
+
4774
+ function formatReviewItemLine(item) {
4775
+ const where =
4776
+ item.appTitle || (item.type === "storage-limit" ? "all their Builds" : "");
4777
+ return [
4778
+ `${item.ref} · ${item.status}${item.details?.isLive ? " (live)" : ""}`,
4779
+ REVIEW_REQUEST_LABELS[item.type] || item.type,
4780
+ `${where ? `${where} · ` : ""}by ${item.requesterUsername || "user"} (${item.requesterId})`,
4781
+ item.summary,
4782
+ item.reason ? `"${item.reason}"` : "",
4783
+ item.reviewReason && item.status !== "pending" ? `note: ${item.reviewReason}` : "",
4784
+ formatReviewAge(item.createdAt),
4785
+ ]
4786
+ .filter(Boolean)
4787
+ .join(" · ");
4788
+ }
4789
+
4790
+ function printReviewEvents(events) {
4791
+ for (const event of events || [])
4792
+ console.log(
4793
+ ` ${new Date(Number(event.createdAt || 0) * 1000).toISOString()} ${event.action} by ${event.actorId}${event.reason ? `: ${event.reason}` : ""}`,
4794
+ );
4795
+ }
4796
+
4797
+ function printReviewRequestResult({ operation, data, result }) {
4798
+ if (operation.name === "review.list") {
4799
+ const items = Array.isArray(data.items) ? data.items : [];
4800
+ const scope = operation.reviewType
4801
+ ? REVIEW_REQUEST_LABELS[operation.reviewType]
4802
+ : "Build";
4803
+ console.log(`${items.length} ${scope} request(s) (${data.filter || "pending"}):`);
4804
+ for (const item of items) console.log(` ${formatReviewItemLine(item)}`);
4805
+ if (data.nextCursor) console.log(`More: --cursor ${data.nextCursor}`);
4806
+ if (items.some((item) => item.decisions?.includes("approve")))
4807
+ console.log(
4808
+ "Decide: lumine admin review approve|reject <ref> (storage: --size; rewards: --config; cardcraft/rewards reject needs --reason).",
4809
+ );
4810
+ return;
4811
+ }
4812
+ const type = operation.reviewType;
4813
+ // Rewards and card crafting keep their detailed printers.
4814
+ if (type === "rewards") {
4815
+ printRewardReviewResult({
4816
+ operation: {
4817
+ ...operation,
4818
+ name:
4819
+ operation.name === "review.decide"
4820
+ ? "reward-review.decide"
4821
+ : "reward-review.show",
4822
+ },
4823
+ data,
4824
+ });
4825
+ return;
4826
+ }
4827
+ if (type === "cardcraft") {
4828
+ printCardCraftReviewResult({
4829
+ operation: {
4830
+ ...operation,
4831
+ name:
4832
+ operation.name === "review.decide"
4833
+ ? "cardcraft-review.decide"
4834
+ : "cardcraft-review.show",
4835
+ },
4836
+ data,
4837
+ });
4838
+ return;
4839
+ }
4840
+ if (operation.name === "review.decide" && type === "storage-limit") {
4841
+ printStorageLimitResult({
4842
+ operation: { ...operation, name: "storage.decide" },
4843
+ data,
4844
+ result,
4845
+ });
4846
+ return;
4847
+ }
4848
+ if (data.item) console.log(formatReviewItemLine(data.item));
4849
+ if (operation.name === "review.decide") {
4850
+ console.log(
4851
+ result?.changed === false
4852
+ ? `Request ${data.item?.ref || ""} was already ${data.item?.status || data.request?.status}.`
4853
+ : `Decision recorded: ${operation.decision}.${type === "project-limit" && operation.decision === "approve" ? " Main and all its branches have the room now." : ""}`,
4854
+ );
4855
+ return;
4856
+ }
4857
+ if (type === "storage-limit" && data.storage) {
4858
+ const storage = data.storage;
4859
+ console.log(
4860
+ `Now: ${formatBytes(storage.runtimeFileStorageBytes)} used of ${formatBytes(storage.maxRuntimeFileStorageBytes)} across ${Number(storage.runtimeFileCount || 0)} file(s) (default ${formatBytes(storage.defaultMaxRuntimeFileStorageBytes)}, cap ${formatBytes(storage.maxApprovableRuntimeFileStorageBytes)}).`,
4861
+ );
4862
+ }
4863
+ if (type === "project-limit" && data.currentLimits) {
4864
+ console.log(
4865
+ `Now: ${data.currentLimits.maxFilesPerProject} files, ${formatBytes(data.currentLimits.maxProjectBytes)} (Main; every branch inherits it).`,
4866
+ );
4867
+ }
4868
+ printReviewEvents(data.events);
4869
+ }
4870
+
4461
4871
  function printAdminResult({ operation, result }) {
4462
4872
  const data = result?.data || {};
4463
4873
  if (printChatSafetyResult({ operation, data })) return;
@@ -4465,6 +4875,10 @@ function printAdminResult({ operation, result }) {
4465
4875
  printRewardActivity(data);
4466
4876
  return;
4467
4877
  }
4878
+ if (operation.name.startsWith("review.")) {
4879
+ printReviewRequestResult({ operation, data, result });
4880
+ return;
4881
+ }
4468
4882
  if (operation.name.startsWith("reward-review.")) {
4469
4883
  printRewardReviewResult({ operation, data });
4470
4884
  return;
@@ -4473,6 +4887,10 @@ function printAdminResult({ operation, result }) {
4473
4887
  printCardCraftReviewResult({ operation, data });
4474
4888
  return;
4475
4889
  }
4890
+ if (operation.name.startsWith("storage.")) {
4891
+ printStorageLimitResult({ operation, data, result });
4892
+ return;
4893
+ }
4476
4894
  if (operation.name === "runtime.evidence") {
4477
4895
  const evidence = data.evidence || {};
4478
4896
  console.log(