@stage5/lumine 0.3.72 → 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 +61 -5
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +16 -3
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"] },
|
|
@@ -114,6 +119,8 @@ export const SDK_CLI_METHODS = {
|
|
|
114
119
|
"sharedDb.getTopics": { path: "api/shared-db/topics", scopes: ["sharedDb:read"] },
|
|
115
120
|
"sharedDb.createTopic": { path: "api/shared-db/topic", scopes: ["sharedDb:write"], write: true },
|
|
116
121
|
"sharedDb.getEntries": { path: "api/shared-db/entries", scopes: ["sharedDb:read"] },
|
|
122
|
+
"sharedDb.queryEntries": { path: "api/shared-db/entries", scopes: ["sharedDb:read"],
|
|
123
|
+
mapArgs: ({ authorUserId, fields, where, countBy, includeAuthor, ...args }) => ({ ...args, query: { authorUserId, fields, where, countBy, includeAuthor } }) },
|
|
117
124
|
"sharedDb.getEntriesByIds": { path: "api/shared-db/entries/by-ids", scopes: ["sharedDb:read"] },
|
|
118
125
|
"sharedDb.addEntry": { path: "api/shared-db/entry", scopes: ["sharedDb:write"], write: true },
|
|
119
126
|
"sharedDb.addEntries": { path: "api/shared-db/entries/batch", scopes: ["sharedDb:write"], write: true },
|
|
@@ -456,7 +463,7 @@ export async function sdkCommand(options) {
|
|
|
456
463
|
|
|
457
464
|
export function printSdkMethodList() {
|
|
458
465
|
console.log(
|
|
459
|
-
"Callable Build endpoints (args JSON
|
|
466
|
+
"Callable Build endpoints (args JSON selects the documented request fields). Prints the raw\n" +
|
|
460
467
|
"endpoint response, which can differ from the SDK return shape (*).\n",
|
|
461
468
|
);
|
|
462
469
|
const byNamespace = new Map();
|
|
@@ -483,8 +490,10 @@ Usage:
|
|
|
483
490
|
lumine sdk call leaderboards.get '{"boardKey": "default", "limit": 10}'
|
|
484
491
|
lumine sdk call aiStories.list '{"difficulty": 1}' --repeat 5 --build 1374
|
|
485
492
|
lumine sdk call userDb.query '{"sql": "SELECT * FROM sqlite_master"}'
|
|
493
|
+
lumine sdk call news.getEdition '{"dayIndex": 1762, "revisionNumber": 1}' --build 1929
|
|
486
494
|
|
|
487
|
-
|
|
495
|
+
Argument shapes follow TWINKLE_BUILD_SDK.md; public newspaper reads use GET
|
|
496
|
+
without a login or Build token. Output is the raw
|
|
488
497
|
endpoint response; (*) methods are reshaped by their Twinkle.* SDK wrapper, so
|
|
489
498
|
check TWINKLE_BUILD_SDK.md for the real SDK return shape. Add --repeat <n> for
|
|
490
499
|
latency stats, --build <id> to target a build outside the current workspace.
|
|
@@ -568,6 +577,9 @@ export async function sdkCall(options) {
|
|
|
568
577
|
const requestedScopes = options.sdkScopes.length
|
|
569
578
|
? options.sdkScopes
|
|
570
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
|
+
}
|
|
571
583
|
// A curated readOnly method (rewards.getStatus) needs a scope whose name is
|
|
572
584
|
// write-capable but only ever sends its read operation; its own default
|
|
573
585
|
// scopes do not trip the gate. Explicit --scopes overrides always do.
|
|
@@ -595,12 +607,23 @@ export async function sdkCall(options) {
|
|
|
595
607
|
// before any auth side effects, so invalid local input fails immediately
|
|
596
608
|
// instead of after starting the device-login flow.
|
|
597
609
|
const buildId = await resolveSdkBuildId(options);
|
|
598
|
-
|
|
599
|
-
|
|
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" });
|
|
600
617
|
const attempts = [];
|
|
601
618
|
let lastResult = null;
|
|
602
619
|
|
|
603
|
-
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") {
|
|
604
627
|
const boardKey = String(args.boardKey || "default");
|
|
605
628
|
const rest = { ...args };
|
|
606
629
|
delete rest.boardKey;
|
|
@@ -740,6 +763,39 @@ export async function sdkCall(options) {
|
|
|
740
763
|
}
|
|
741
764
|
}
|
|
742
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
|
+
|
|
743
799
|
export function printSdkAttemptLine(attemptNumber, result) {
|
|
744
800
|
console.error(
|
|
745
801
|
`#${attemptNumber} ${result.status}${result.ok ? " OK" : ""} · ` +
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build SDK Index
|
|
2
2
|
|
|
3
|
-
Version: 1.
|
|
4
|
-
Updated: 2026-10-
|
|
5
|
-
Generated: 2026-10-
|
|
3
|
+
Version: 1.74.0
|
|
4
|
+
Updated: 2026-10-29
|
|
5
|
+
Generated: 2026-10-28T20:08:43.889Z
|
|
6
6
|
|
|
7
7
|
## Platform rules
|
|
8
8
|
Each rule below is a way an app breaks once it runs inside Twinkle's sandboxed frame, even when the code itself is correct. They outrank every other guideline: follow them in every change, including small ones, and fix any break of them you see in code you touch.
|
|
@@ -1402,6 +1402,19 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
1402
1402
|
- Use limit or pageSize to choose how many entries to fetch per page. Default is 20, max is 100.
|
|
1403
1403
|
- For oldest-first chronological reads, pass order: 'asc' or order: 'oldest'. sort and direction are accepted as aliases.
|
|
1404
1404
|
- Supports cursor-based pagination. Newest-first cursors page toward older entries; oldest-first cursors page toward newer entries.
|
|
1405
|
+
- async queryEntries(topicName, { limit, pageSize, cursor, order, authorUserId, fields, where, countBy, includeAuthor } = {}) | scopes: sharedDb:read
|
|
1406
|
+
- Returns: { entries?, groups?, cursor, hasMore, queryVersion: 1 }
|
|
1407
|
+
- Read selected shared data fields with server-side creator filtering, scalar matching and grouped counts.
|
|
1408
|
+
- Returns the same paginated entries as getEntries, plus queryVersion: 1.
|
|
1409
|
+
- authorUserId filters to one entry creator on the server. It does not change read or write permissions.
|
|
1410
|
+
- fields selects at most 24 top-level data field names; missing selected fields are null. Omit fields for complete data; [] returns only row metadata.
|
|
1411
|
+
- where: { field, equals } filters one top-level data field by exact JSON scalar equality (string, number, boolean or null).
|
|
1412
|
+
- Alternatively where: { field, truthy: true } requires a present JSON value other than null, false, zero or an empty string.
|
|
1413
|
+
- countBy groups matching entries by one top-level data field and returns { groups: [{ value, count }], cursor, hasMore, queryVersion: 1 } instead of entries. This avoids transferring documents just to count comments. Groups page in first-entry order; use the returned cursor with the same query. Per-viewer hidden entries are excluded before counting.
|
|
1414
|
+
- includeAuthor: false skips username/profile-photo enrichment for count/index reads; id, topicId, userId and timestamps remain available.
|
|
1415
|
+
- Field names use letters, digits and underscores, starting with a letter or underscore, at most 64 characters. No SQL or nested paths.
|
|
1416
|
+
- Keep the same filters/fields/order when passing a cursor. Reads retain app/topic boundaries, published guest opt-ins and per-viewer content visibility. Signed-in queries read canonical writer state.
|
|
1417
|
+
- Use metadata lists for browsing and getEntriesByIds only when a member opens a full recording/document.
|
|
1405
1418
|
- async loadMoreEntries(topicName, { limit, pageSize, cursor, order, sort, direction } = {}) | scopes: sharedDb:read
|
|
1406
1419
|
- Returns: { entries: [{ id, topicId, userId, username, profilePicUrl, data, createdAt, updatedAt }], cursor?, hasMore }
|
|
1407
1420
|
- Fetch the next sharedDb page.
|