@stage5/lumine 0.3.84 → 0.3.86
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 +6 -5
- package/lib/admin-arena.js +60 -1
- package/lib/admin-daily.js +3 -2
- package/lib/admin.js +19 -1
- package/lib/assets.js +4 -4
- package/lib/commands.js +5 -5
- package/lib/constants.js +17 -10
- package/lib/platformRules.generated.js +1 -1
- package/lib/thumbnail.js +1 -1
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +19 -12
- package/sdk/LUMINE_ADMIN.md +22 -0
package/README.md
CHANGED
|
@@ -377,14 +377,15 @@ To preserve Pro output in an existing script, change the option to
|
|
|
377
377
|
using Pro through the server's legacy model alias. This CLI release must be
|
|
378
378
|
published after the API update that accepts the new model IDs.
|
|
379
379
|
|
|
380
|
-
`lumine assets generate "<prompt>" --model <
|
|
380
|
+
`lumine assets generate "<prompt>" --model <flare|sunburst|gpt-image-2|nano-banana|nano-banana-pro>` creates
|
|
381
381
|
an AI-generated image asset instead of uploading one. `--model` is required
|
|
382
|
-
(
|
|
383
|
-
|
|
382
|
+
(flare = GPT Image 2.6 Flare, fast generation; sunburst = GPT Image 2.6
|
|
383
|
+
Sunburst, precise editing; the full ids gpt-image-2.6-flare/-sunburst work too,
|
|
384
|
+
and the retired 2.5 ids now mean 2.6; gpt-image-2 = original model; nano-banana = Gemini Nano Banana 2.1, nano-banana-pro = Gemini Nano Banana Pro). Generation spends your Twinkle AI Battery, so the
|
|
384
385
|
CLI shows the estimated cost and asks for confirmation first — non-interactive
|
|
385
386
|
runs must pass
|
|
386
387
|
`--yes` to consent. `--quality low|medium|high|xhigh|max` applies to GPT Image
|
|
387
|
-
models; `xhigh` and `max` require a 2.
|
|
388
|
+
models; `xhigh` and `max` require a 2.6 model (flare or sunburst). Estimates describe image output;
|
|
388
389
|
actual battery usage also includes prompt and reference input.
|
|
389
390
|
|
|
390
391
|
## Thumbnails
|
|
@@ -392,7 +393,7 @@ actual battery usage also includes prompt and reference input.
|
|
|
392
393
|
`lumine thumbnail set <file>` uploads a jpg/png/webp (max 8MB) as the build's
|
|
393
394
|
thumbnail. `lumine thumbnail capture` screenshots the running app server-side
|
|
394
395
|
and sets the result (add `--out <file>` to keep a local copy).
|
|
395
|
-
`lumine thumbnail generate ["<prompt>"] --model <
|
|
396
|
+
`lumine thumbnail generate ["<prompt>"] --model <flare|sunburst|gpt-image-2|nano-banana|nano-banana-pro>`
|
|
396
397
|
generates an AI image and sets it as the thumbnail (the image is also kept as a
|
|
397
398
|
normal reusable asset); without a prompt the server composes one from the build
|
|
398
399
|
title and description. Replacing an existing thumbnail asks for confirmation;
|
package/lib/admin-arena.js
CHANGED
|
@@ -13,6 +13,10 @@ import path from "node:path";
|
|
|
13
13
|
// at once; a dry run (validation only) unless --confirm;
|
|
14
14
|
// re-importing a folder retires that paper's earlier items
|
|
15
15
|
// status --item <id> --to live|retired|checked
|
|
16
|
+
// paper-assemble --file <manifest.json> publish a complete paper from existing,
|
|
17
|
+
// never-served IDs, including reviewed text passages
|
|
18
|
+
// and unscored writing; preview unless --confirm;
|
|
19
|
+
// preserves content versions and exposures
|
|
16
20
|
// revise --file <revisions.json> in-place text/passage/visual/review fixes to
|
|
17
21
|
// live items: same item id, a new content version;
|
|
18
22
|
// a dry run unless --confirm; audited
|
|
@@ -25,7 +29,7 @@ import path from "node:path";
|
|
|
25
29
|
// <itemId>.png renders of picture-option items
|
|
26
30
|
// (panels are drawn by the web, not stored)
|
|
27
31
|
export const ARENA_USAGE =
|
|
28
|
-
"Usage: lumine admin arena queue | paper-queue [--cursor <id>] [--limit <1-100>] | marking [--cursor <id>] [--limit <1-200>] | import (--dir <paper folder> | --additive --file <reviewed-items.json>) [--audio audio/<exam>/<paper>] [--live] [--confirm] | status --item <id> --to live|retired|checked [--confirm] | revise --file <revisions.json> [--confirm] | today-queue [--cursor <id>] [--limit <1-100>] | today-member --user <id> | today-import --file <reviewed-questions.json> [--confirm] | today-withdraw --user <id> (--keys <key,...> | --file <withdraw.json>) --reason <text> [--confirm] | picture-audit [--items <id,id,...> | --cursor <id>] [--limit <1-20>] [--renders <dir of <itemId>.png>] [--output <review-list.json>] [--json]";
|
|
32
|
+
"Usage: lumine admin arena queue | paper-queue [--cursor <id>] [--limit <1-100>] | marking [--cursor <id>] [--limit <1-200>] | import (--dir <paper folder> | --additive --file <reviewed-items.json>) [--audio audio/<exam>/<paper>] [--live] [--confirm] | paper-assemble --file <manifest.json> [--confirm] | status --item <id> --to live|retired|checked [--confirm] | revise --file <revisions.json> [--confirm] | today-queue [--cursor <id>] [--limit <1-100>] | today-member --user <id> | today-import --file <reviewed-questions.json> [--confirm] | today-withdraw --user <id> (--keys <key,...> | --file <withdraw.json>) --reason <text> [--confirm] | picture-audit [--items <id,id,...> | --cursor <id>] [--limit <1-20>] [--renders <dir of <itemId>.png>] [--output <review-list.json>] [--json]";
|
|
29
33
|
const MAX_ITEMS = 300;
|
|
30
34
|
|
|
31
35
|
function validationError(message) {
|
|
@@ -154,6 +158,56 @@ function buildTodayWithdraw(options) {
|
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
export function buildArenaOperation({ action, options }) {
|
|
161
|
+
if (action === "paper-assemble") {
|
|
162
|
+
if (!options.adminFile || options.adminDir || options.adminArenaAdditive || options.adminArenaAudio) {
|
|
163
|
+
throw validationError("Use paper-assemble --file <manifest.json> [--confirm]; it uses existing item IDs, never a content import.");
|
|
164
|
+
}
|
|
165
|
+
const file = path.resolve(options.adminFile);
|
|
166
|
+
let parsed;
|
|
167
|
+
try {
|
|
168
|
+
if (fs.statSync(file).size > 200_000) throw validationError("The assembly manifest must be under 200 KB.");
|
|
169
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error.code === "CLI_ADMIN_CLI_VALIDATION") throw error;
|
|
172
|
+
throw validationError(`Cannot read the assembly manifest ${file}: ${error.message}`);
|
|
173
|
+
}
|
|
174
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) ||
|
|
175
|
+
typeof parsed.exam !== "string" || !["easy", "normal", "hard"].includes(parsed.difficulty) ||
|
|
176
|
+
typeof parsed.paper !== "string" || !/^[A-Za-z0-9][A-Za-z0-9-]{0,39}$/.test(parsed.paper) ||
|
|
177
|
+
!Array.isArray(parsed.items) || !parsed.items.length || parsed.items.length > MAX_ITEMS || !parsed.review) {
|
|
178
|
+
throw validationError("The manifest needs exam, difficulty, paper, reviewed existing items and review evidence.");
|
|
179
|
+
}
|
|
180
|
+
for (const item of parsed.items) {
|
|
181
|
+
if (!item || !Number.isSafeInteger(item.itemId) || item.itemId <= 0 ||
|
|
182
|
+
!Number.isSafeInteger(item.expectedVersion) || item.expectedVersion < 1 ||
|
|
183
|
+
!Number.isSafeInteger(item.number) || item.number < 1 ||
|
|
184
|
+
typeof item.contentHash !== "string" || !/^[a-f0-9]{64}$/.test(item.contentHash) ||
|
|
185
|
+
(item.passageId !== undefined && (!Number.isSafeInteger(item.passageId) || item.passageId <= 0)) ||
|
|
186
|
+
Object.keys(item).some(key => !["itemId", "expectedVersion", "number", "contentHash", "passageId"].includes(key))) {
|
|
187
|
+
throw validationError("Each entry needs itemId, expectedVersion, number and contentHash; replacement content is not accepted.");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (new Set(parsed.items.map(i => i.itemId)).size !== parsed.items.length || new Set(parsed.items.map(i => i.number)).size !== parsed.items.length) {
|
|
191
|
+
throw validationError("Item IDs and blueprint numbers must be unique.");
|
|
192
|
+
}
|
|
193
|
+
if (parsed.passages !== undefined && (!Array.isArray(parsed.passages) || parsed.passages.length > MAX_ITEMS ||
|
|
194
|
+
parsed.passages.some(p => !p || !Number.isSafeInteger(p.passageId) || p.passageId <= 0 ||
|
|
195
|
+
!Number.isSafeInteger(p.expectedVersion) || p.expectedVersion <= 0 ||
|
|
196
|
+
typeof p.contentHash !== "string" || !/^[a-f0-9]{64}$/.test(p.contentHash) ||
|
|
197
|
+
Object.keys(p).some(key => !["passageId", "expectedVersion", "contentHash"].includes(key))) ||
|
|
198
|
+
new Set(parsed.passages.map(p => p.passageId)).size !== parsed.passages.length)) {
|
|
199
|
+
throw validationError("Passages need unique existing IDs, expectedVersion and canonical hashes, never replacement text.");
|
|
200
|
+
}
|
|
201
|
+
const referenced = [...new Set(parsed.items.flatMap(i => i.passageId === undefined ? [] : [i.passageId]))].sort((a, b) => a - b);
|
|
202
|
+
const reviewed = (parsed.passages || []).map(p => p.passageId).sort((a, b) => a - b);
|
|
203
|
+
if (JSON.stringify(referenced) !== JSON.stringify(reviewed)) throw validationError("Review every referenced passage exactly once and omit unreferenced passages.");
|
|
204
|
+
const confirm = confirmed(options);
|
|
205
|
+
return { name: confirm ? "arena.paper.assemble" : "arena.paper.assemble.preview", method: "POST",
|
|
206
|
+
path: "/cli/admin/arena/papers/assemble",
|
|
207
|
+
body: { exam: parsed.exam, difficulty: parsed.difficulty, paper: parsed.paper, items: parsed.items,
|
|
208
|
+
...(parsed.passages !== undefined ? { passages: parsed.passages } : {}), review: parsed.review, dryRun: !confirm },
|
|
209
|
+
mutates: confirm, requiresRun: false, timeoutMs: 120000 };
|
|
210
|
+
}
|
|
157
211
|
if (action === "picture-audit") {
|
|
158
212
|
const limit = Number(options.adminLimit ?? 10);
|
|
159
213
|
if (!Number.isInteger(limit) || limit < 1 || limit > PICTURE_AUDIT_MAX) throw validationError(`Use --limit from 1 to ${PICTURE_AUDIT_MAX}.`);
|
|
@@ -307,6 +361,11 @@ export function buildArenaOperation({ action, options }) {
|
|
|
307
361
|
}
|
|
308
362
|
|
|
309
363
|
export function formatArenaResult({ operation, data }) {
|
|
364
|
+
if (operation.name === "arena.paper.assemble" || operation.name === "arena.paper.assemble.preview") {
|
|
365
|
+
if (data?.alreadyAssembled) return [`${data.paper}: already assembled from the same existing items; nothing changed.`];
|
|
366
|
+
return [`${data?.dryRun ? "Dry run: would assemble" : "Assembled"} ${data?.wouldAssemble ?? data?.assembled ?? 0} existing items as ${data?.paper}. IDs, content versions and exposures are preserved.`,
|
|
367
|
+
...(data?.dryRun ? ["Add --confirm to publish this reviewed complete paper."] : [])];
|
|
368
|
+
}
|
|
310
369
|
if (operation.name === "arena.paper.queue") return [
|
|
311
370
|
`Checked ${data?.scannedMembers ?? 0} members; ${data?.entries?.length ?? 0} exam tracks are waiting for fresh papers on this page.`,
|
|
312
371
|
...(data?.entries || []).map(row => `${row.username} (${row.userId}): ${row.examKey} ${row.difficulty}/${row.length}${row.reason === "solo_mastery_paper" ? " — solo mastery ready" : ""}`),
|
package/lib/admin-daily.js
CHANGED
|
@@ -250,7 +250,7 @@ export async function dailyRunGather(options, { run = runAdmin, now, clock } = {
|
|
|
250
250
|
step("brief", ["brief"], "brief.json");
|
|
251
251
|
step("todos", ["todo", "list"], "todos.json");
|
|
252
252
|
step("escalations", ["escalation", "list"], "escalations.json");
|
|
253
|
-
const paperQueue = { complete: false, pages: 0, waitingTracks: 0 };
|
|
253
|
+
const paperQueue = { complete: false, pages: 0, waitingTracks: 0, demandBases: [] };
|
|
254
254
|
let paperCursor = 0;
|
|
255
255
|
for (;;) {
|
|
256
256
|
paperQueue.pages++;
|
|
@@ -260,11 +260,12 @@ export async function dailyRunGather(options, { run = runAdmin, now, clock } = {
|
|
|
260
260
|
if (!res.ok) break;
|
|
261
261
|
const data = res.json?.data;
|
|
262
262
|
const next = data?.nextCursor;
|
|
263
|
-
if (
|
|
263
|
+
if (!["waiting-published-papers-v1", "cleared-exhausted-papers-v2"].includes(data?.demandBasis) || !Array.isArray(data?.entries) ||
|
|
264
264
|
!(next === null || (Number.isSafeInteger(next) && next > paperCursor))) {
|
|
265
265
|
failLast("Invalid or non-advancing Arena paper-waiting page.");
|
|
266
266
|
break;
|
|
267
267
|
}
|
|
268
|
+
if (!paperQueue.demandBases.includes(data.demandBasis)) paperQueue.demandBases.push(data.demandBasis);
|
|
268
269
|
paperQueue.waitingTracks += data.entries.length;
|
|
269
270
|
if (next === null) { paperQueue.complete = true; break; }
|
|
270
271
|
if (paperQueue.pages >= 100) {
|
package/lib/admin.js
CHANGED
|
@@ -430,7 +430,7 @@ function buildApprovalOperation({ action, target, options }) {
|
|
|
430
430
|
// Code agent passes wrote (no model is called), then review what they could
|
|
431
431
|
// not settle (Mikey 2026-10-06: bulk AI work runs on Claude Code agents).
|
|
432
432
|
const ENGLISH_USAGE =
|
|
433
|
-
"Use: lumine admin english import <verdicts.json> --pass a|b | adjudicate <verdicts.json> | add-questions <questions.json> | relevel <moves.json> | mastery <userId> --reason <why> | backfill-distractors | summary | review [--limit <n>] | accept|retire <questionId> [--reason <text>] [--fault] | retag <questionId> --skill <code> [--reason <text>] | build-candidates [--after <questionId>] [--limit <n>] | build-import <verdicts.json> | check-candidates [--after <questionId>] [--limit <n>] | check-import <verdicts.json> | check-broken [--after <questionId>] [--limit <n>] | check-apply <fixes.json> | check-verify <checked.json> | check-follow-faulty [--after <next>] [--limit <n>] | audit-backlog [--after <questionId>] [--limit <n>] | audit-mark <marks.json> | lesson-candidates [--after <skillCode>] [--limit <n>] | lesson-import <lessons.json> | lesson-approve <approvals.json> | lessons [--status draft|approved]";
|
|
433
|
+
"Use: lumine admin english import <verdicts.json> --pass a|b | adjudicate <verdicts.json> | add-questions <questions.json> | relevel <moves.json> | mastery <userId> --reason <why> | backfill-distractors | summary | review [--limit <n>] | accept|retire <questionId> [--reason <text>] [--fault] | retag <questionId> --skill <code> [--reason <text>] | build-candidates [--after <questionId>] [--limit <n>] | build-import <verdicts.json> | check-candidates [--after <questionId>] [--limit <n>] | check-import <verdicts.json> | check-broken [--after <questionId>] [--limit <n>] | check-apply <fixes.json> | check-verify <checked.json> | check-follow-faulty [--after <next>] [--limit <n>] | quest-evidence <case.json> [--confirm] | audit-backlog [--after <questionId>] [--limit <n>] | audit-mark <marks.json> | lesson-candidates [--after <skillCode>] [--limit <n>] | lesson-import <lessons.json> | lesson-approve <approvals.json> | lessons [--status draft|approved]";
|
|
434
434
|
const ENGLISH_IMPORT_MAX = 500;
|
|
435
435
|
|
|
436
436
|
function buildEnglishOperation({ action, target, options }) {
|
|
@@ -512,6 +512,24 @@ function buildEnglishOperation({ action, target, options }) {
|
|
|
512
512
|
if (after && !/^(?:[ufc]:)?\d+:\d+$/.test(after)) throw cliValidationError("--after takes the last reply's next (<phase:id:version>).");
|
|
513
513
|
return writeOperation("english.check_follow_faulty", "POST", "/cli/admin/english/item-checks/follow-faulty", { after, limit: limitOf(200, 100) }, { requiresRun: false, timeoutMs: 300000 });
|
|
514
514
|
}
|
|
515
|
+
// A single legacy Quest skill is previewed from its complete saved history.
|
|
516
|
+
// Applying requires both --confirm and that exact preview's source hash.
|
|
517
|
+
if (action === "quest-evidence" && target) {
|
|
518
|
+
const input = readAdminJsonFile(target, "<quest-evidence.json>", { maxBytes: 16 * 1024 });
|
|
519
|
+
if (!input || !Number.isSafeInteger(input.userId) || input.userId <= 0 ||
|
|
520
|
+
typeof input.skillCode !== "string" || !input.skillCode || input.skillCode.length > 64) {
|
|
521
|
+
throw cliValidationError("The file needs one positive userId and one skillCode.");
|
|
522
|
+
}
|
|
523
|
+
if (options.adminConfirm && options.adminConfirm !== "true") throw cliValidationError("Pass --confirm on its own, without a value.");
|
|
524
|
+
const confirm = options.adminConfirm === "true";
|
|
525
|
+
if (confirm && options.adminDryRun) throw cliValidationError("Choose --confirm or --dry-run, not both.");
|
|
526
|
+
if (confirm && !/^[a-f0-9]{64}$/.test(String(input.expectedHash || ""))) {
|
|
527
|
+
throw cliValidationError("Applying needs expectedHash from the exact preview.");
|
|
528
|
+
}
|
|
529
|
+
return writeOperation("english.quest-evidence", "POST", "/cli/admin/english/quest-evidence/reconcile",
|
|
530
|
+
{ userId: input.userId, skillCode: input.skillCode, expectedHash: input.expectedHash, dryRun: !confirm },
|
|
531
|
+
{ requiresRun: false, mutates: confirm, timeoutMs: 120000 });
|
|
532
|
+
}
|
|
515
533
|
// Grammar Quest audit passes (Mikey 2026-10-17): the checked items no pass
|
|
516
534
|
// has looked at yet, first; and marking what a pass read,
|
|
517
535
|
// { audit: "<name>", items: [{ questionId, version, contentHash, outcome }] }
|
package/lib/assets.js
CHANGED
|
@@ -66,7 +66,7 @@ export async function assetsCommand(options) {
|
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
throw new Error(
|
|
69
|
-
'Usage: lumine assets [list] | lumine assets upload <file...> | lumine assets generate "<prompt>" --model <
|
|
69
|
+
'Usage: lumine assets [list] | lumine assets upload <file...> | lumine assets generate "<prompt>" --model <flare|sunburst|gpt-image-2|nano-banana|nano-banana-pro> | lumine assets delete <assetId> | lumine assets prune [--yes] | lumine assets storage | lumine assets request-storage [--size 500MB|1GB|2GB] [--reason "<what it is for>"]',
|
|
70
70
|
);
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -156,7 +156,7 @@ export function resolveGenerateModel(options) {
|
|
|
156
156
|
const model = GENERATE_MODEL_ALIASES[rawModel];
|
|
157
157
|
if (!model) {
|
|
158
158
|
throw new Error(
|
|
159
|
-
"--model is required:
|
|
159
|
+
"--model is required: flare (GPT Image 2.6 Flare, fast generation), sunburst (GPT Image 2.6 Sunburst, precise editing), gpt-image-2, nano-banana (Nano Banana 2.1) or nano-banana-pro. No default model is applied.",
|
|
160
160
|
);
|
|
161
161
|
}
|
|
162
162
|
const rawQuality = String(options.quality || "").trim();
|
|
@@ -169,7 +169,7 @@ export function resolveGenerateModel(options) {
|
|
|
169
169
|
);
|
|
170
170
|
}
|
|
171
171
|
if (model === "gpt-image-2" && ["xhigh", "max"].includes(rawQuality)) {
|
|
172
|
-
throw new Error("xhigh and max require a GPT Image 2.
|
|
172
|
+
throw new Error("xhigh and max require a GPT Image 2.6 model (flare or sunburst).");
|
|
173
173
|
}
|
|
174
174
|
return { model, quality: rawQuality || null };
|
|
175
175
|
}
|
|
@@ -212,7 +212,7 @@ export async function assetsGenerate(options) {
|
|
|
212
212
|
const prompt = String(options.positional[1] || "").trim();
|
|
213
213
|
if (!prompt) {
|
|
214
214
|
throw new Error(
|
|
215
|
-
'Usage: lumine assets generate "<prompt>" --model <
|
|
215
|
+
'Usage: lumine assets generate "<prompt>" --model <flare|sunburst|gpt-image-2|nano-banana|nano-banana-pro> [--quality low|medium|high|xhigh|max] [--name <fileName>] [--yes]',
|
|
216
216
|
);
|
|
217
217
|
}
|
|
218
218
|
const { model, quality } = resolveGenerateModel(options);
|
package/lib/commands.js
CHANGED
|
@@ -3287,7 +3287,7 @@ export function printHelp() {
|
|
|
3287
3287
|
lumine sdk call <namespace.method> [jsonArgs]
|
|
3288
3288
|
lumine assets [list]
|
|
3289
3289
|
lumine assets upload <file...>
|
|
3290
|
-
lumine assets generate "<prompt>" --model <
|
|
3290
|
+
lumine assets generate "<prompt>" --model <flare|sunburst|gpt-image-2|nano-banana|nano-banana-pro>
|
|
3291
3291
|
lumine assets delete <assetId>
|
|
3292
3292
|
lumine assets prune [--yes]
|
|
3293
3293
|
lumine assets storage [--json]
|
|
@@ -3301,7 +3301,7 @@ export function printHelp() {
|
|
|
3301
3301
|
lumine cardcraft check|status|review
|
|
3302
3302
|
lumine thumbnail set <file>
|
|
3303
3303
|
lumine thumbnail capture [--out <file>]
|
|
3304
|
-
lumine thumbnail generate ["<prompt>"] --model <
|
|
3304
|
+
lumine thumbnail generate ["<prompt>"] --model <flare|sunburst|gpt-image-2|nano-banana|nano-banana-pro>
|
|
3305
3305
|
lumine doctor runtime-assets
|
|
3306
3306
|
lumine admin identity list|status|use <zero|ciel|auto> [--json]
|
|
3307
3307
|
lumine admin identity inspect <user-id|username> --reason <management-reason> [--include-private-evidence] [--output <file>] [--json] (nothing kept on disk unless --output; that copy is 0600)
|
|
@@ -3494,7 +3494,7 @@ Examples:
|
|
|
3494
3494
|
npx @stage5/lumine@latest assets generate "pixel-art forest background" --model nano-banana
|
|
3495
3495
|
npx @stage5/lumine@latest thumbnail set art/cover.png
|
|
3496
3496
|
npx @stage5/lumine@latest thumbnail capture --out capture.png
|
|
3497
|
-
npx @stage5/lumine@latest thumbnail generate --model
|
|
3497
|
+
npx @stage5/lumine@latest thumbnail generate --model flare --yes
|
|
3498
3498
|
npx @stage5/lumine@latest doctor runtime-assets --build 917 --json
|
|
3499
3499
|
npx @stage5/lumine@latest admin daily-run start --identity auto --comment-mode off --json
|
|
3500
3500
|
npx @stage5/lumine@latest admin daily-run start --scope featured --identity auto --json
|
|
@@ -3588,8 +3588,8 @@ Options:
|
|
|
3588
3588
|
--json Print machine-readable output where supported
|
|
3589
3589
|
--keep-assets Keep doctor probe assets instead of deleting them
|
|
3590
3590
|
--no-browser Skip doctor browser probes
|
|
3591
|
-
--model <model> Image model for generate:
|
|
3592
|
-
--quality <q> GPT Image quality: low, medium, high, xhigh, max (default high; xhigh/max require
|
|
3591
|
+
--model <model> Image model for generate: flare (GPT Image 2.6 Flare), sunburst (GPT Image 2.6 Sunburst), gpt-image-2, nano-banana (Nano Banana 2.1) or nano-banana-pro (required)
|
|
3592
|
+
--quality <q> GPT Image quality: low, medium, high, xhigh, max (default high; xhigh/max require flare or sunburst)
|
|
3593
3593
|
--name <fileName> File name hint for a generated asset
|
|
3594
3594
|
--out <path> With thumbnail capture: also save the capture locally
|
|
3595
3595
|
--transparent Ask GPT Image for a transparent background (assets generate)
|
package/lib/constants.js
CHANGED
|
@@ -21,10 +21,13 @@ export const DEFAULT_TIMEOUT_MS = 20000;
|
|
|
21
21
|
export const ASSET_GENERATE_TIMEOUT_MS = 6 * 60 * 1000;
|
|
22
22
|
export const THUMBNAIL_CAPTURE_TIMEOUT_MS = 90 * 1000;
|
|
23
23
|
export const GENERATE_MODEL_ALIASES = {
|
|
24
|
-
"gpt-image-2.
|
|
25
|
-
"gpt-image-2.
|
|
26
|
-
flare: "gpt-image-2.
|
|
27
|
-
sunburst: "gpt-image-2.
|
|
24
|
+
"gpt-image-2.6-flare": "gpt-image-2.6-flare",
|
|
25
|
+
"gpt-image-2.6-sunburst": "gpt-image-2.6-sunburst",
|
|
26
|
+
flare: "gpt-image-2.6-flare",
|
|
27
|
+
sunburst: "gpt-image-2.6-sunburst",
|
|
28
|
+
// GPT Image 2.6 replaced 2.5 (2026-11-07); old scripts keep working.
|
|
29
|
+
"gpt-image-2.5-flare": "gpt-image-2.6-flare",
|
|
30
|
+
"gpt-image-2.5-sunburst": "gpt-image-2.6-sunburst",
|
|
28
31
|
"gpt-image-2": "gpt-image-2",
|
|
29
32
|
// Gemini by tier (the API's NANO_BANANA_MODEL / NANO_BANANA_PRO_MODEL):
|
|
30
33
|
// nano-banana = the fast tier (Nano Banana 2.1 since 2026-10-08), pro = Pro
|
|
@@ -314,12 +317,16 @@ lumine save --summary "Describe the change"
|
|
|
314
317
|
replaces. Update code references, then delete or prune the old asset.
|
|
315
318
|
- Prefer one small assets module (e.g. src/assets.js) mapping names to asset
|
|
316
319
|
URLs over scattering URLs through the code.
|
|
317
|
-
- \`lumine assets generate "<prompt>" --model <
|
|
318
|
-
an AI-generated image asset. --model is REQUIRED (no default):
|
|
319
|
-
|
|
320
|
-
|
|
320
|
+
- \`lumine assets generate "<prompt>" --model <flare|sunburst|nano-banana|nano-banana-pro|gpt-image-2>\` creates
|
|
321
|
+
an AI-generated image asset. --model is REQUIRED (no default): flare
|
|
322
|
+
(GPT Image 2.6 Flare) is the usual choice: high quality, clean lettering,
|
|
323
|
+
about 10 seconds; sunburst (GPT Image 2.6 Sunburst) is slower and best for
|
|
324
|
+
careful edits of an existing image; nano-banana (Gemini Nano Banana 2.1) is
|
|
325
|
+
the cheapest; nano-banana-pro (Gemini Nano Banana Pro) sits between;
|
|
326
|
+
gpt-image-2 is the older model, several times pricier than flare. It spends the logged-in user's AI Battery, so it asks y/N with the
|
|
321
327
|
estimated cost first; non-interactive runs must pass --yes to consent.
|
|
322
|
-
Optional: --quality low|medium|high (
|
|
328
|
+
Optional: --quality low|medium|high|xhigh|max (GPT Image models only,
|
|
329
|
+
default high; xhigh and max need flare or sunburst),
|
|
323
330
|
--name <fileName>. The asset lands in .twinkle/${ASSETS_METADATA_FILE} like an upload.
|
|
324
331
|
|
|
325
332
|
## Studying Game Music
|
|
@@ -349,7 +356,7 @@ lumine save --summary "Describe the change"
|
|
|
349
356
|
build's thumbnail. Thumbnails display ~16:9; the image is uploaded uncropped.
|
|
350
357
|
- \`lumine thumbnail capture\` screenshots the running app server-side and sets
|
|
351
358
|
the result as the thumbnail (add --out <file> to also save it locally).
|
|
352
|
-
- \`lumine thumbnail generate "<prompt>" --model <
|
|
359
|
+
- \`lumine thumbnail generate "<prompt>" --model <flare|sunburst|nano-banana|nano-banana-pro|gpt-image-2>\`
|
|
353
360
|
generates an AI image and sets it as the thumbnail (also stored as a normal
|
|
354
361
|
reusable asset). Same battery-cost consent rules as \`assets generate\`.
|
|
355
362
|
- Replacing an existing thumbnail asks y/N; pass --yes for non-interactive runs.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// Generated by twinkle-api/scripts/generate-build-sdk-index.mjs from
|
|
2
2
|
// constants/buildSdkManifest.json (platformRules, essentialSdk). Do not edit by hand.
|
|
3
|
-
export const LUMINE_PLATFORM_RULES = "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.\n\n1. Every page that draws on a <canvas> (2D, WebGL or Three.js) says what kind of page it is on <html>: data-twinkle-game for a game that fills the screen, data-twinkle-document for a normal scrolling page with a drawing pad, chart, visualizer or background effect. Without it, unless the page lays itself out with Twinkle.preview, Twinkle guesses from the canvas, and any canvas larger than 160 px, even one hidden in a tab, locks the page to one screen so it no longer scrolls. Add the attribute together with the first canvas.\n2. Games (data-twinkle-game) size themselves from Twinkle.preview: subscribe with Twinkle.preview.subscribe(listener, { immediate: true }), size canvases and renderers from layout.stage or layout.viewport, and report the play area with Twinkle.preview.setPlayfield(). Never size a game from 100vh/100vw, window.innerWidth/innerHeight or visualViewport, and never draw a fake phone or browser frame unless the creator asks for a device mockup: those fight the host and leave the game blank, cropped or scrolling on phones. Pages marked data-twinkle-document use ordinary CSS layout instead.\n3. alert(), confirm() and prompt() are silently blocked: alert shows nothing, confirm() always returns false (so the action it guards never happens) and prompt() returns null. Use in-page UI instead: a message element, an in-page confirm step, an <input>.\n4. Apps cannot open new windows or tabs: window.open() and target=\"_blank\" links do nothing, and neither does a download link to an uploaded file. Open Twinkle pages with Twinkle.app.openContent(url) from a tap, save files with Twinkle.files.saveAs({ url, fileName }) (or { blob, fileName } for a file the app made), and show other web addresses as plain text.\n5. Do not use <form> elements: native form submission is blocked, so a submit button inside a form does nothing. Handle inputs and buttons in JavaScript.\n6. Code loads only from project files and Twinkle's vendor paths (/build/vendor/...). Package imports, CDN scripts, external network calls and app-local /api/* routes fail at runtime; use the Twinkle SDK for data, AI and storage.\n7. Call only Twinkle.* methods documented in the SDK reference. An invented or guessed method name throws and breaks the feature.\n8. Twinkle.privateDb and Twinkle.sharedDb save on a user action or as an occasional snapshot, never per frame, tick or keystroke: fast repeated writes are refused with 429 and the data is lost. A privateDb value is at most 16 KB. Keep live state in memory, and use Twinkle.world for realtime multiplayer.\n9. Start sound and image generation inside the viewer's tap or click handler. A Web Audio AudioContext stays silent until it is created or resumed in a tap (iPhones always enforce this), and Twinkle.ai.generateImage called from load or a timer fails with USER_ACTIVATION_REQUIRED.\
|
|
3
|
+
export const LUMINE_PLATFORM_RULES = "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.\n\n1. Every page that draws on a <canvas> (2D, WebGL or Three.js) says what kind of page it is on <html>: data-twinkle-game for a game that fills the screen, data-twinkle-document for a normal scrolling page with a drawing pad, chart, visualizer or background effect. Without it, unless the page lays itself out with Twinkle.preview, Twinkle guesses from the canvas, and any canvas larger than 160 px, even one hidden in a tab, locks the page to one screen so it no longer scrolls. Add the attribute together with the first canvas.\n2. Games (data-twinkle-game) size themselves from Twinkle.preview: subscribe with Twinkle.preview.subscribe(listener, { immediate: true }), size canvases and renderers from layout.stage or layout.viewport, and report the play area with Twinkle.preview.setPlayfield(). Never size a game from 100vh/100vw, window.innerWidth/innerHeight or visualViewport, and never draw a fake phone or browser frame unless the creator asks for a device mockup: those fight the host and leave the game blank, cropped or scrolling on phones. Pages marked data-twinkle-document use ordinary CSS layout instead.\n3. alert(), confirm() and prompt() are silently blocked: alert shows nothing, confirm() always returns false (so the action it guards never happens) and prompt() returns null. Use in-page UI instead: a message element, an in-page confirm step, an <input>.\n4. Apps cannot open new windows or tabs: window.open() and target=\"_blank\" links do nothing, and neither does a download link to an uploaded file. Open Twinkle pages with Twinkle.app.openContent(url) from a tap, save files with Twinkle.files.saveAs({ url, fileName }) (or { blob, fileName } for a file the app made), and show other web addresses as plain text.\n5. Do not use <form> elements: native form submission is blocked, so a submit button inside a form does nothing. Handle inputs and buttons in JavaScript.\n6. Code loads only from project files and Twinkle's vendor paths (/build/vendor/...). Package imports, CDN scripts, external network calls and app-local /api/* routes fail at runtime; use the Twinkle SDK for data, AI and storage.\n7. Call only Twinkle.* methods documented in the SDK reference. An invented or guessed method name throws and breaks the feature.\n8. Twinkle.privateDb and Twinkle.sharedDb save on a user action or as an occasional snapshot, never per frame, tick or keystroke: fast repeated writes are refused with 429 and the data is lost. A privateDb value is at most 16 KB. Keep live state in memory, and use Twinkle.world for realtime multiplayer.\n9. Before changing an existing view, read its relevant event handlers, animations and state flow, and identify the behavior the edit must keep. A new appearance, reference image, renderer or performance improvement does not authorize removing an interaction, animation or capability; preserve it unless the creator explicitly asks for that change. After editing, compare the affected behavior before and after, including its active and inactive states (for example, playing and paused). Exercise those states when tools allow; a still screenshot or a clean console cannot prove motion or interaction. If a state cannot be exercised, report that limit rather than claiming it is preserved.\n10. When changing stored data or a renderer, inspect both the old save/resume shape and every reader, including fallback views. Keep existing saves readable with an explicit, non-destructive conversion and retain fields needed by another supported renderer. Verify an old save in the new version, new state after reload, and switching to the fallback (for example, 2D x/y toppings becoming 3D positions and returning to a flat view). Never silently drop saved objects or reset progress to make the new view work; an incomplete migration is part of this change to finish, not unrelated future polish.\n11. Keep local gameplay and unsaved edits separate from confirmed server state. Await storage writes and replace saved counters, scores and other durable state from the documented canonical response; Twinkle.privateDb.get/set return { item }, and the stored data is item.value (get may return item: null). Never read a guessed response shape, present a local increment as saved, or swallow a save/load error. Keep unfinished work recoverable, show a clear pending or failed-save state, and let the viewer retry deliberately without counting the same action twice. Verify canonical response restoration, rejected saves and delayed replies, not only a successful local animation.\n12. Start sound and image generation inside the viewer's tap or click handler. A Web Audio AudioContext stays silent until it is created or resumed in a tap (iPhones always enforce this), and Twinkle.ai.generateImage called from load or a timer fails with USER_ACTIVATION_REQUIRED.\n13. Call setPointerCapture only inside a pointerdown handler with that event's pointerId, wrapped in try/catch, and release only when hasPointerCapture says the element holds it. From click, mouse, timer or synthetic handlers it throws and the control stops working.\n14. Keyboard games call preventDefault for their keys (Space, arrows, WASD) from a window or document keydown listener added with { passive: false }, skipping inputs, textareas, selects and contenteditable, and make the playfield focusable; otherwise those keys scroll or jump the page during play.\n15. On phones a long press on game controls selects text or opens the Copy/Look Up menu unless the whole game area (controls, HUD, menus, empty play space) is marked data-twinkle-no-select and styled user-select: none; -webkit-user-select: none; -webkit-touch-callout: none. A game drawn without a canvas sets data-twinkle-game on <html> too, so Twinkle's phone touch protections apply. Keep inputs and copyable text selectable with data-twinkle-selectable.";
|
|
4
4
|
export const LUMINE_ESSENTIAL_SDK = "Features every app needs whenever they apply. When you build a new app or make a substantial change, add every one that applies as part of that change, without being asked. On a small, targeted request, add a missing one only when the request touches what it covers (a change to levels, scoring or saving: keep-place; a new screen: back-button); otherwise name it in one line so the creator can ask. Say in one line what you added. Never add one that does not apply.\n\n1. When: The app has a place worth keeping: a game, a level or score in play, an open document, drawing or editor, a quiz in progress, a chosen view or tab, a long page the viewer scrolls. Use: Register Twinkle.app.lifecycle.onSuspend(() => smallState, { work: [...] }) and, at startup, restore from await Twinkle.app.lifecycle.getResumeState(). The snapshot holds where the viewer is looking (scroll, open tab or view, selected item) and unfinished work (a game or level in play, a half-written answer, an unsaved drawing, a quiz in progress); list the work keys in { work }. A return (the phone unloaded the app, a reload, a quick switch away) gets everything back; a new visit (the next day, a new tab) starts at the app's start and gets only the work keys. Phones unload apps that are not on screen (3D apps right away) and browsers kill tabs; without this the player starts over every time.\n2. When: The app has more than one view: a list and its details, menus, chapters, pages, settings. Use: Twinkle.app.history.push on each confirmed view change and Twinkle.app.history.subscribe to restore views, so Back and Forward move inside the app instead of leaving it.\n3. When: Progress, collections, unlocks, high scores or settings the viewer expects to still have tomorrow. Use: Twinkle.privateDb (Twinkle.sharedDb for what everyone sees), written on a user action or as an occasional snapshot, never per frame.\n4. When: The app shares or links to something inside itself (a level, a post, a result). Use: Twinkle.app.getShareUrl(pathSegment) for the link, and read the segment back at startup, instead of building twin-kle.com URLs by hand.";
|
package/lib/thumbnail.js
CHANGED
|
@@ -42,7 +42,7 @@ export async function thumbnailCommand(options) {
|
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
throw new Error(
|
|
45
|
-
'Usage: lumine thumbnail set <file> | lumine thumbnail capture [--out <file>] | lumine thumbnail generate "<prompt>" --model <
|
|
45
|
+
'Usage: lumine thumbnail set <file> | lumine thumbnail capture [--out <file>] | lumine thumbnail generate "<prompt>" --model <flare|sunburst|gpt-image-2|nano-banana|nano-banana-pro>',
|
|
46
46
|
);
|
|
47
47
|
}
|
|
48
48
|
|
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-11-
|
|
5
|
-
Generated: 2026-11-
|
|
3
|
+
Version: 1.81.0
|
|
4
|
+
Updated: 2026-11-07
|
|
5
|
+
Generated: 2026-11-07T02:35:09.137Z
|
|
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.
|
|
@@ -15,10 +15,13 @@ Each rule below is a way an app breaks once it runs inside Twinkle's sandboxed f
|
|
|
15
15
|
6. Code loads only from project files and Twinkle's vendor paths (/build/vendor/...). Package imports, CDN scripts, external network calls and app-local /api/* routes fail at runtime; use the Twinkle SDK for data, AI and storage.
|
|
16
16
|
7. Call only Twinkle.* methods documented in the SDK reference. An invented or guessed method name throws and breaks the feature.
|
|
17
17
|
8. Twinkle.privateDb and Twinkle.sharedDb save on a user action or as an occasional snapshot, never per frame, tick or keystroke: fast repeated writes are refused with 429 and the data is lost. A privateDb value is at most 16 KB. Keep live state in memory, and use Twinkle.world for realtime multiplayer.
|
|
18
|
-
9.
|
|
19
|
-
10.
|
|
20
|
-
11.
|
|
21
|
-
12.
|
|
18
|
+
9. Before changing an existing view, read its relevant event handlers, animations and state flow, and identify the behavior the edit must keep. A new appearance, reference image, renderer or performance improvement does not authorize removing an interaction, animation or capability; preserve it unless the creator explicitly asks for that change. After editing, compare the affected behavior before and after, including its active and inactive states (for example, playing and paused). Exercise those states when tools allow; a still screenshot or a clean console cannot prove motion or interaction. If a state cannot be exercised, report that limit rather than claiming it is preserved.
|
|
19
|
+
10. When changing stored data or a renderer, inspect both the old save/resume shape and every reader, including fallback views. Keep existing saves readable with an explicit, non-destructive conversion and retain fields needed by another supported renderer. Verify an old save in the new version, new state after reload, and switching to the fallback (for example, 2D x/y toppings becoming 3D positions and returning to a flat view). Never silently drop saved objects or reset progress to make the new view work; an incomplete migration is part of this change to finish, not unrelated future polish.
|
|
20
|
+
11. Keep local gameplay and unsaved edits separate from confirmed server state. Await storage writes and replace saved counters, scores and other durable state from the documented canonical response; Twinkle.privateDb.get/set return { item }, and the stored data is item.value (get may return item: null). Never read a guessed response shape, present a local increment as saved, or swallow a save/load error. Keep unfinished work recoverable, show a clear pending or failed-save state, and let the viewer retry deliberately without counting the same action twice. Verify canonical response restoration, rejected saves and delayed replies, not only a successful local animation.
|
|
21
|
+
12. Start sound and image generation inside the viewer's tap or click handler. A Web Audio AudioContext stays silent until it is created or resumed in a tap (iPhones always enforce this), and Twinkle.ai.generateImage called from load or a timer fails with USER_ACTIVATION_REQUIRED.
|
|
22
|
+
13. Call setPointerCapture only inside a pointerdown handler with that event's pointerId, wrapped in try/catch, and release only when hasPointerCapture says the element holds it. From click, mouse, timer or synthetic handlers it throws and the control stops working.
|
|
23
|
+
14. Keyboard games call preventDefault for their keys (Space, arrows, WASD) from a window or document keydown listener added with { passive: false }, skipping inputs, textareas, selects and contenteditable, and make the playfield focusable; otherwise those keys scroll or jump the page during play.
|
|
24
|
+
15. On phones a long press on game controls selects text or opens the Copy/Look Up menu unless the whole game area (controls, HUD, menus, empty play space) is marked data-twinkle-no-select and styled user-select: none; -webkit-user-select: none; -webkit-touch-callout: none. A game drawn without a canvas sets data-twinkle-game on <html> too, so Twinkle's phone touch protections apply. Keep inputs and copyable text selectable with data-twinkle-selectable.
|
|
22
25
|
|
|
23
26
|
## Essential SDK features
|
|
24
27
|
Features every app needs whenever they apply. When you build a new app or make a substantial change, add every one that applies as part of that change, without being asked. On a small, targeted request, add a missing one only when the request touches what it covers (a change to levels, scoring or saving: keep-place; a new screen: back-button); otherwise name it in one line so the creator can ask. Say in one line what you added. Never add one that does not apply.
|
|
@@ -543,7 +546,7 @@ renderBattery(policy?.energyPercent, policy?.energySegmentsRemaining);
|
|
|
543
546
|
- Example: const chatHistory = conversation.slice(-12).map((entry) => ({ role: entry.role === 'assistant' ? 'assistant' : 'user', content: entry.text }));
|
|
544
547
|
const result = await Twinkle.ai.chat({ message, history: chatHistory, systemPrompt: 'You are a cheerful pirate helper who answers in one sentence.', onText: (text, meta) => renderReply(text), onStatus: (status) => setThinking(status === 'thinking') });
|
|
545
548
|
- async decide({ state, questions, images }) | scopes: none
|
|
546
|
-
- Returns: { answers, model, provider, aiUsagePolicy, imageCount? }
|
|
549
|
+
- Returns: { decisionId, answers, model, provider, aiUsagePolicy, imageCount? }
|
|
547
550
|
- Use JEV for fast contextual choices, yes/no probabilities, and scores inside a running app. Send several independent questions about the same state together.
|
|
548
551
|
- Read the AI decision design guidance when deciding whether this capability benefits the app. This method evaluates supplied context; it does not generate prose, browse, run tools, grant access, or award XP/Coins. It looks at pictures only through the preview images option below.
|
|
549
552
|
- state is non-empty text, a JSON object, or a JSON array. questions maps your question IDs to { type, instructions, criteria? }. Instructions may be non-empty text or structured JSON. Every question sees the same state and answers independently; put the complete question in instructions, not just its ID.
|
|
@@ -552,6 +555,7 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
|
|
|
552
555
|
- Yes/no probability: { type: 'noul', instructions, criteria?: { true: description, false: description } }. Returns { type: 'noul', noul }, where noul is the probability of yes between 0 and 1. There is no separate confidence field. If criteria is supplied, describe both true and false.
|
|
553
556
|
- A request accepts 1–32 questions, at most 64 KB of JSON and 20 levels of nesting. Question IDs and option names must be non-empty, at most 128 characters, and cannot be __proto__, constructor, or prototype. Descriptions may use structured JSON; state must contain only data the app is allowed to access.
|
|
554
557
|
- Requires a signed-in viewer and uses the normal runtime AI rate limits. Twinkle selects and calls JEV on its server; never put provider credentials or an external endpoint in app code. Decisions are covered by Twinkle: they never use the viewer's AI Energy, and a viewer with empty Energy still gets answers. The response includes the viewer's current canonical aiUsagePolicy, unchanged by the call.
|
|
558
|
+
- Successful decisions include a server-generated decisionId. Keep it with the returned provider and model when recording a decision outcome; it identifies that decision, not a fight or a reward receipt. A failed request may include requestId when server decision handling began; failures before that point may have no ID.
|
|
555
559
|
- Errors include invalid_ai_decision (bad input), ai_decision_unavailable, ai_decision_timeout, ai_decision_rate_limited, ai_decision_invalid_response, and ai_usage_unavailable, plus the existing auth, access, Energy, and rate-limit errors. Invalid answers are rejected. An answer that fails validation is retried once on the server within the same deadline; there is no other provider retry, fabricated answer, or paid LLM fallback. Do not automatically retry a rejected request.
|
|
556
560
|
- Preview: images (not generally available yet). Pass images: { name: picture } with 1-4 entries to let JEV look at pictures. Names use lowercase letters, digits and underscores (e.g. drawing); each name becomes a key of state, so state must then be a JSON object without that key, and instructions refer to the picture by name (e.g. 'Does `drawing` show a cat?'). A picture is this app's own upload URL (from Twinkle.files, at most 5 MB each), a canvas, or a PNG/JPEG/WebP Blob of at most 3 MB (5 MB of snapshots per decision). All pictures of one decision together must be at most 10 MB; a larger one is refused with 'Image too large: max 5 MB each, 10 MB per decision. Resize the picture first.' so resize big pictures before asking. Image decisions are covered by Twinkle like text decisions. Only some run at once (one per player, several per app, and a generous ceiling per minute for the app), so wait for one answer before asking again and do not call decide in a tight loop. Until Twinkle turns them on for the app, and whenever a picture cannot be checked or too many decisions are running, decide throws ai_decision_unavailable with a reason (disabled, app_disabled, rate_limited, capped, images_dropped, timeout, ...). There is never a text-only answer that ignores the pictures. Use images only when the creator asks for it, keep the feature usable without it (for example skip the AI check and let the player continue), and never use the answer for safety, access, XP or Coins.
|
|
557
561
|
- Call on meaningful app events with bounded frequency, batch questions, and keep rendering and deterministic game rules in local code. Ignore a result if the app state or turn changed while it was pending. Test uncertain inputs and choose a fallback appropriate to the experience; confidence is not a correctness guarantee.
|
|
@@ -599,10 +603,10 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
|
|
|
599
603
|
- Pass requestId when you need to correlate browser logs, backend logs, and iframe status events for one generation.
|
|
600
604
|
- partial_image statuses may include partialImageB64 for progressive preview UI before the final imageUrl arrives.
|
|
601
605
|
- referenceImageB64 may be a raw base64 string or a data:image/...;base64 URL.
|
|
602
|
-
- Optional model: gpt-image-2.
|
|
603
|
-
- Quality accepts low, medium, high, xhigh, or max. xhigh and max require a GPT Image 2.
|
|
606
|
+
- Optional model: gpt-image-2.6-flare (fast) or gpt-image-2.6-sunburst (careful edits, slower). Without a model, OpenAI uses Flare for new images and Sunburst when a reference image or continuation is supplied. GPT Image 2.6 replaced 2.5: an app that still sends gpt-image-2.5-flare or gpt-image-2.5-sunburst gets the 2.6 model of the same name. Explicit gpt-image-2 remains supported.
|
|
607
|
+
- Quality accepts low, medium, high, xhigh, or max. xhigh and max require a GPT Image 2.6 model. Gemini has one quality tier. On 2.6, low and medium are faster but cost almost as much as high (roughly 47% and 82% of high), so pick quality for speed rather than to save energy.
|
|
604
608
|
- engine: 'gemini' uses Google's Nano Banana 2.1 (the fast Gemini image tier; it replaced the retired original Nano Banana on 2026-10-08).
|
|
605
|
-
- GPT Image 2.
|
|
609
|
+
- GPT Image 2.6 battery spending uses actual image-model input and output token usage. The confirmation shows an image-output estimate; prompts and reference images use additional energy.
|
|
606
610
|
- responseId and imageId are opaque continuation handles. Pass them back unchanged to edit a prior result; do not assume an OpenAI ID format. Existing GPT Image 2 continuations remain usable.
|
|
607
611
|
- Example: const result = await Twinkle.ai.generateImage({ prompt: 'Create a fashion guide portrait for this face with flattering colors and outfit ideas', referenceImageB64, quality: 'high', onStatus: (status) => console.log(status.stage) });
|
|
608
612
|
- async generateMusic({ prompt, length, instrumental, requestId, timeoutMs } = {}) | scopes: none
|
|
@@ -1000,13 +1004,14 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
1000
1004
|
- limit is 1-50 (default 10). kind filters to helper or workshop builds.
|
|
1001
1005
|
- Example: const { builds } = await Twinkle.minecraft.getZeroBuilds({ limit: 10, kind: 'workshop' });
|
|
1002
1006
|
- async getPeople() | scopes: content:read
|
|
1003
|
-
- Returns: { canManage, people: [{ uuid, name, role: 'visitor'|'member'|'builder'|'moderator', op, online, isZero, twinkle: { userId, username } | null, ...owner: groups, banned, protected, firstSeenAt, lastSeenAt, endorsements: [{ voucherName, voucherUuid, endorsedAt }] | ...others: seen: 'online'|'today'|'week'|'month'|'older'|null }], roles }
|
|
1007
|
+
- Returns: { canManage, people: [{ uuid, name, role: 'visitor'|'member'|'builder'|'moderator', op, online, isZero, twinkle: { userId, username } | null, ...owner: groups, banned, protected, firstSeenAt, lastSeenAt, endorsements: [{ voucherName, voucherUuid, endorsedAt }] | ...others: seen: 'online'|'today'|'week'|'month'|'older'|null }], visitorBuilders: [{ userId, username, linkedUuids }] | null, roles }
|
|
1004
1008
|
- Everyone who has joined the server with their in-game rank, for a player directory; the server owner also gets the full records for role management.
|
|
1005
1009
|
- Every viewer gets the list. Only the server owner, in an app they own, gets canManage: true with full records (groups, bans, exact first/last seen); everyone else gets canManage: false and the public view: banned players left out, and seen is a rough bucket instead of exact times. Show role controls only when canManage is true.
|
|
1006
1010
|
- Roles: visitor (play and chat), member (/tpa, /home, /back, may ask Zero to build), builder (member + /fly and creative/survival), moderator (builder + teleport others, CoreProtect rollback, /kick). op: true players are server operators and have every power regardless of role.
|
|
1007
1011
|
- protected: true players (the owner and Zero's account) can't be changed from the app. Sorted online first, then most recently seen.
|
|
1008
1012
|
- Not cached; call on screen open or after a change, not on a timer.
|
|
1009
1013
|
- twinkle is the Twinkle account linked to that player with /link, or null.
|
|
1014
|
+
- visitorBuilders lists approved Twinkle-only Builders and linked Minecraft UUIDs; null means unavailable. Merge by userId or linked UUID. This approval permits Zero orders and is not itself an in-game role change.
|
|
1010
1015
|
- endorsements (owner view only) lists who backed this player's Builder application: a moderator or anyone above typed /vouch for a player already on Twinkle. voucherUuid is the voucher's Minecraft uuid (null when endorsed from the app or console); endorsedAt is an ISO time. Empty when there are none, or when the server's vouch list could not be read.
|
|
1011
1016
|
- Example: const { canManage, people } = await Twinkle.minecraft.getPeople(); renderDirectory(people, { editable: canManage });
|
|
1012
1017
|
- async setPlayerRole({ uuid, role }) | scopes: content:write
|
|
@@ -1652,6 +1657,7 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1652
1657
|
- Returns: { item: { id, key, value, updatedAt } | null }
|
|
1653
1658
|
- Read one key from the default private per-user JSON store.
|
|
1654
1659
|
- Reads a single key from the current viewer's private store.
|
|
1660
|
+
- Read the stored value from response.item.value, not response.value or the response itself. item is null when the key is absent; a rejected read is not an empty saved value.
|
|
1655
1661
|
- async list({ prefix, limit, cursor } = {}) | scopes: privateDb:read
|
|
1656
1662
|
- Returns: { items: [{ id, key, value, updatedAt }], cursor? }
|
|
1657
1663
|
- List keys from the default private per-user JSON store.
|
|
@@ -1660,6 +1666,7 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1660
1666
|
- Returns: { item: { id, key, value, updatedAt } }
|
|
1661
1667
|
- Upsert one JSON-serializable value in the default private per-user store.
|
|
1662
1668
|
- Upserts one key for the current viewer. Value must be JSON-serializable (max 16 KB).
|
|
1669
|
+
- Await the write and adopt response.item.value as the confirmed saved state. Keep pending local changes separate; on failure show an unsaved state and retain the work for a deliberate retry instead of swallowing the error or incrementing a saved counter again.
|
|
1663
1670
|
- async remove(key) | scopes: privateDb:write
|
|
1664
1671
|
- Returns: { success: true, deleted: boolean }
|
|
1665
1672
|
- Delete one key from the default private per-user JSON store.
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -5698,6 +5698,28 @@ Bridge Builder qualifies from QR check-ins: 7 different people, or one meetup a
|
|
|
5698
5698
|
Every decision requires Mikey's authorization for that crew and action. Mutations are audited and idempotent; `--output <path>` is an explicit request to retain the canonical JSON. General write receipts omit private crew details.
|
|
5699
5699
|
|
|
5700
5700
|
|
|
5701
|
+
## Grammar Quest legacy Ghost evidence
|
|
5702
|
+
|
|
5703
|
+
`lumine admin english quest-evidence <case.json> --json` previews one member's
|
|
5704
|
+
held legacy retry-evidence case. The private JSON input contains a numeric
|
|
5705
|
+
`userId` and `skillCode`. Compare the returned count, due-date evidence, pending
|
|
5706
|
+
runs and `sourceHash` with the individually reviewed history. To apply a
|
|
5707
|
+
`ready` preview, add its exact `sourceHash` to the same input as `expectedHash`
|
|
5708
|
+
and repeat with `--confirm`. A changed history refuses the request; re-read and
|
|
5709
|
+
review it rather than refreshing the hash blindly. The mutation and
|
|
5710
|
+
idempotency/audit receipt commit together. Use a stable `--idempotency-key` and
|
|
5711
|
+
inspect any unconfirmed outcome before retrying.
|
|
5712
|
+
|
|
5713
|
+
The command resolves a no-effect hold or removes one proven excess below the
|
|
5714
|
+
Ghost threshold. With no completed or open scheduled review, it can also move
|
|
5715
|
+
an erroneously early due date to the reconstructed date only if that date is
|
|
5716
|
+
already past or today, so a due review stays available. It never increases a
|
|
5717
|
+
miss count or changes an answer or Ghost stage. Advanced or open/completed
|
|
5718
|
+
scheduled Ghost reviews, other due-date differences, incomplete history and
|
|
5719
|
+
unrecognized evidence remain held for individual review. The source command is
|
|
5720
|
+
available in this checkout; check the release receipt before assuming a
|
|
5721
|
+
published npm version contains it.
|
|
5722
|
+
|
|
5701
5723
|
## Arena timed-bank item revisions
|
|
5702
5724
|
|
|
5703
5725
|
Reviewed text, option and visual fixes to a published paper go live in place, without
|