@stage5/lumine 0.2.20 → 0.2.21
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 +110 -1
- package/lib/commands.js +4 -0
- package/package.json +1 -1
- package/sdk/LUMINE_ADMIN.md +139 -0
package/lib/admin.js
CHANGED
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { assertAuthScope, resolveAuth } from "./auth.js";
|
|
3
4
|
import { requestJson } from "./http.js";
|
|
4
5
|
|
|
6
|
+
const MAX_EDITORIAL_FILE_BYTES = 256 * 1024;
|
|
7
|
+
|
|
8
|
+
function readEditorialFile(filePath) {
|
|
9
|
+
const normalizedPath = String(filePath || "").trim();
|
|
10
|
+
if (!normalizedPath) {
|
|
11
|
+
throw cliValidationError(
|
|
12
|
+
"Pass the editorial JSON with --file <editorial.json>.",
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
let contents;
|
|
16
|
+
try {
|
|
17
|
+
contents = readFileSync(normalizedPath, "utf8");
|
|
18
|
+
} catch {
|
|
19
|
+
throw cliValidationError(`Could not read ${normalizedPath}.`);
|
|
20
|
+
}
|
|
21
|
+
if (Buffer.byteLength(contents, "utf8") > MAX_EDITORIAL_FILE_BYTES) {
|
|
22
|
+
throw cliValidationError("The editorial file must be under 256KB.");
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(contents);
|
|
26
|
+
} catch {
|
|
27
|
+
throw cliValidationError(`${normalizedPath} is not valid JSON.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
5
31
|
export async function adminCommand(options) {
|
|
6
32
|
const operation = parseAdminOperation(options);
|
|
7
33
|
const recommendationContentTypes =
|
|
@@ -374,6 +400,51 @@ export function parseAdminOperation(options) {
|
|
|
374
400
|
return recommendOperation(action, options);
|
|
375
401
|
}
|
|
376
402
|
|
|
403
|
+
if (namespace === "news") {
|
|
404
|
+
if (!action || action === "status") {
|
|
405
|
+
return readOperation("news.status", "/cli/admin/news");
|
|
406
|
+
}
|
|
407
|
+
if (action === "print") {
|
|
408
|
+
return writeOperation("news.print", "POST", "/cli/admin/news/print", {});
|
|
409
|
+
}
|
|
410
|
+
if (action === "claim") {
|
|
411
|
+
const repairDate = String(options.adminDate || "").trim();
|
|
412
|
+
if (repairDate && !/^\d{4}-\d{2}-\d{2}$/.test(repairDate)) {
|
|
413
|
+
throw cliValidationError("--date must be YYYY-MM-DD.");
|
|
414
|
+
}
|
|
415
|
+
return writeOperation("news.claim", "POST", "/cli/admin/news/claim", {
|
|
416
|
+
...(repairDate ? { date: repairDate } : {}),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
if (action === "submit") {
|
|
420
|
+
const editionId = parseRequiredInteger(
|
|
421
|
+
options.adminEditionId,
|
|
422
|
+
"--edition-id",
|
|
423
|
+
1,
|
|
424
|
+
);
|
|
425
|
+
const leaseToken = String(options.adminLeaseToken || "").trim();
|
|
426
|
+
if (!leaseToken) {
|
|
427
|
+
throw cliValidationError(
|
|
428
|
+
"Pass the claim's lease token with --lease-token <token>.",
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
return writeOperation(
|
|
432
|
+
"news.submit",
|
|
433
|
+
"POST",
|
|
434
|
+
"/cli/admin/news/submit",
|
|
435
|
+
{
|
|
436
|
+
editionId,
|
|
437
|
+
leaseToken,
|
|
438
|
+
editorial: readEditorialFile(options.adminFile),
|
|
439
|
+
model: options.model || undefined,
|
|
440
|
+
},
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
throw cliValidationError(
|
|
444
|
+
"Usage: lumine admin news [status] | news print | news claim | news submit --edition-id <id> --lease-token <token> --file <editorial.json>",
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
377
448
|
if (namespace === "audit" && (!action || action === "list")) {
|
|
378
449
|
const runFilter = String(options.adminRun || "").trim();
|
|
379
450
|
if (runFilter && !["current", "last"].includes(runFilter)) {
|
|
@@ -432,7 +503,7 @@ export function parseAdminOperation(options) {
|
|
|
432
503
|
}
|
|
433
504
|
|
|
434
505
|
throw cliValidationError(
|
|
435
|
-
"Usage: lumine admin identity|daily-run|recommendations|post|subjects|subject|featured|comment|audit ...",
|
|
506
|
+
"Usage: lumine admin identity|daily-run|recommendations|post|subjects|subject|featured|comment|news|audit ...",
|
|
436
507
|
);
|
|
437
508
|
}
|
|
438
509
|
|
|
@@ -799,6 +870,44 @@ function printAdminResult({ operation, result }) {
|
|
|
799
870
|
printPagination(data.pagination);
|
|
800
871
|
return;
|
|
801
872
|
}
|
|
873
|
+
if (data.claim) {
|
|
874
|
+
console.log(
|
|
875
|
+
`Claimed edition #${data.claim.editionId} (${data.claim.dateKey}): ${data.claim.events.length} event(s); lease token ${data.claim.leaseToken}.`,
|
|
876
|
+
);
|
|
877
|
+
console.log(
|
|
878
|
+
`Write the editorial JSON, then run: lumine admin news submit --edition-id ${data.claim.editionId} --lease-token ${data.claim.leaseToken} --file editorial.json`,
|
|
879
|
+
);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (data.newspaper) {
|
|
883
|
+
const paper = data.newspaper;
|
|
884
|
+
if (paper.printedToday) {
|
|
885
|
+
const printed = paper.latestPrinted || {};
|
|
886
|
+
console.log(
|
|
887
|
+
`Newspaper ${paper.dateKey}: printed (revision ${printed.revisionNumber || 1}, ${printed.sourceEventCount ?? 0} sources).`,
|
|
888
|
+
);
|
|
889
|
+
} else {
|
|
890
|
+
console.log(
|
|
891
|
+
`Newspaper ${paper.dateKey}: not printed (${paper.generationStatus}).`,
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
if (paper.requestedAction && paper.requestedAction !== "none") {
|
|
895
|
+
console.log(
|
|
896
|
+
`${paper.requestedAction === "retry" ? "Queued a retry of" : "Queued"} today's edition; the press typesets it within about a minute. Re-check with: lumine admin news`,
|
|
897
|
+
);
|
|
898
|
+
} else if (
|
|
899
|
+
!paper.printedToday &&
|
|
900
|
+
["pending", "generating"].includes(paper.generationStatus)
|
|
901
|
+
) {
|
|
902
|
+
console.log(
|
|
903
|
+
"An edition is being typeset now. Re-check with: lumine admin news",
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
if (paper.failureMessage && !paper.printedToday) {
|
|
907
|
+
console.log(`Last attempt failed: ${paper.failureMessage}`);
|
|
908
|
+
}
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
802
911
|
if (data.skip) {
|
|
803
912
|
console.log(
|
|
804
913
|
`${result.status}: ${data.skip.contentType}:${data.skip.contentId} skipped.`,
|
package/lib/commands.js
CHANGED
|
@@ -2187,6 +2187,10 @@ export function parseArgs(args) {
|
|
|
2187
2187
|
adminRun: raw.run ? String(raw.run) : "",
|
|
2188
2188
|
adminTarget: raw.target ? String(raw.target) : "",
|
|
2189
2189
|
adminActions: raw.actions ? String(raw.actions) : "",
|
|
2190
|
+
adminDate: raw.date ? String(raw.date) : "",
|
|
2191
|
+
adminEditionId: raw.editionId ? String(raw.editionId) : "",
|
|
2192
|
+
adminLeaseToken: raw.leaseToken ? String(raw.leaseToken) : "",
|
|
2193
|
+
adminFile: raw.file ? String(raw.file) : "",
|
|
2190
2194
|
adminFull: parseBoolean(raw.full, false),
|
|
2191
2195
|
rewardTwinkles: raw.rewardTwinkles ? String(raw.rewardTwinkles) : "",
|
|
2192
2196
|
includeComments: parseBoolean(raw.includeComments, false),
|
package/package.json
CHANGED
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -711,6 +711,145 @@ type PostSkip = Success<{
|
|
|
711
711
|
}>;
|
|
712
712
|
```
|
|
713
713
|
|
|
714
|
+
## Twinkle Newspaper
|
|
715
|
+
|
|
716
|
+
```bash
|
|
717
|
+
lumine admin news --json
|
|
718
|
+
lumine admin news claim --json
|
|
719
|
+
lumine admin news submit --edition-id 42 --lease-token <token> \
|
|
720
|
+
--file editorial.json --model "Claude" --json
|
|
721
|
+
lumine admin news print --json
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
The Twinkle Newspaper (Build app 1929) is normally printed by a community
|
|
725
|
+
member spending their own AI Energy. Making sure today's paper exists is part
|
|
726
|
+
of every delegated website-management run: check `lumine admin news` early in
|
|
727
|
+
the run, and if `printedToday` is false with no edition `pending` or
|
|
728
|
+
`generating`, print it.
|
|
729
|
+
|
|
730
|
+
**Preferred: write the editorial yourself.** `news claim` reserves today's
|
|
731
|
+
edition under the server's generation lease and returns the exact canonical
|
|
732
|
+
event digest the server would otherwise send to its own model, so no provider
|
|
733
|
+
API credits are spent. Write a `GeneratedEditorial` JSON and send it back with
|
|
734
|
+
`news submit` within the ten-minute lease. The server treats the editorial as
|
|
735
|
+
untrusted regardless of author: every story must cite an exact `eventKey`
|
|
736
|
+
from the digest, front-page `sourceQuote`s must be verbatim contiguous
|
|
737
|
+
passages of the cited event's summary (invalid quotes are replaced with
|
|
738
|
+
canonical text), section and page layout are server-enforced, announcements
|
|
739
|
+
are appended verbatim outside your output, and source visibility is
|
|
740
|
+
re-checked transactionally at commit.
|
|
741
|
+
|
|
742
|
+
```ts
|
|
743
|
+
type GeneratedEditorial = {
|
|
744
|
+
mastheadHeadline: string;
|
|
745
|
+
mastheadDeck: string;
|
|
746
|
+
lead: { eventKey: string; headline: string; summary: string; sourceQuote: string } | null;
|
|
747
|
+
stories: Array<{ eventKey: string; headline: string; summary: string; sourceQuote: string }>;
|
|
748
|
+
editorsNote: string;
|
|
749
|
+
};
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
Editorial rules (the same ones the server's own model works under): use only
|
|
753
|
+
the supplied events — never world news, invented names, invented statistics,
|
|
754
|
+
or unsupported claims. Subjects and shared Daily Reflections are the primary
|
|
755
|
+
authored material; only a `section: "front"` event may be the lead. Preserve
|
|
756
|
+
substance, names, and numbers. Do not mention official announcements (the
|
|
757
|
+
server adds them), and give non-front events an empty `sourceQuote`.
|
|
758
|
+
|
|
759
|
+
Claim edge cases: a quiet day (no editorial events) is committed as the
|
|
760
|
+
canonical quiet edition at claim time — no editorial needed, the response
|
|
761
|
+
says so. If the claim is not submitted before the lease expires, the server's
|
|
762
|
+
press worker falls back to generating the edition itself. `news submit`
|
|
763
|
+
failing with `CLI_ADMIN_NEWS_CLAIM_LOST` means the lease was superseded —
|
|
764
|
+
re-check `lumine admin news` and claim again only if the paper still needs
|
|
765
|
+
printing.
|
|
766
|
+
|
|
767
|
+
**Repairing a past edition.** `news claim --date YYYY-MM-DD` leases an
|
|
768
|
+
already-printed historical edition and returns a fresh digest of its original
|
|
769
|
+
coverage window (primary Subjects/Reflections are re-projected from canonical
|
|
770
|
+
tables, and anything since deleted or made private drops out). Submitting
|
|
771
|
+
appends the next revision — every prior press run stays browsable in the
|
|
772
|
+
archive, and repairs never re-notify subscribers (only a day's first revision
|
|
773
|
+
does). Today's edition is never repaired this way; refreshing today is the
|
|
774
|
+
Newspaper owner's website-only action. Repair only when an edition is
|
|
775
|
+
genuinely degraded (missing masthead, missing lead, empty pages), not to
|
|
776
|
+
rewrite history editorially.
|
|
777
|
+
|
|
778
|
+
**Fallback: queue the server's own model.** `news print` reserves the edition
|
|
779
|
+
and lets the server's press worker write it (spends provider credits). It is
|
|
780
|
+
idempotent per day: it queues a new edition when today has none, requeues a
|
|
781
|
+
retry when today's only attempts failed, and returns `already_done` when the
|
|
782
|
+
paper is printed or being typeset.
|
|
783
|
+
|
|
784
|
+
Neither path ever reprints or refreshes an already-printed edition —
|
|
785
|
+
refreshing is the Newspaper owner's website-only action. The acting bot is
|
|
786
|
+
recorded as the requester, and the management bots are exempt from AI Energy
|
|
787
|
+
for newspaper generation: the platform absorbs the cost, exactly like their
|
|
788
|
+
coin-exempt recommends and rewards. When a day's first edition is printed,
|
|
789
|
+
the server notifies the app's notification subscribers (users can mute the
|
|
790
|
+
app or unsubscribe in the app; the bots never need to send anything). All
|
|
791
|
+
three mutations require the `news:print` scope (in every run's base scopes)
|
|
792
|
+
and are audited as `news.print` / `news.claim` / `news.submit` against
|
|
793
|
+
`news_edition` targets.
|
|
794
|
+
|
|
795
|
+
```ts
|
|
796
|
+
type NewsStatus = Success<{
|
|
797
|
+
newspaper: {
|
|
798
|
+
dayIndex: number;
|
|
799
|
+
dateKey: string; // YYYY-MM-DD
|
|
800
|
+
printedToday: boolean;
|
|
801
|
+
generationStatus:
|
|
802
|
+
| "available" // no edition requested today
|
|
803
|
+
| "pending"
|
|
804
|
+
| "generating"
|
|
805
|
+
| "ready"
|
|
806
|
+
| "failed";
|
|
807
|
+
failureMessage: string | null;
|
|
808
|
+
attempts: number | null;
|
|
809
|
+
latestPrinted: {
|
|
810
|
+
dayIndex: number;
|
|
811
|
+
dateKey: string;
|
|
812
|
+
generatedAt: number | null;
|
|
813
|
+
sourceEventCount: number;
|
|
814
|
+
revisionNumber: number;
|
|
815
|
+
} | null; // most recent printed edition, possibly a previous day
|
|
816
|
+
nextEditionAt: number;
|
|
817
|
+
printDecision: "already_printed" | "in_progress" | "retry" | "create";
|
|
818
|
+
requestedAction?: "none" | "retry" | "create"; // print responses only
|
|
819
|
+
};
|
|
820
|
+
}>;
|
|
821
|
+
|
|
822
|
+
type NewsPrint = NewsStatus; // "success" (queued) or "already_done"
|
|
823
|
+
|
|
824
|
+
type NewsClaim = Success<{
|
|
825
|
+
newspaper: NewsStatus["data"]["newspaper"] & {
|
|
826
|
+
quietEditionPrinted?: boolean;
|
|
827
|
+
};
|
|
828
|
+
claim: {
|
|
829
|
+
editionId: number;
|
|
830
|
+
dayIndex: number;
|
|
831
|
+
dateKey: string;
|
|
832
|
+
leaseToken: string;
|
|
833
|
+
leaseExpiresAt: number;
|
|
834
|
+
coverage: { startedAt: number; endedAt: number };
|
|
835
|
+
maxSourceQuoteLength: number;
|
|
836
|
+
announcementCount: number;
|
|
837
|
+
events: Array<{
|
|
838
|
+
eventKey: string;
|
|
839
|
+
kind: string;
|
|
840
|
+
section: string; // front | community | notices | scores | marketplace
|
|
841
|
+
occurredAt: number;
|
|
842
|
+
priority: number;
|
|
843
|
+
title: string;
|
|
844
|
+
summary: string;
|
|
845
|
+
payload: unknown; // may include author and canonical topComments
|
|
846
|
+
}>;
|
|
847
|
+
} | null; // null: already printed/typesetting, or the quiet edition auto-committed
|
|
848
|
+
}>;
|
|
849
|
+
|
|
850
|
+
type NewsSubmit = NewsStatus; // "success"; newspaper includes revisionNumber
|
|
851
|
+
```
|
|
852
|
+
|
|
714
853
|
## Audit history
|
|
715
854
|
|
|
716
855
|
```bash
|