@stage5/lumine 0.3.73 → 0.3.74
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 +11 -3
- package/lib/admin-arena.js +4 -2
- package/lib/math-lab-build-sheet.js +32 -23
- package/lib/sdk.js +59 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -305,10 +305,11 @@ viewer sees (by default the MCP client's own name).
|
|
|
305
305
|
## Inspecting Build SDK data
|
|
306
306
|
|
|
307
307
|
`lumine sdk call <namespace.method> '<jsonArgs>'` calls a build's data SDK
|
|
308
|
-
endpoint
|
|
308
|
+
endpoint and prints the response, so you can inspect real data
|
|
309
309
|
and measure latency while building. Run `lumine sdk list` to see callable
|
|
310
|
-
methods.
|
|
311
|
-
`TWINKLE_BUILD_SDK.md`).
|
|
310
|
+
methods. Most methods use your login and send JSON args as the request body
|
|
311
|
+
(shapes follow `TWINKLE_BUILD_SDK.md`). Newspaper reads use the same public
|
|
312
|
+
GET routes as the app and send no login or Build API token.
|
|
312
313
|
|
|
313
314
|
```bash
|
|
314
315
|
lumine sdk call aiStories.chapters '{"limit": 5}'
|
|
@@ -317,6 +318,9 @@ lumine sdk call live.list '{}'
|
|
|
317
318
|
lumine sdk call live.get '{"sessionId": "..."}'
|
|
318
319
|
lumine sdk call live.listReplays '{"limit": 20}'
|
|
319
320
|
lumine sdk call live.getReplay '{"replayId": "..."}'
|
|
321
|
+
lumine sdk call news.getCurrentEdition '{}' --build 1929
|
|
322
|
+
lumine sdk call news.listEditions '{"limit": 12}' --build 1929
|
|
323
|
+
lumine sdk call news.getEdition '{"dayIndex": 1762, "revisionNumber": 1}' --build 1929
|
|
320
324
|
```
|
|
321
325
|
|
|
322
326
|
It targets the build in the current workspace, or pass `--build <id>`. Add
|
|
@@ -324,6 +328,10 @@ It targets the build in the current workspace, or pass `--build <id>`. Add
|
|
|
324
328
|
which can differ from a method's `Twinkle.*` SDK return shape — check
|
|
325
329
|
`TWINKLE_BUILD_SDK.md` for SDK return shapes. Methods that change data require
|
|
326
330
|
`--allow-write`.
|
|
331
|
+
For newspaper reads, omit `revisionNumber` to read the day's canonical revision;
|
|
332
|
+
pass the archive's `cursor` to `news.listEditions` for the next page. These reads
|
|
333
|
+
never queue generation or refresh an edition; publication uses the separate
|
|
334
|
+
authorized newspaper workflow.
|
|
327
335
|
Stopping a hosted stream is available as
|
|
328
336
|
`lumine sdk call live.stop '{"sessionId":"..."}' --allow-write`; the command
|
|
329
337
|
prints canonical server state and refuses to mint `live:write` without the
|
package/lib/admin-arena.js
CHANGED
|
@@ -186,12 +186,14 @@ export function formatArenaResult({ operation, data }) {
|
|
|
186
186
|
...types.map(
|
|
187
187
|
(t) =>
|
|
188
188
|
`${t.examFamily} ${t.difficulty || "normal"} ${t.itemType}: ${t.live} live, ${t.activeLearners ?? "unknown"} active learner(s), ${t.minFresh} unseen${
|
|
189
|
-
t.
|
|
189
|
+
t.refillUse === "complete-paper-only" ? " (complete paper required; check paper-queue)"
|
|
190
|
+
: t.refillUse === "unknown-type" ? " (unknown current type; review before authoring)"
|
|
191
|
+
: t.stockLow && !t.refillDue && t.activeLearners === 0 ? " (low stock; no active demand)" : ""
|
|
190
192
|
}${
|
|
191
193
|
t.refillDue ? ` -> write ${t.toWrite}` : ""
|
|
192
194
|
}`,
|
|
193
195
|
),
|
|
194
|
-
due.length ? `${due.length} type(s) due for a refill.` : "No refill due.",
|
|
196
|
+
due.length ? `${due.length} type(s) due for a refill.` : "No standalone practice refill due; check paper-queue for waiting exams.",
|
|
195
197
|
];
|
|
196
198
|
}
|
|
197
199
|
if (operation.name === "arena.import.preview") {
|
|
@@ -14,11 +14,37 @@ import {
|
|
|
14
14
|
verifyRewardEconomy,
|
|
15
15
|
} from "./math-lab-preservation.js";
|
|
16
16
|
|
|
17
|
+
export function serializeMathLabSets(sets) {
|
|
18
|
+
return sets.map((set) => ({
|
|
19
|
+
key: set.key,
|
|
20
|
+
// This also makes a set eligible as a rematch of the named puzzle.
|
|
21
|
+
// It remains an ordinary collection puzzle for other learners.
|
|
22
|
+
// Preserve it for existing and appended sets; let canonical validation
|
|
23
|
+
// reject invalid references instead of silently changing their meaning.
|
|
24
|
+
...(set.variantOf !== undefined ? { variantOf: set.variantOf } : {}),
|
|
25
|
+
questions: set.questions.map((q) => ({
|
|
26
|
+
prompt: q.prompt,
|
|
27
|
+
answer: q.answer,
|
|
28
|
+
...(q.hint ? { hint: q.hint } : {}),
|
|
29
|
+
...(q.guide ? { guide: q.guide } : {}),
|
|
30
|
+
})),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
|
|
17
34
|
export async function buildMathLabSheet({
|
|
18
35
|
contentDir,
|
|
36
|
+
apiRoot,
|
|
37
|
+
appDir,
|
|
38
|
+
outputFile,
|
|
19
39
|
args = process.argv.slice(2),
|
|
20
40
|
}) {
|
|
21
41
|
const here = path.resolve(contentDir);
|
|
42
|
+
// Private review directories need not imitate the historical sibling layout.
|
|
43
|
+
// Defaults preserve existing wrappers; explicit paths retain the same guards.
|
|
44
|
+
const policyRoot = path.resolve(
|
|
45
|
+
apiRoot || path.join(here, "..", "..", "twinkle-api"),
|
|
46
|
+
);
|
|
47
|
+
const declaredApp = path.resolve(appDir || path.join(here, "..", "math-lab"));
|
|
22
48
|
const parts = path.join(here, "parts");
|
|
23
49
|
if (
|
|
24
50
|
args.length !== 2 ||
|
|
@@ -59,18 +85,12 @@ export async function buildMathLabSheet({
|
|
|
59
85
|
}
|
|
60
86
|
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
61
87
|
sheet.rules[`${level}-daily`] = {
|
|
62
|
-
sets: data.sets
|
|
63
|
-
key: set.key,
|
|
64
|
-
questions: set.questions.map((q) => ({
|
|
65
|
-
prompt: q.prompt,
|
|
66
|
-
answer: q.answer,
|
|
67
|
-
...(q.hint ? { hint: q.hint } : {}),
|
|
68
|
-
...(q.guide ? { guide: q.guide } : {}),
|
|
69
|
-
})),
|
|
70
|
-
})),
|
|
88
|
+
sets: serializeMathLabSets(data.sets),
|
|
71
89
|
};
|
|
72
90
|
}
|
|
73
|
-
const out = path.resolve(
|
|
91
|
+
const out = path.resolve(
|
|
92
|
+
outputFile || path.join(here, "..", "math-lab-sheet.json"),
|
|
93
|
+
);
|
|
74
94
|
if (missing.length)
|
|
75
95
|
throw new Error(
|
|
76
96
|
`Missing grade parts: ${missing.join(", ")}. No sheet was written.`,
|
|
@@ -80,22 +100,11 @@ export async function buildMathLabSheet({
|
|
|
80
100
|
// Server-identical validation: rewards.json + sheet → approved-shape config.
|
|
81
101
|
const policy = await import(
|
|
82
102
|
pathToFileURL(
|
|
83
|
-
path.
|
|
84
|
-
here,
|
|
85
|
-
"..",
|
|
86
|
-
"..",
|
|
87
|
-
"twinkle-api",
|
|
88
|
-
"helpers",
|
|
89
|
-
"build",
|
|
90
|
-
"rewardsPolicy.ts",
|
|
91
|
-
),
|
|
103
|
+
path.join(policyRoot, "helpers", "build", "rewardsPolicy.ts"),
|
|
92
104
|
).href
|
|
93
105
|
);
|
|
94
106
|
const declared = JSON.parse(
|
|
95
|
-
fs.readFileSync(
|
|
96
|
-
path.resolve(here, "..", "math-lab", "rewards.json"),
|
|
97
|
-
"utf8",
|
|
98
|
-
),
|
|
107
|
+
fs.readFileSync(path.join(declaredApp, "rewards.json"), "utf8"),
|
|
99
108
|
);
|
|
100
109
|
const composed = policy.composeRewardConfig({ declared, sheet });
|
|
101
110
|
if (composed.errors.length) {
|
package/lib/sdk.js
CHANGED
|
@@ -13,6 +13,11 @@ import { findLocalProjectMetadata } from "./workspace.js";
|
|
|
13
13
|
// TWINKLE_BUILD_SDK.md. Methods that mutate data require --allow-write.
|
|
14
14
|
// Anything not listed can be called with --path api/<endpoint>.
|
|
15
15
|
export const SDK_CLI_METHODS = {
|
|
16
|
+
// Newspaper reads use the public GET routes, not POST /api/* or Build
|
|
17
|
+
// tokens. Generation remains a separate, explicitly authorized workflow.
|
|
18
|
+
"news.getCurrentEdition": { special: "newsRead", operation: "current", publicRead: true },
|
|
19
|
+
"news.listEditions": { special: "newsRead", operation: "list", publicRead: true },
|
|
20
|
+
"news.getEdition": { special: "newsRead", operation: "edition", publicRead: true },
|
|
16
21
|
"soundtracks.searchSources": { path: "api/soundtracks/sources", scopes: ["sharedDb:read"] },
|
|
17
22
|
"soundtracks.getTags": { path: "api/soundtracks/tags", scopes: ["sharedDb:read"] },
|
|
18
23
|
"soundtracks.getRecommendations": { path: "api/soundtracks/recommendations", scopes: ["sharedDb:read"] },
|
|
@@ -458,7 +463,7 @@ export async function sdkCommand(options) {
|
|
|
458
463
|
|
|
459
464
|
export function printSdkMethodList() {
|
|
460
465
|
console.log(
|
|
461
|
-
"Callable Build endpoints (args JSON
|
|
466
|
+
"Callable Build endpoints (args JSON selects the documented request fields). Prints the raw\n" +
|
|
462
467
|
"endpoint response, which can differ from the SDK return shape (*).\n",
|
|
463
468
|
);
|
|
464
469
|
const byNamespace = new Map();
|
|
@@ -485,8 +490,10 @@ Usage:
|
|
|
485
490
|
lumine sdk call leaderboards.get '{"boardKey": "default", "limit": 10}'
|
|
486
491
|
lumine sdk call aiStories.list '{"difficulty": 1}' --repeat 5 --build 1374
|
|
487
492
|
lumine sdk call userDb.query '{"sql": "SELECT * FROM sqlite_master"}'
|
|
493
|
+
lumine sdk call news.getEdition '{"dayIndex": 1762, "revisionNumber": 1}' --build 1929
|
|
488
494
|
|
|
489
|
-
|
|
495
|
+
Argument shapes follow TWINKLE_BUILD_SDK.md; public newspaper reads use GET
|
|
496
|
+
without a login or Build token. Output is the raw
|
|
490
497
|
endpoint response; (*) methods are reshaped by their Twinkle.* SDK wrapper, so
|
|
491
498
|
check TWINKLE_BUILD_SDK.md for the real SDK return shape. Add --repeat <n> for
|
|
492
499
|
latency stats, --build <id> to target a build outside the current workspace.
|
|
@@ -570,6 +577,9 @@ export async function sdkCall(options) {
|
|
|
570
577
|
const requestedScopes = options.sdkScopes.length
|
|
571
578
|
? options.sdkScopes
|
|
572
579
|
: endpoint.scopes || [];
|
|
580
|
+
if (endpoint.publicRead && options.sdkScopes.length) {
|
|
581
|
+
throw new Error(`${methodName} is a public read and does not use --scopes.`);
|
|
582
|
+
}
|
|
573
583
|
// A curated readOnly method (rewards.getStatus) needs a scope whose name is
|
|
574
584
|
// write-capable but only ever sends its read operation; its own default
|
|
575
585
|
// scopes do not trip the gate. Explicit --scopes overrides always do.
|
|
@@ -597,12 +607,23 @@ export async function sdkCall(options) {
|
|
|
597
607
|
// before any auth side effects, so invalid local input fails immediately
|
|
598
608
|
// instead of after starting the device-login flow.
|
|
599
609
|
const buildId = await resolveSdkBuildId(options);
|
|
600
|
-
|
|
601
|
-
|
|
610
|
+
// Validate selectors before login/network calls. Public reads deliberately
|
|
611
|
+
// carry no saved login, even when one is available in this CLI session.
|
|
612
|
+
const newsUrl = endpoint.special === "newsRead"
|
|
613
|
+
? buildNewsReadUrl({ apiUrl: options.apiUrl, buildId, operation: endpoint.operation, args })
|
|
614
|
+
: null;
|
|
615
|
+
const auth = endpoint.publicRead ? null : await ensureAuth(options);
|
|
616
|
+
if (auth) await assertAuthScope({ options, auth, scope: "build:sdk" });
|
|
602
617
|
const attempts = [];
|
|
603
618
|
let lastResult = null;
|
|
604
619
|
|
|
605
|
-
if (endpoint.special === "
|
|
620
|
+
if (endpoint.special === "newsRead") {
|
|
621
|
+
for (let attempt = 0; attempt < options.repeat; attempt += 1) {
|
|
622
|
+
lastResult = await executeSdkHttpCall({ options, method: "GET", url: newsUrl });
|
|
623
|
+
attempts.push(lastResult);
|
|
624
|
+
printSdkAttemptLine(attempts.length, lastResult);
|
|
625
|
+
}
|
|
626
|
+
} else if (endpoint.special === "leaderboardGet" || endpoint.special === "leaderboardSubmit") {
|
|
606
627
|
const boardKey = String(args.boardKey || "default");
|
|
607
628
|
const rest = { ...args };
|
|
608
629
|
delete rest.boardKey;
|
|
@@ -742,6 +763,39 @@ export async function sdkCall(options) {
|
|
|
742
763
|
}
|
|
743
764
|
}
|
|
744
765
|
|
|
766
|
+
function buildNewsReadUrl({ apiUrl, buildId, operation, args }) {
|
|
767
|
+
const allowed = operation === "edition" ? ["dayIndex", "revisionNumber"]
|
|
768
|
+
: operation === "list" ? ["limit", "cursor"] : [];
|
|
769
|
+
if (Object.keys(args).some((key) => !allowed.includes(key))) {
|
|
770
|
+
throw new Error(`Unexpected newspaper selector; accepted fields: ${allowed.join(", ") || "none"}.`);
|
|
771
|
+
}
|
|
772
|
+
let suffix = "current";
|
|
773
|
+
if (operation === "edition") {
|
|
774
|
+
if (!Number.isSafeInteger(args.dayIndex) || args.dayIndex < 0) {
|
|
775
|
+
throw new Error("dayIndex is required and must be a nonnegative integer.");
|
|
776
|
+
}
|
|
777
|
+
if (args.revisionNumber != null && (!Number.isSafeInteger(args.revisionNumber) || args.revisionNumber <= 0)) {
|
|
778
|
+
throw new Error("revisionNumber must be a positive integer.");
|
|
779
|
+
}
|
|
780
|
+
suffix = `editions/${args.dayIndex}`;
|
|
781
|
+
} else if (operation === "list") {
|
|
782
|
+
if (args.limit != null && (!Number.isSafeInteger(args.limit) || args.limit < 1)) {
|
|
783
|
+
throw new Error("limit must be a positive integer (capped at 30).");
|
|
784
|
+
}
|
|
785
|
+
if (args.cursor != null && (typeof args.cursor !== "string" || args.cursor.length > 200)) {
|
|
786
|
+
throw new Error("cursor must be the string returned by news.listEditions.");
|
|
787
|
+
}
|
|
788
|
+
suffix = "editions";
|
|
789
|
+
}
|
|
790
|
+
const url = new URL(`${apiUrl}/build/${buildId}/news/${suffix}`);
|
|
791
|
+
for (const [key, value] of operation === "edition"
|
|
792
|
+
? [["revision", args.revisionNumber]] : operation === "list"
|
|
793
|
+
? [["limit", args.limit == null ? null : Math.min(30, args.limit)], ["cursor", args.cursor]] : []) {
|
|
794
|
+
if (value != null) url.searchParams.set(key, String(value));
|
|
795
|
+
}
|
|
796
|
+
return url.toString();
|
|
797
|
+
}
|
|
798
|
+
|
|
745
799
|
export function printSdkAttemptLine(attemptNumber, result) {
|
|
746
800
|
console.error(
|
|
747
801
|
`#${attemptNumber} ${result.status}${result.ok ? " OK" : ""} · ` +
|