@stage5/lumine 0.2.88 → 0.2.90
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/chatlog.js +108 -0
- package/lib/commands.js +11 -0
- package/lib/constants.js +1 -0
- package/lib/rewards.js +23 -0
- package/lib/sdk.js +16 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +192 -8
- package/sdk/LUMINE_ADMIN.md +24 -1
package/lib/chatlog.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// lumine chatlog: what players said in a Build app's realtime world, for the
|
|
2
|
+
// app's owner (or an admin). Logging is off until switched on per app:
|
|
3
|
+
//
|
|
4
|
+
// lumine chatlog [build] [--since 2h] [--limit 100] [--instance <id>] [--json]
|
|
5
|
+
// lumine chatlog enable [build] (players see a notice while it is on)
|
|
6
|
+
// lumine chatlog disable [build]
|
|
7
|
+
import { requestJson } from "./http.js";
|
|
8
|
+
import { ensureAuth, assertAuthScope } from "./auth.js";
|
|
9
|
+
import { resolveRequiredBuildId } from "./util.js";
|
|
10
|
+
import { resolveRequiredBuildIdOrSelected } from "./commands.js";
|
|
11
|
+
|
|
12
|
+
const ACTIONS = new Set(["enable", "disable", "show"]);
|
|
13
|
+
|
|
14
|
+
async function resolveTarget(options, auth, rawTarget) {
|
|
15
|
+
if (rawTarget) {
|
|
16
|
+
const buildId = resolveRequiredBuildId(rawTarget);
|
|
17
|
+
if (!Number.isSafeInteger(buildId) || buildId <= 0) {
|
|
18
|
+
throw new Error("Pass a Twinkle build URL or positive integer build id.");
|
|
19
|
+
}
|
|
20
|
+
return buildId;
|
|
21
|
+
}
|
|
22
|
+
return await resolveRequiredBuildIdOrSelected(options, auth);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatTime(unixSeconds) {
|
|
26
|
+
const d = new Date(Number(unixSeconds) * 1000);
|
|
27
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
28
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function chatlogCommand(options) {
|
|
32
|
+
const positional = options.positional || [];
|
|
33
|
+
const action = ACTIONS.has(String(positional[0] || ""))
|
|
34
|
+
? String(positional[0])
|
|
35
|
+
: "show";
|
|
36
|
+
const rawTarget = String(
|
|
37
|
+
options.target && !ACTIONS.has(options.target)
|
|
38
|
+
? options.target
|
|
39
|
+
: action === "show"
|
|
40
|
+
? positional[0] || ""
|
|
41
|
+
: positional[1] || "",
|
|
42
|
+
).trim();
|
|
43
|
+
const auth = await ensureAuth(options);
|
|
44
|
+
await assertAuthScope({
|
|
45
|
+
options,
|
|
46
|
+
auth,
|
|
47
|
+
scope: action === "show" ? "build:read" : "build:write",
|
|
48
|
+
});
|
|
49
|
+
const buildId = await resolveTarget(options, auth, rawTarget);
|
|
50
|
+
|
|
51
|
+
if (action !== "show") {
|
|
52
|
+
const enabled = action === "enable";
|
|
53
|
+
const result = await requestJson({
|
|
54
|
+
method: "PUT",
|
|
55
|
+
url: `${options.apiUrl}/build/${buildId}/chat-log-setting`,
|
|
56
|
+
authToken: auth.token,
|
|
57
|
+
body: { enabled },
|
|
58
|
+
timeoutMs: options.timeoutMs,
|
|
59
|
+
});
|
|
60
|
+
if (options.json) {
|
|
61
|
+
console.log(JSON.stringify(result, null, 2));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
console.log(
|
|
65
|
+
enabled
|
|
66
|
+
? `Chat logging is ON for ${result.title} (#${result.buildId}). Lines are kept ${result.retentionDays} days, and players see a notice while it is on.`
|
|
67
|
+
: `Chat logging is OFF for ${result.title} (#${result.buildId}). Nothing new is stored; saved lines expire after ${result.retentionDays} days.`,
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const query = new URLSearchParams();
|
|
73
|
+
query.set("since", String(options.chatlogSince || "24h"));
|
|
74
|
+
const limit = Math.floor(Number(options.chatlogLimit || 100));
|
|
75
|
+
query.set("limit", String(Number.isFinite(limit) && limit > 0 ? limit : 100));
|
|
76
|
+
if (options.chatlogInstance) query.set("instance", options.chatlogInstance);
|
|
77
|
+
if (options.cursor) query.set("cursor", String(options.cursor));
|
|
78
|
+
const result = await requestJson({
|
|
79
|
+
method: "GET",
|
|
80
|
+
url: `${options.apiUrl}/build/${buildId}/chat-log?${query.toString()}`,
|
|
81
|
+
authToken: auth.token,
|
|
82
|
+
timeoutMs: options.timeoutMs,
|
|
83
|
+
});
|
|
84
|
+
if (options.json) {
|
|
85
|
+
console.log(JSON.stringify(result, null, 2));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const messages = Array.isArray(result.messages) ? result.messages : [];
|
|
89
|
+
console.log(
|
|
90
|
+
`Chat log for ${result.title} (#${result.buildId}): logging ${result.enabled ? "ON" : "OFF"}, kept ${result.retentionDays} days.`,
|
|
91
|
+
);
|
|
92
|
+
if (!messages.length) {
|
|
93
|
+
console.log(
|
|
94
|
+
result.enabled
|
|
95
|
+
? `No chat since ${query.get("since")}.`
|
|
96
|
+
: "Nothing logged. Turn it on with: lumine chatlog enable",
|
|
97
|
+
);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// oldest first, like reading a conversation
|
|
101
|
+
for (const m of [...messages].reverse()) {
|
|
102
|
+
const who = m.username || (m.guest ? "Guest" : `user ${m.userId}`);
|
|
103
|
+
console.log(`${formatTime(m.createdAt)} [${m.instanceId || "-"}] ${who}: ${m.text}`);
|
|
104
|
+
}
|
|
105
|
+
if (result.nextCursor) {
|
|
106
|
+
console.log(`Older lines: lumine chatlog ${result.buildId} --since ${query.get("since")} --cursor ${result.nextCursor}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
package/lib/commands.js
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
} from "./assets.js";
|
|
59
59
|
import { thumbnailCommand } from "./thumbnail.js";
|
|
60
60
|
import { rewardsCommand, reportRewardDeclaration } from "./rewards.js";
|
|
61
|
+
import { chatlogCommand } from "./chatlog.js";
|
|
61
62
|
import {
|
|
62
63
|
assertAuthScope,
|
|
63
64
|
ensureAuth,
|
|
@@ -275,6 +276,10 @@ export async function main() {
|
|
|
275
276
|
await assetsCommand(options);
|
|
276
277
|
return;
|
|
277
278
|
}
|
|
279
|
+
if (options.command === "chatlog") {
|
|
280
|
+
await chatlogCommand(options);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
278
283
|
if (options.command === "rewards") {
|
|
279
284
|
await rewardsCommand(options);
|
|
280
285
|
return;
|
|
@@ -2472,6 +2477,9 @@ export function parseArgs(args) {
|
|
|
2472
2477
|
: ""),
|
|
2473
2478
|
).trim() || "",
|
|
2474
2479
|
cursor: Math.max(0, Math.floor(Number(raw.cursor) || 0)),
|
|
2480
|
+
chatlogSince: raw.since ? String(raw.since) : "",
|
|
2481
|
+
chatlogLimit: raw.limit ? String(raw.limit) : "",
|
|
2482
|
+
chatlogInstance: raw.instance ? String(raw.instance) : "",
|
|
2475
2483
|
adminCursor: raw.cursor ? String(raw.cursor) : "",
|
|
2476
2484
|
adminAfter: raw.after ? String(raw.after) : "",
|
|
2477
2485
|
adminPostedAfter: raw.postedAfter ? String(raw.postedAfter) : "",
|
|
@@ -2939,9 +2947,12 @@ export function printHelp() {
|
|
|
2939
2947
|
lumine assets generate "<prompt>" --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana>
|
|
2940
2948
|
lumine assets delete <assetId>
|
|
2941
2949
|
lumine assets prune [--yes]
|
|
2950
|
+
lumine chatlog [build] [--since 2h] [--limit 100] [--instance <id>] [--json]
|
|
2951
|
+
lumine chatlog enable|disable [build]
|
|
2942
2952
|
lumine rewards check
|
|
2943
2953
|
lumine rewards sheet <file.json>
|
|
2944
2954
|
lumine rewards sheet --show
|
|
2955
|
+
lumine rewards review
|
|
2945
2956
|
lumine thumbnail set <file>
|
|
2946
2957
|
lumine thumbnail capture [--out <file>]
|
|
2947
2958
|
lumine thumbnail generate ["<prompt>"] --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana>
|
package/lib/constants.js
CHANGED
package/lib/rewards.js
CHANGED
|
@@ -203,6 +203,28 @@ export async function rewardsCommand(options) {
|
|
|
203
203
|
if (!result.ok) process.exitCode = 1;
|
|
204
204
|
return;
|
|
205
205
|
}
|
|
206
|
+
if (action === "review") {
|
|
207
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
208
|
+
const result = await requestJson({
|
|
209
|
+
url: `${options.apiUrl}/build/${buildId}/rewards/reviews`,
|
|
210
|
+
method: "POST",
|
|
211
|
+
authToken: auth.token,
|
|
212
|
+
timeoutMs: options.timeoutMs,
|
|
213
|
+
body: {},
|
|
214
|
+
});
|
|
215
|
+
if (options.json) {
|
|
216
|
+
console.log(JSON.stringify(result, null, 2));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const reviewId = result?.policy?.latestReviewId;
|
|
220
|
+
console.log(
|
|
221
|
+
`Sent Build ${buildId} for reward review${reviewId ? ` (request #${reviewId})` : ""}. State: ${result?.state || "unknown"}.`,
|
|
222
|
+
);
|
|
223
|
+
console.log(
|
|
224
|
+
"The saved version and the question sheet on file are frozen for the reviewer. Saving again closes this request; send it again after the save.",
|
|
225
|
+
);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
206
228
|
printRewardsHelp();
|
|
207
229
|
throw new Error(`Unknown rewards action: ${action}`);
|
|
208
230
|
}
|
|
@@ -212,6 +234,7 @@ function printRewardsHelp() {
|
|
|
212
234
|
lumine rewards check Validate rewards.json (workspace or saved) against the question sheet on file
|
|
213
235
|
lumine rewards sheet <file.json> Upload the private question sheet ({ rules: { <ruleId>: { questions?, sets? } } })
|
|
214
236
|
lumine rewards sheet --show Summarize the sheet on file (never prints answer keys)
|
|
237
|
+
lumine rewards review Send the saved version (with the sheet on file) for XP/Coin review, like "Send for review" on the website
|
|
215
238
|
|
|
216
239
|
rewards.json (project root) declares the economy the reviewer approves:
|
|
217
240
|
{ "userDailyXP", "userDailyCoins", "userDailyClaims"?,
|
package/lib/sdk.js
CHANGED
|
@@ -35,6 +35,22 @@ export const SDK_CLI_METHODS = {
|
|
|
35
35
|
"minecraft.getMyLink": { path: "api/minecraft/link", scopes: ["content:read"] },
|
|
36
36
|
"minecraft.createLinkCode": { path: "api/minecraft/link/code", scopes: ["content:write"], write: true },
|
|
37
37
|
"minecraft.unlinkMinecraft": { path: "api/minecraft/link/unlink", scopes: ["content:write"], write: true },
|
|
38
|
+
"minecraft.getDesigns": { path: "api/minecraft/designs", scopes: ["content:read"] },
|
|
39
|
+
"minecraft.getDesign": { path: "api/minecraft/design", scopes: ["content:read"] },
|
|
40
|
+
"minecraft.saveDesign": { path: "api/minecraft/designs/save", scopes: ["content:write"], write: true },
|
|
41
|
+
"minecraft.updateDesign": { path: "api/minecraft/designs/update", scopes: ["content:write"], write: true },
|
|
42
|
+
"minecraft.deleteDesign": { path: "api/minecraft/designs/delete", scopes: ["content:write"], write: true },
|
|
43
|
+
"minecraft.checkPlacement": { path: "api/minecraft/placement/check", scopes: ["content:read"] },
|
|
44
|
+
"minecraft.buildDesign": { path: "api/minecraft/placement/build", scopes: ["content:write"], write: true },
|
|
45
|
+
"minecraft.stopZero": { path: "api/minecraft/zero/stop", scopes: ["content:write"], write: true },
|
|
46
|
+
"minecraft.undoBuild": { path: "api/minecraft/zero/undo", scopes: ["content:write"], write: true },
|
|
47
|
+
"minecraft.getSnaps": { path: "api/minecraft/snaps", scopes: ["content:read"] },
|
|
48
|
+
"minecraft.getSnap": { path: "api/minecraft/snap", scopes: ["content:read"] },
|
|
49
|
+
"minecraft.deleteSnap": { path: "api/minecraft/snaps/delete", scopes: ["content:write"], write: true },
|
|
50
|
+
"minecraft.getServerLogs": { path: "api/minecraft/logs", scopes: ["content:read"] },
|
|
51
|
+
"minecraft.getArrivalPoints": { path: "api/minecraft/homes", scopes: ["content:read"] },
|
|
52
|
+
"minecraft.setArrivalPoint": { path: "api/minecraft/homes/set", scopes: ["content:write"], write: true },
|
|
53
|
+
"minecraft.clearArrivalPoint": { path: "api/minecraft/homes/clear", scopes: ["content:write"], write: true },
|
|
38
54
|
"subjects.getMySubjects": { path: "api/content/my-subjects", scopes: ["content:read"] },
|
|
39
55
|
"subjects.search": { path: "api/content/subjects/search", scopes: ["content:read"] },
|
|
40
56
|
"subjects.getSubject": { path: "api/content/subject", scopes: ["content:read"] },
|
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-09-
|
|
5
|
-
Generated: 2026-09-
|
|
3
|
+
Version: 1.59.0
|
|
4
|
+
Updated: 2026-09-26
|
|
5
|
+
Generated: 2026-09-26T05:55:48.545Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -488,9 +488,10 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
|
|
|
488
488
|
- 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.
|
|
489
489
|
- 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.
|
|
490
490
|
- Example: const { answers } = await Twinkle.ai.decide({ state: { playerRequest, availableActions }, questions: { action: { type: 'choice', instructions: 'Which available companion action best fits playerRequest? Use wait when unclear.', criteria: { follow: 'Follow the player', guard: 'Stay and keep watch', wait: 'Do nothing until clarified' } }, needsClarification: { type: 'noul', instructions: 'Is playerRequest too ambiguous to act on?' } } }); const action = answers.needsClarification.noul > 0.5 ? 'wait' : answers.action.choice;
|
|
491
|
-
- async generateObject({ prompt, expectedStructure, thinkingMode, mode, model, instructions, systemPrompt, webSearch, requestId, onText, onStatus, onReasoning } = {}) | scopes: none
|
|
491
|
+
- async generateObject({ prompt, expectedStructure, images, thinkingMode, mode, model, instructions, systemPrompt, webSearch, requestId, onText, onStatus, onReasoning } = {}) | scopes: none
|
|
492
492
|
- Returns: { object, result, model, provider, thinkingMode, requestedThinkingMode, requestedModel, webSearch, aiUsagePolicy }
|
|
493
493
|
- Generate a validated structured JSON object for app decisions, routing, grading, and game-state logic, with optional live output/status callbacks and web search.
|
|
494
|
+
- images: up to 3 reference image URLs the model looks at along with the prompt (a sketch, a photo, a screenshot). They must be Twinkle-hosted uploads (Twinkle.files.pickAndUpload / uploadGenerated asset URLs; PNG, JPEG, WebP or GIF); anything else is refused with 400 before any model call. Image input counts toward the AI Energy of the call like any other input.
|
|
494
495
|
- Signed-in viewers only.
|
|
495
496
|
- Use this instead of asking Twinkle.ai.chat to return JSON.
|
|
496
497
|
- expectedStructure must be a JSON object that describes the exact returned object shape.
|
|
@@ -532,6 +533,20 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
|
|
|
532
533
|
- GPT Image 2.5 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.
|
|
533
534
|
- 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.
|
|
534
535
|
- 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) });
|
|
536
|
+
- async generateMusic({ prompt, length, instrumental, requestId, timeoutMs } = {}) | scopes: none
|
|
537
|
+
- Returns: { success, asset, url, mimeType, structure, length, instrumental, model, requestId, replayed?, aiUsagePolicy }
|
|
538
|
+
- Generate a finished piece of music (rendered audio, not notes) from a text description with Google Lyria. The audio is saved to the viewer's own Twinkle.files and returned as an asset URL.
|
|
539
|
+
- Signed-in viewers only. Call it directly from an explicit viewer action such as a button click; calls from page load, timers or programmatic retries are rejected (USER_ACTIVATION_REQUIRED).
|
|
540
|
+
- Twinkle shows a host-owned confirmation with the battery cost for every generation. One approval authorizes exactly one request.
|
|
541
|
+
- length: 'full' (default) is a complete song of about two to three minutes (Lyria 3.5); 'clip' is a 30-second piece (Lyria 3 Clip) at half the cost.
|
|
542
|
+
- Describe genre, mood, instruments, tempo and structure in the prompt; ask for vocals or pass instrumental: true for no vocals. Duration and vocals have no other controls.
|
|
543
|
+
- Prompts naming artists, bands, songs or copyrighted lyrics are refused with code music_prompt_blocked and cost nothing: show the viewer the error message so they can reword it.
|
|
544
|
+
- The result is a normal Twinkle.files asset (asset.id, asset.url, audio/mpeg) owned by the viewer and counted against their file storage; it also appears in Twinkle.files lists. Store asset.url (e.g. in privateDb/sharedDb) to play it later.
|
|
545
|
+
- structure is the model's song-structure text (section markers, and lyrics when there are vocals).
|
|
546
|
+
- AI Energy is charged only after the music is saved. Failures, timeouts and refusals are not charged.
|
|
547
|
+
- Only one music generation per viewer may run at a time (code ai_music_generation_in_progress). A retry with the same requestId returns the finished result without paying again (replayed: true), or code music_in_progress while it is still being made.
|
|
548
|
+
- Generation usually takes one to three minutes; the SDK timeout defaults to 600000ms. Show progress UI while waiting.
|
|
549
|
+
- Example: const song = await Twinkle.ai.generateMusic({ prompt: 'Warm lo-fi hip hop with dusty drums, a mellow Rhodes and rain in the background', instrumental: true }); audio.src = song.url;
|
|
535
550
|
- onImageGenerationStatus(listener) | scopes: none
|
|
536
551
|
- Returns: unsubscribe function
|
|
537
552
|
- Subscribe to real-time image generation status events forwarded into the build iframe.
|
|
@@ -787,14 +802,14 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
787
802
|
- limit is 1-50 (default 10). kind filters to helper or workshop builds.
|
|
788
803
|
- Example: const { builds } = await Twinkle.minecraft.getZeroBuilds({ limit: 10, kind: 'workshop' });
|
|
789
804
|
- async getPeople() | scopes: content:read
|
|
790
|
-
- Returns: { canManage, people: [{ uuid, name, role: 'visitor'|'member'|'builder'|'moderator',
|
|
791
|
-
- Everyone who has joined the server with their in-game
|
|
792
|
-
- Only the server owner, in an app they own, gets
|
|
805
|
+
- Returns: { canManage, people: [{ uuid, name, role: 'visitor'|'member'|'builder'|'moderator', op, online, isZero, twinkle: { userId, username } | null, ...owner: groups, banned, protected, firstSeenAt, lastSeenAt | ...others: seen: 'online'|'today'|'week'|'month'|'older'|null }], roles }
|
|
806
|
+
- 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.
|
|
807
|
+
- 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.
|
|
793
808
|
- 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.
|
|
794
809
|
- protected: true players (the owner and Zero's account) can't be changed from the app. Sorted online first, then most recently seen.
|
|
795
810
|
- Not cached; call on screen open or after a change, not on a timer.
|
|
796
811
|
- twinkle is the Twinkle account linked to that player with /link, or null.
|
|
797
|
-
- Example: const { canManage, people } = await Twinkle.minecraft.getPeople();
|
|
812
|
+
- Example: const { canManage, people } = await Twinkle.minecraft.getPeople(); renderDirectory(people, { editable: canManage });
|
|
798
813
|
- async setPlayerRole({ uuid, role }) | scopes: content:write
|
|
799
814
|
- Returns: { player: { uuid, name, role, op } }
|
|
800
815
|
- Change a player's in-game role; it applies immediately, even while they are online.
|
|
@@ -845,6 +860,175 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
845
860
|
- Remove one of the viewer's own linked Minecraft accounts.
|
|
846
861
|
- Only the viewer's own links; removed is false when there was nothing to remove.
|
|
847
862
|
- Example: await Twinkle.minecraft.unlinkMinecraft({ uuid: link.uuid });
|
|
863
|
+
- async getDesigns({ query } = {}) | scopes: content:read
|
|
864
|
+
- Returns: { designs: [{ id, version, name, category, summary, size: { width, height, depth }, blocks, status, author, authorUserId, visibility: 'private'|'public', previewUrl, createdAt }], access: { rank, linked, canOrder, canModerate, isOwner, canKeepPrivate } | null }
|
|
865
|
+
- Zero's design library: published designs plus the viewer's own private ones.
|
|
866
|
+
- Built-in designs (redstone devices and so on) have no author and are public.
|
|
867
|
+
- previewUrl is an image set with updateDesign, or null.
|
|
868
|
+
- access says what this viewer may do: canOrder shows placement tools (builders, moderators, the owner).
|
|
869
|
+
- Example: const { designs } = await Twinkle.minecraft.getDesigns({ query: 'castle' });
|
|
870
|
+
- async getDesign({ id }) | scopes: content:read
|
|
871
|
+
- Returns: { design: { id, version, name, category, summary, size: { width, height, depth }, blocks, status, author, authorUserId, visibility: 'private'|'public', previewUrl, createdAt }, size: [w, h, d], palette: [blockState], cells: number[] }
|
|
872
|
+
- A design's blocks for a 3D preview.
|
|
873
|
+
- cells is a flat list of [x, y, z, paletteIndex] numbers; air is left out. palette entries are block states like "oak_stairs[facing=north,half=bottom]".
|
|
874
|
+
- Private designs are only returned to their designer.
|
|
875
|
+
- Example: const { size, palette, cells } = await Twinkle.minecraft.getDesign({ id: 'harbor_house' }); for (let i = 0; i < cells.length; i += 4) addCube(cells[i], cells[i + 1], cells[i + 2], palette[cells[i + 3]]);
|
|
876
|
+
- async saveDesign({ id, name, summary, category, tags, visibility, parts }) | scopes: content:write
|
|
877
|
+
- Returns: { design: { id, version, name, category, summary, size: { width, height, depth }, blocks, status, author, authorUserId, visibility: 'private'|'public', previewUrl, createdAt } }
|
|
878
|
+
- Save a design (or a new version of your design) from blueprint parts.
|
|
879
|
+
- parts use Zero's blueprint shapes: box, hollow_box, walls, floor, line, block, cylinder, sphere, gable_roof, pyramid_roof; offsets x = east, y = up, z = south; later parts override earlier ones (carve doors/windows with "air"). Up to 60,000 blocks and 96 blocks in each direction.
|
|
880
|
+
- category is building, decor, farm or path. visibility private (default) or public. Saving the same id again adds a version; only its designer can do that. Rejections come back as 400 minecraft_studio_rejected with a readable message.
|
|
881
|
+
- Who may: any signed-in viewer; they own what they save (they can change or delete it). Only builders and above (access.canKeepPrivate from getDesigns) may keep a design private; for anyone else visibility must be 'public' (omitted means public) or the call fails with 403 minecraft_private_needs_builder. Builders and above save private designs when visibility is omitted. Players have a limit on how many designs they keep.
|
|
882
|
+
- Example: await Twinkle.minecraft.saveDesign({ id: 'harbor_house', name: 'Harbor house', category: 'building', visibility: 'private', parts: [{ shape: 'floor', from: [0, 0, 0], to: [8, 0, 6], block: 'stone_bricks' }, { shape: 'walls', from: [0, 1, 0], to: [8, 4, 6], block: 'spruce_planks' }] });
|
|
883
|
+
- async updateDesign({ id, visibility, previewUrl, name, summary }) | scopes: content:write
|
|
884
|
+
- Returns: { design: { id, version, name, category, summary, size: { width, height, depth }, blocks, status, author, authorUserId, visibility: 'private'|'public', previewUrl, createdAt } }
|
|
885
|
+
- Publish or unpublish a design, set its preview image, or rename it.
|
|
886
|
+
- previewUrl is typically a Twinkle.files upload of a rendered preview.
|
|
887
|
+
- Who may: a design's own designer; moderators (by linked account) and the server owner may change or delete anyone's. Only builders and above may set visibility 'private' (403 minecraft_private_needs_builder otherwise).
|
|
888
|
+
- Example: await Twinkle.minecraft.updateDesign({ id: 'harbor_house', visibility: 'public' });
|
|
889
|
+
- async deleteDesign({ id }) | scopes: content:write
|
|
890
|
+
- Returns: { deleted }
|
|
891
|
+
- Remove one of your designs from the library.
|
|
892
|
+
- Built-in designs can't be deleted.
|
|
893
|
+
- Who may: a design's own designer; moderators (by linked account) and the server owner may change or delete anyone's.
|
|
894
|
+
- Example: await Twinkle.minecraft.deleteDesign({ id: 'old_test' });
|
|
895
|
+
- async checkPlacement({ designId, world, x, z, y?, facing?: 'north'|'east'|'south'|'west' }) | scopes: content:read
|
|
896
|
+
- Returns: { check: { clear, reasons, world, facing, anchor, bounds: { min, max }, ground: { y, min, max, water }, builtBlocks, builtSamples, placedBy: [{ name, blocks }], design } }
|
|
897
|
+
- Check a building site for a design: where it would sit and whether it would touch anyone's builds.
|
|
898
|
+
- Who may: the server owner, and builders or moderators identified by their linked Minecraft account (Twinkle.minecraft.createLinkCode + /link). Others get 403 minecraft_not_linked or minecraft_rank_too_low.
|
|
899
|
+
- The footprint is centred on x/z and turned to face facing; without y it sits on the ground. clear is false when built (non-natural) blocks stand in the footprint or 3 blocks around it, the ground is very uneven, or it would leave the world; reasons explains. placedBy lists players CoreProtect recorded placing blocks there.
|
|
900
|
+
- Call on a user action (drop or rotate), not while dragging.
|
|
901
|
+
- Example: const { check } = await Twinkle.minecraft.checkPlacement({ designId: 'harbor_house', world: 'world', x: 1200, z: 700, facing: 'south' }); outline.color = check.clear ? 'green' : 'red';
|
|
902
|
+
- async buildDesign({ ...placement, force }) | scopes: content:write
|
|
903
|
+
- Returns: { jobId, check, priority }
|
|
904
|
+
- Order Zero to build a design at a site; he switches to creative and builds it before other work.
|
|
905
|
+
- Who may: the server owner, and builders or moderators identified by their linked Minecraft account (Twinkle.minecraft.createLinkCode + /link). Others get 403 minecraft_not_linked or minecraft_rank_too_low.
|
|
906
|
+
- Follow progress with getZero() (currentBuild) and getZeroBuilds().
|
|
907
|
+
- Builders: one order waiting or building at a time, published designs or their own, and orders wait in the normal queue (priority false). Moderators: up to 3 orders, ahead of other work. The owner: no limit. force (build on a site that isn't clear) is owner-only.
|
|
908
|
+
- Example: const { jobId } = await Twinkle.minecraft.buildDesign({ designId: 'harbor_house', world: 'world', x: 1200, z: 700, facing: 'south' });
|
|
909
|
+
- async stopZero() | scopes: content:write
|
|
910
|
+
- Returns: { stopped, message }
|
|
911
|
+
- Stop the build Zero is doing right now (placed blocks stay).
|
|
912
|
+
- Who may: the server owner, and builders or moderators identified by their linked Minecraft account (Twinkle.minecraft.createLinkCode + /link). Others get 403 minecraft_not_linked or minecraft_rank_too_low.
|
|
913
|
+
- Anyone may stop or undo their own builds; moderators also builders' and members' builds and Zero's own projects; the owner anything.
|
|
914
|
+
- Example: await Twinkle.minecraft.stopZero();
|
|
915
|
+
- async undoBuild({ jobId }) | scopes: content:write
|
|
916
|
+
- Returns: { undone, title, message }
|
|
917
|
+
- Undo one of Zero's builds, putting back what was there before.
|
|
918
|
+
- Who may: the server owner, and builders or moderators identified by their linked Minecraft account (Twinkle.minecraft.createLinkCode + /link). Others get 403 minecraft_not_linked or minecraft_rank_too_low.
|
|
919
|
+
- Anyone may stop or undo their own builds; moderators also builders' and members' builds and Zero's own projects; the owner anything.
|
|
920
|
+
- Example: await Twinkle.minecraft.undoBuild({ jobId });
|
|
921
|
+
- async getSnaps({ before, limit, uuid } = {}) | scopes: content:read
|
|
922
|
+
- Returns: { snaps: [{ id, takenAt, name, uuid, world, x, y, z, caption, blocks, players, twinkleUserId }] }
|
|
923
|
+
- The /snap Gallery: 3D scenes players took in game with /snap, newest first.
|
|
924
|
+
- Every viewer gets the list. before is a time in ms (the takenAt of the last snap you have) for paging; limit 1-60 (default 30); uuid filters to one player's snaps.
|
|
925
|
+
- Example: const { snaps } = await Twinkle.minecraft.getSnaps({ limit: 30 });
|
|
926
|
+
- async getSnap({ id }) | scopes: content:read
|
|
927
|
+
- Returns: { scene: { version, id, at, by, caption, origin: [x, y, z], size: [w, h, d], palette, count, blocks, light, biomes: { palette, grid }, camera: { x, y, z, yaw, pitch, fov }, world: { name, environment, time, moonPhase, storm, thunder }, players: [{ name, uuid, x, y, z, yaw, pitch, bodyYaw, pose, sneaking, self, zero, slim, skin }] } }
|
|
928
|
+
- One snap's scene, to render in 3D from where it was taken.
|
|
929
|
+
- blocks is base64 of 5 bytes per visible block: x, y, z (relative to origin) and the palette index as a big-endian uint16. palette entries are block states like oak_stairs[facing=east,half=bottom,shape=straight,waterlogged=false].
|
|
930
|
+
- light is base64 of 7 bytes per block, in the same order: the light each face receives (+x, -x, +y, -y, +z, -z) and the block's own cell, each sky << 4 | block (0-15).
|
|
931
|
+
- biomes.grid is base64 of one byte per column (x-major: index = x * depth + z) into biomes.palette (biome ids like plains, cherry_grove).
|
|
932
|
+
- camera is in world coordinates (subtract origin for scene space); yaw 0 looks toward +z (south), 90 toward -x; pitch is positive looking down. world.time is the tick of day (0 sunrise, 6000 noon, 18000 midnight).
|
|
933
|
+
- players[].skin is a data: URL of the 64x64 skin PNG when known; slim means the thin-armed model.
|
|
934
|
+
- Scenes are large (up to a few MB); fetch one at a time.
|
|
935
|
+
- Example: const { scene } = await Twinkle.minecraft.getSnap({ id });
|
|
936
|
+
- async deleteSnap({ id }) | scopes: content:write
|
|
937
|
+
- Returns: { deleted }
|
|
938
|
+
- Delete a snap from the Gallery.
|
|
939
|
+
- Who may: the player who took it (by a linked Minecraft account), moderators, and the server owner. Others get 403 minecraft_snap_forbidden.
|
|
940
|
+
- Example: await Twinkle.minecraft.deleteSnap({ id });
|
|
941
|
+
- async getArrivalPoints() | scopes: content:read
|
|
942
|
+
- Returns: { accounts: [{ uuid, name, online, world, worldLabel, points: [{ world, label, x, y, z, savedAt }] }] }
|
|
943
|
+
- The viewer's private arrival points: where the Twinkle Gate portals drop their linked Minecraft account in each world.
|
|
944
|
+
- Only the caller's own linked accounts; 400 minecraft_not_linked when none is linked.
|
|
945
|
+
- online/world tell whether setArrivalPoint can use the player's current spot right now.
|
|
946
|
+
- Example: const { accounts } = await Twinkle.minecraft.getArrivalPoints();
|
|
947
|
+
- async setArrivalPoint({ uuid } = {}) | scopes: content:write
|
|
948
|
+
- Returns: { uuid, name, world, worldLabel, points }
|
|
949
|
+
- Save where the viewer's Minecraft player is standing right now as their arrival point in that world (the Twinkle Gate portal to that world lands them there).
|
|
950
|
+
- The player must be online; the spot is their current position, never typed coordinates (so nobody can land inside places they could not walk to).
|
|
951
|
+
- 400 minecraft_arrival_rejected when not online or the spot is unsafe (no floor, no room, lava); uuid picks one of several linked accounts.
|
|
952
|
+
- Example: const { worldLabel } = await Twinkle.minecraft.setArrivalPoint();
|
|
953
|
+
- async clearArrivalPoint({ world, uuid }) | scopes: content:write
|
|
954
|
+
- Returns: { cleared, points }
|
|
955
|
+
- Remove the viewer's arrival point in a world (back to the shared arrival).
|
|
956
|
+
- world is a world id such as world, world1, world3, world_nether.
|
|
957
|
+
- Example: await Twinkle.minecraft.clearArrivalPoint({ world: 'world3' });
|
|
958
|
+
- async getPlayAreas() | scopes: content:read
|
|
959
|
+
- Returns: { levels: [{ id, name, world, box: [x0,y0,z0,x1,y1,z1], spawn: [x,y,z], by, createdAt, status: 'capturing'|'ready'|'failed', chunkCount, done, blocks }] }
|
|
960
|
+
- Play areas: captured Minecraft builds (a castle, a cathedral) that apps turn into first-person games, newest first.
|
|
961
|
+
- Anyone can read. box is inclusive world coordinates; status 'capturing' areas are still being saved (done of chunkCount).
|
|
962
|
+
- Example: const { levels } = await Twinkle.minecraft.getPlayAreas();
|
|
963
|
+
- async getPlayArea({ id }) | scopes: content:read
|
|
964
|
+
- Returns: { level: { ...getPlayAreas fields, chunks: [[cx, cz, visibleBlocks]] } }
|
|
965
|
+
- One play area with its list of captured chunk columns, to stream with getPlayAreaChunk.
|
|
966
|
+
- Chunk columns are 16x16 blocks (cx = floor(x / 16)); load the ones near the player first.
|
|
967
|
+
- Example: const { level } = await Twinkle.minecraft.getPlayArea({ id: 'twinkle-keep' });
|
|
968
|
+
- async getPlayAreaChunks({ id, chunks: [[cx, cz], ...] }) | scopes: content:read
|
|
969
|
+
- Returns: { scenes: [{ cx, cz, scene: { origin: [x,y,z], size: [w,h,d], palette, blocks, light, biomes, count, ... } | null }] }
|
|
970
|
+
- Up to 9 chunk columns of a play area in the /snap scene format (packed visible blocks with per-face light), cropped to the area's box. Stream the ones around the player.
|
|
971
|
+
- Same scene format as getSnap; only blocks with a visible face inside the area are included. scene is null for a chunk that is not part of the area.
|
|
972
|
+
- At most 9 chunks per call; reads share the 180-per-minute build read limit, so cache chunks and load the nearest first.
|
|
973
|
+
- Example: const { scenes } = await Twinkle.minecraft.getPlayAreaChunks({ id: level.id, chunks: [[64, 29], [65, 29]] });
|
|
974
|
+
- async createPlayArea({ name, world, box, spawn, id }) | scopes: content:write
|
|
975
|
+
- Returns: { level }
|
|
976
|
+
- Capture a box of a Minecraft world as a new play area (or re-capture an existing id). Runs in the background: poll getPlayArea until status is 'ready'.
|
|
977
|
+
- Moderators and the server owner only; others get 403 minecraft_level_forbidden.
|
|
978
|
+
- At most 320 blocks across each way and 255 tall. spawn [x,y,z] is where players start (default: top centre of the box).
|
|
979
|
+
- Example: await Twinkle.minecraft.createPlayArea({ name: 'Sand Castle', world: 'world', box: [900, 60, 400, 980, 130, 470] });
|
|
980
|
+
- async getBuildQueue() | scopes: content:read
|
|
981
|
+
- Returns: { active: job | null, queue: [{ id, title, kind, status, requester, world, bounds: { min, max }, designId, position, priority, mine, placed, total }], campaign: { id, title, active, steps, done, skipped, percent, current, resumeAt, world, steps: [{ index, title, status, designId, anchor, bounds, blocks }] } | null, noBuildWorlds, access: { rank, linked, canOrder, canModerate, isOwner } | null }
|
|
982
|
+
- Zero's build queue: the build he is on, the orders waiting (in the order he will take them) and the campaign steps he works through when no order waits. For an RTS-style build panel and for drawing queued footprints on a map.
|
|
983
|
+
- Everyone can read it; mine marks the viewer's own orders (linked account). Orders always run before campaign steps.
|
|
984
|
+
- noBuildWorlds lists worlds Zero never builds in (World 3, World 4): don't offer placement there.
|
|
985
|
+
- Example: const { active, queue, campaign, access } = await Twinkle.minecraft.getBuildQueue();
|
|
986
|
+
- async moveQueuedBuild({ jobId, to }) | scopes: content:write
|
|
987
|
+
- Returns: same as getBuildQueue
|
|
988
|
+
- Server owner: move a waiting order to position `to` (0 = next).
|
|
989
|
+
- Owner only (403 minecraft_queue_owner_only).
|
|
990
|
+
- Example: await Twinkle.minecraft.moveQueuedBuild({ jobId: 142, to: 0 });
|
|
991
|
+
- async removeQueuedBuild({ jobId }) | scopes: content:write
|
|
992
|
+
- Returns: same as getBuildQueue
|
|
993
|
+
- Take an order out of the queue (stops it if Zero is already building it). Your own orders; moderators also lower ranks'; the owner any.
|
|
994
|
+
- Builders and up with a linked account; the rank rule is the same as stopZero.
|
|
995
|
+
- Example: await Twinkle.minecraft.removeQueuedBuild({ jobId: 142 });
|
|
996
|
+
- async moveCampaignStep({ index, before }) | scopes: content:write
|
|
997
|
+
- Returns: same as getBuildQueue
|
|
998
|
+
- Server owner: move a campaign step that has not started to just before the step at index `before` (null = last). Indexes are the step.index values from getBuildQueue.
|
|
999
|
+
- Owner only.
|
|
1000
|
+
- Example: await Twinkle.minecraft.moveCampaignStep({ index: 17, before: 15 });
|
|
1001
|
+
- async skipCampaignStep({ index, skip = true }) | scopes: content:write
|
|
1002
|
+
- Returns: same as getBuildQueue
|
|
1003
|
+
- Server owner: skip a campaign step, or bring a skipped or failed one back (skip: false).
|
|
1004
|
+
- Owner only. A step Zero is building has to be stopped first.
|
|
1005
|
+
- Example: await Twinkle.minecraft.skipCampaignStep({ index: 18 });
|
|
1006
|
+
- async setCampaignActive({ active }) | scopes: content:write
|
|
1007
|
+
- Returns: same as getBuildQueue
|
|
1008
|
+
- Server owner: pause or resume Zero's campaign (a paused campaign waits; orders still run).
|
|
1009
|
+
- Owner only.
|
|
1010
|
+
- Example: await Twinkle.minecraft.setCampaignActive({ active: false });
|
|
1011
|
+
- async getServerLogs({ days, kinds, query, limit } = {}) | scopes: content:read
|
|
1012
|
+
- Returns: { total, lines: [{ at, kind: 'join'|'leave'|'kick'|'command'|'warn'|'error'|'zero'|'server', level, text }] }
|
|
1013
|
+
- The Minecraft server log for the server owner: joins, kicks, commands, warnings, errors and Zero's lines, newest first, IP addresses removed.
|
|
1014
|
+
- Server owner only, in an app they own; everyone else gets 403 minecraft_roles_forbidden.
|
|
1015
|
+
- days 1-7 (default 1); kinds any of join, leave, kick, command, warn, error, zero, server (default all); query filters by text; limit 1-1000 (default 300). Chat is not included (getChat and getChatHistory have it).
|
|
1016
|
+
- Example: const { lines } = await Twinkle.minecraft.getServerLogs({ days: 2, kinds: ['kick', 'error'] });
|
|
1017
|
+
- async getNews({ limit } = {}) | scopes: content:read
|
|
1018
|
+
- Returns: { posts: [{ id, title, body, author, at }], canPost }
|
|
1019
|
+
- Server news: short posts about what's new on Twinkle Minecraft, newest first (players see the newest on join and all of it with /news in game).
|
|
1020
|
+
- Every viewer reads the news. at is the post time in ms; limit 1-50 (default 20). canPost is true for moderators and the server owner (they can call addNews and deleteNews).
|
|
1021
|
+
- Example: const { posts, canPost } = await Twinkle.minecraft.getNews({ limit: 10 });
|
|
1022
|
+
- async addNews({ title, body }) | scopes: content:write
|
|
1023
|
+
- Returns: { post: { id, title, body, author, at } }
|
|
1024
|
+
- Post server news (moderators and the server owner). Players in game see the headline right away.
|
|
1025
|
+
- title up to 120 characters (required), body up to 2000. Others get 403 minecraft_news_forbidden. The author is the signed-in viewer's Twinkle username.
|
|
1026
|
+
- Example: await Twinkle.minecraft.addNews({ title: 'World 3 reopens', body: 'Come build!' });
|
|
1027
|
+
- async deleteNews({ id }) | scopes: content:write
|
|
1028
|
+
- Returns: { deleted }
|
|
1029
|
+
- Remove a news post (moderators and the server owner).
|
|
1030
|
+
- Others get 403 minecraft_news_forbidden.
|
|
1031
|
+
- Example: await Twinkle.minecraft.deleteNews({ id: post.id });
|
|
848
1032
|
|
|
849
1033
|
### Twinkle.leaderboards
|
|
850
1034
|
- async get({ boardKey = 'default', limit, cursor } = {}) | scopes: none
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -1090,7 +1090,7 @@ app declares its economy in `rewards.json` at the project root (rule ids,
|
|
|
1090
1090
|
titles, XP, Coins, tries, retry share, budgets); quiz rules get their questions
|
|
1091
1091
|
and answer keys from a private question sheet the creator's Lumine uploads with
|
|
1092
1092
|
`lumine rewards sheet <file.json>` (never a project file: published source is
|
|
1093
|
-
readable by every player). **Send for review** freezes the code and proposes
|
|
1093
|
+
readable by every player). **Send for review** (website, or `lumine rewards review` from the workspace) freezes the code and proposes
|
|
1094
1094
|
`rewards.json` merged with the sheet. Approval is Mikey's decision: read the
|
|
1095
1095
|
frozen code, check that the amounts are right and that the app cannot be
|
|
1096
1096
|
farmed, change anything that is wrong, approve. **Approval publishes** (since
|
|
@@ -1218,6 +1218,19 @@ submitted. The website equivalent is the Management panel's "Edit a copy to
|
|
|
1218
1218
|
propose changes" (a private workspace copy owned by the reviewer) followed by
|
|
1219
1219
|
"Offer my copy with these rules".
|
|
1220
1220
|
|
|
1221
|
+
**Try this version (since 2026-09-26, API 3c08af5d / vite 2.2.86).** While an
|
|
1222
|
+
offer is `changes_offered`, the creator and the reviewer (and nobody else) get
|
|
1223
|
+
a **Try this version** button on the chat card, in the creator's reward
|
|
1224
|
+
settings and in the Management approvals panel. It plays the offered
|
|
1225
|
+
`proposalSnapshot` as a normal app preview through
|
|
1226
|
+
`GET /build/preview/build/:buildId/reward-proposal/:reviewId`, with a token of
|
|
1227
|
+
its own scope (`reward-proposal:preview`, bound to viewer, build, review and
|
|
1228
|
+
revision; re-checked on every file request). XP and Coins inside it are
|
|
1229
|
+
simulated against the offered rules and never pay. The app's own SDK data
|
|
1230
|
+
calls (privateDb, sharedDb…) still reach the build's real data as the viewer,
|
|
1231
|
+
like a draft preview, and the modal says so. A newer revision or an answered
|
|
1232
|
+
offer closes the preview (409).
|
|
1233
|
+
|
|
1221
1234
|
Each offer has a server-owned revision. Changing the files, rules or note
|
|
1222
1235
|
creates a new revision; a creator looking at an older comparison or decline
|
|
1223
1236
|
confirmation cannot answer the replacement offer. The creator sees its reward
|
|
@@ -3290,6 +3303,16 @@ acting, `off` disables):
|
|
|
3290
3303
|
Separately from JEV, a run that reaches its round cap with nothing saved while
|
|
3291
3304
|
Energy remains gets one extra apply-only round (`applyOnlyRound`).
|
|
3292
3305
|
|
|
3306
|
+
Before any JEV question (since 2026-09-26, API ea257c0a): when the remaining
|
|
3307
|
+
Energy, after the model's hand-off reserve, cannot cover two typical rounds
|
|
3308
|
+
(one read and one edit, `LUMINE_MIN_ROUNDS_FOR_PROJECT_EDIT`) on the chosen
|
|
3309
|
+
model but can on a lighter one, the run does not start and shows the
|
|
3310
|
+
switch-model card; nothing is spent. Lineage metadata records
|
|
3311
|
+
`tooTightForAnEdit`, `roundsOnThisModel` and `lighterModelRounds`. The
|
|
3312
|
+
composer checks the same arithmetic up front (model options carry
|
|
3313
|
+
`handoffReserveEnergyUnits`): it offers a one-tap switch, and it disables
|
|
3314
|
+
sending with an explanation when no model can afford even one step.
|
|
3315
|
+
|
|
3293
3316
|
Every UTC day in `lumine admin energy-budget --json` now carries `pacing`.
|
|
3294
3317
|
Report in **"Insights for Mikey"** in every full run, for the last completed
|
|
3295
3318
|
day and the in-progress day while the feature is new:
|