@stage5/lumine 0.2.15 → 0.2.17

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 CHANGED
@@ -10,6 +10,7 @@ npx @stage5/lumine@latest new --title "Daily Reflection App" --description "Priv
10
10
  npx @stage5/lumine@latest rename "My New Build Title"
11
11
  npx @stage5/lumine@latest rename "New Title" --target 123
12
12
  npx @stage5/lumine@latest projects
13
+ npx @stage5/lumine@latest branches 884
13
14
  npx @stage5/lumine@latest explore --sort forks
14
15
  npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
15
16
  npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
@@ -35,6 +36,10 @@ pulling the owner's main project creates or reuses your contribution branch and
35
36
  checks out that branch locally. Saves go to your branch, so the project owner
36
37
  can merge or replace main from Twinkle.
37
38
 
39
+ Use `lumine branches <build-url-or-id>` to list the contribution branches you
40
+ can review, including each contributor, branch number, status, and URL. Then use
41
+ `lumine diff <branch-url>` to inspect one branch.
42
+
38
43
  Use `lumine explore` to list public open-source Build apps that can be used as
39
44
  examples or starting points. It supports `--search` and `--sort forks`,
40
45
  `--sort popular`, or `--sort recent`. Use `lumine reference <build-url-or-id>`
package/lib/api.js CHANGED
@@ -30,6 +30,21 @@ export async function listOpenSourceBuilds({ options, auth }) {
30
30
  return Array.isArray(result.builds) ? result.builds : [];
31
31
  }
32
32
 
33
+ export async function listContributionBranches({
34
+ options,
35
+ auth,
36
+ buildId,
37
+ limit,
38
+ }) {
39
+ const url = new URL(`${options.apiUrl}/build/${buildId}/contributions`);
40
+ url.searchParams.set("limit", String(limit));
41
+ return await requestJson({
42
+ url: url.toString(),
43
+ authToken: auth.token,
44
+ timeoutMs: options.timeoutMs,
45
+ });
46
+ }
47
+
33
48
  export async function loadBuildMetadata({ options, auth, buildId }) {
34
49
  const result = await loadBuildFiles({
35
50
  options,
package/lib/commands.js CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  loadBuildVersionFiles,
32
32
  loadBuildVersions,
33
33
  loadContributionDiff,
34
+ listContributionBranches,
34
35
  loadLumineCliVersionInfo,
35
36
  loadOpenSourceBuildFiles,
36
37
  maybeCheckForLumineCliUpdate,
@@ -76,6 +77,7 @@ import {
76
77
  collectProjectLimitFindings,
77
78
  findLocalProjectMetadata,
78
79
  isIndexHtmlPath,
80
+ isReadOnlyProjectMetadata,
79
81
  isReadOnlyReferenceMetadata,
80
82
  removeLocalProjectFilesNotIn,
81
83
  stashLocalProjectFilesBeforePull,
@@ -130,6 +132,10 @@ export async function main() {
130
132
  await projects(options);
131
133
  return;
132
134
  }
135
+ if (options.command === "branches") {
136
+ await branches(options);
137
+ return;
138
+ }
133
139
  if (options.command === "explore") {
134
140
  await explore(options);
135
141
  return;
@@ -263,7 +269,8 @@ export async function renameBuild(options) {
263
269
  await saveSelectedBuild({ options, auth, build: updatedBuild });
264
270
  if (
265
271
  localProject?.rootDir &&
266
- Number(localProject.metadata?.buildId || 0) === buildId
272
+ Number(localProject.metadata?.buildId || 0) === buildId &&
273
+ !isReadOnlyProjectMetadata(localProject.metadata)
267
274
  ) {
268
275
  await writeProjectMetadata({
269
276
  dir: localProject.rootDir,
@@ -303,6 +310,33 @@ export async function projects(options) {
303
310
  printBuildList(builds);
304
311
  }
305
312
 
313
+ export async function branches(options) {
314
+ const auth = await resolveAuth(options);
315
+ const requestedBuildId = await resolveRequiredBuildIdOrSelected(
316
+ options,
317
+ auth,
318
+ );
319
+ const requestedBuild = await loadBuildMetadata({
320
+ options,
321
+ auth,
322
+ buildId: requestedBuildId,
323
+ });
324
+ const rootBuildId =
325
+ Number(requestedBuild.contributionRootBuildId || 0) ||
326
+ Number(requestedBuild.id || 0);
327
+ const rootBuild =
328
+ rootBuildId === Number(requestedBuild.id || 0)
329
+ ? requestedBuild
330
+ : await loadBuildMetadata({ options, auth, buildId: rootBuildId });
331
+ const result = await listContributionBranches({
332
+ options,
333
+ auth,
334
+ buildId: rootBuildId,
335
+ limit: options.limit,
336
+ });
337
+ printContributionBranches({ result, rootBuild, options });
338
+ }
339
+
306
340
  export async function explore(options) {
307
341
  const auth = await resolveAuth(options);
308
342
  const builds = await listOpenSourceBuilds({ options, auth });
@@ -1618,6 +1652,34 @@ export function printContributionDiff({ result, build }) {
1618
1652
  }
1619
1653
  }
1620
1654
 
1655
+ export function printContributionBranches({ result, rootBuild, options }) {
1656
+ const contributions = Array.isArray(result?.contributions)
1657
+ ? result.contributions
1658
+ : [];
1659
+ console.log(`Branches for ${formatBuildTitle(rootBuild)}:`);
1660
+ if (!contributions.length) {
1661
+ console.log("No contribution branches.");
1662
+ return;
1663
+ }
1664
+ for (const [index, contribution] of contributions.entries()) {
1665
+ const branchNumber = Number(contribution.contributionBranchNumber || 0);
1666
+ const branchLabel = branchNumber
1667
+ ? `branch ${branchNumber}`
1668
+ : `branch #${contribution.id}`;
1669
+ const title = String(contribution.title || "Untitled branch").trim();
1670
+ const contributor = String(contribution.username || "unknown").trim();
1671
+ const status = String(contribution.contributionStatus || "draft").trim();
1672
+ console.log(
1673
+ `${index + 1}. ${title} - ${contributor} - ${branchLabel} (${status})`,
1674
+ );
1675
+ if (branchNumber) {
1676
+ console.log(
1677
+ ` ${options.siteUrl}/build/${Number(rootBuild.id)}/${branchNumber}`,
1678
+ );
1679
+ }
1680
+ }
1681
+ }
1682
+
1621
1683
  export function printUpdateFromMainResult({ result, build, dir, mergedFiles }) {
1622
1684
  const branchNumber = Number(build?.contributionBranchNumber || 0) || 0;
1623
1685
  const branchLabel = branchNumber
@@ -1981,6 +2043,7 @@ export function printHelp() {
1981
2043
  lumine new [title]
1982
2044
  lumine rename [title] [--target <twinkle-build-url-or-id>]
1983
2045
  lumine projects
2046
+ lumine branches [twinkle-build-url-or-id] [--limit <n>]
1984
2047
  lumine explore [search terms]
1985
2048
  lumine select [twinkle-build-url]
1986
2049
  lumine pull [twinkle-build-url]
@@ -2016,6 +2079,7 @@ Examples:
2016
2079
  npx @stage5/lumine@latest rename "My New Build Title"
2017
2080
  npx @stage5/lumine@latest rename "New Title" --target 123
2018
2081
  npx @stage5/lumine@latest new --title "Daily Reflection App" --description "Private journal with streaks"
2082
+ npx @stage5/lumine@latest branches 884
2019
2083
  npx @stage5/lumine@latest explore --sort forks
2020
2084
  npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
2021
2085
  npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
package/lib/constants.js CHANGED
@@ -118,7 +118,7 @@ Use these current source-of-truth rules:
118
118
  - Use Twinkle.sharedDb for shared multi-user JSON data.
119
119
  - Use Twinkle.aiCards.list/search/get for existing public AI Card words, exampleText sentence material, and word levels.
120
120
  - Use Twinkle.aiStories.list/search/get for existing AI Story passage text, story media, and questions.
121
- - Use Twinkle.ai.chat with history entries shaped as { role, content }, not { text }.
121
+ - Use Twinkle.ai.chat with history entries shaped as { role, content }, not { text }. Live web search is enabled by default; pass webSearch: false to disable it for the app.
122
122
  - Use Twinkle.preview for canvas, WebGL, Three.js, fullscreen, and game layout.
123
123
  - Prefer existing documented Twinkle.* methods over guessing names from old code.
124
124
  `;
@@ -308,6 +308,7 @@ export const AGENT_INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md"];
308
308
  export const MAIN_CHECKOUT_READONLY_COMMANDS = new Set([
309
309
  "pull",
310
310
  "check",
311
+ "branches",
311
312
  "diff",
312
313
  "sdk",
313
314
  "select",
@@ -326,6 +327,7 @@ export const COMMANDS = new Set([
326
327
  "new",
327
328
  "rename",
328
329
  "projects",
330
+ "branches",
329
331
  "explore",
330
332
  "select",
331
333
  "pull",
package/lib/workspace.js CHANGED
@@ -779,14 +779,15 @@ export function resolveProjectDirForSave({ options, localProject }) {
779
779
  export function assertLocalProjectCanBeSaved(localProject) {
780
780
  const metadata = localProject?.metadata;
781
781
  if (!metadata) return;
782
- if (metadata.mainCheckout === true) {
782
+ const readOnlyKind = readOnlyProjectMetadataKind(metadata);
783
+ if (readOnlyKind === "main") {
783
784
  const rootBuildId =
784
785
  Number(metadata.buildId || 0) || Number(metadata.build?.id || 0) || 0;
785
786
  throw new Error(
786
787
  `This is a read-only checkout of main${rootBuildId ? ` for Build ${rootBuildId}` : ""}. Make edits in your branch workspace (\`lumine pull${rootBuildId ? ` ${rootBuildId}` : ""}\`), and run \`lumine update-from-main\` there to bring main's changes into it.`,
787
788
  );
788
789
  }
789
- if (metadata.versionCheckout === true) {
790
+ if (readOnlyKind === "version") {
790
791
  const checkoutBuildId =
791
792
  Number(metadata.buildId || 0) || Number(metadata.build?.id || 0) || 0;
792
793
  const checkoutVersion = Number(metadata.checkoutVersion || 0) || 0;
@@ -794,7 +795,7 @@ export function assertLocalProjectCanBeSaved(localProject) {
794
795
  `This is a read-only checkout of a previous save${checkoutVersion ? ` (v${checkoutVersion})` : ""}${checkoutBuildId ? ` for Build ${checkoutBuildId}` : ""}. To bring this save back, run \`lumine restore${checkoutVersion ? ` ${checkoutVersion}` : " <n>"}\` from the editable workspace, then \`lumine save\`.`,
795
796
  );
796
797
  }
797
- if (isReadOnlyReferenceMetadata(metadata)) {
798
+ if (readOnlyKind === "reference") {
798
799
  const sourceBuildId =
799
800
  Number(metadata.reference?.sourceBuildId || 0) ||
800
801
  Number(metadata.buildId || 0) ||
@@ -804,7 +805,7 @@ export function assertLocalProjectCanBeSaved(localProject) {
804
805
  `This is a read-only Lumine reference${sourceBuildId ? ` for Build ${sourceBuildId}` : ""}. Run \`lumine fork${sourceBuildId ? ` ${sourceBuildId}` : ""}\` to create an editable workspace.`,
805
806
  );
806
807
  }
807
- if (metadata.build?.canWrite === false) {
808
+ if (readOnlyKind === "server") {
808
809
  throw new Error(
809
810
  "This Lumine checkout is read-only for the current CLI login. Pull or diff it for review; project-owner branch edits must go through merge or replace-main.",
810
811
  );
@@ -819,6 +820,18 @@ export function isReadOnlyReferenceMetadata(metadata) {
819
820
  );
820
821
  }
821
822
 
823
+ export function isReadOnlyProjectMetadata(metadata) {
824
+ return readOnlyProjectMetadataKind(metadata) !== null;
825
+ }
826
+
827
+ export function readOnlyProjectMetadataKind(metadata) {
828
+ if (metadata?.mainCheckout === true) return "main";
829
+ if (metadata?.versionCheckout === true) return "version";
830
+ if (isReadOnlyReferenceMetadata(metadata)) return "reference";
831
+ if (metadata?.build?.canWrite === false) return "server";
832
+ return null;
833
+ }
834
+
822
835
  export function resolveLocalProjectFilePath({ rootDir, projectPath }) {
823
836
  const relativePath = projectPathToRelativePath(projectPath);
824
837
  const root = path.resolve(rootDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,8 +1,8 @@
1
1
  # Build SDK Index
2
2
 
3
- Version: 1.27.0
4
- Updated: 2026-07-21
5
- Generated: 2026-07-23T05:57:27.486Z
3
+ Version: 1.29.0
4
+ Updated: 2026-07-31
5
+ Generated: 2026-07-31T05:39:44.540Z
6
6
 
7
7
  ## Notes
8
8
  - This SDK is injected into Build iframes via the Build preview/runtime.
@@ -12,6 +12,7 @@ Generated: 2026-07-23T05:57:27.486Z
12
12
  - Match storage to update frequency: privateDb and sharedDb are for LOW-frequency durable state that changes on a user action. NEVER write per-frame/per-tick state to them (camera or cursor position, animation, live movement, presence, autosave every frame/tick). Keep live state in client memory, broadcast realtime/presence via Twinkle.world, and flush only occasional durable snapshots (on an interval or on exit, never per frame). The server enforces per-key write rate limits and returns 429 on excess; never retry-loop a 429.
13
13
  - Use Twinkle.userDb only for advanced private SQLite needs such as tables, indexes, many rows, filtered queries, or aggregates.
14
14
  - Use Twinkle.leaderboards for public Build scoreboards. Signed-in viewers are ranked by Twinkle username; guests can submit with a display name.
15
+ - Use Twinkle.news to read the globally shared Twinkle Daily edition or let a signed-in viewer queue today's edition. The server permits only one ready edition per Twinkle day and generation does not spend the requesting viewer's AI Energy.
15
16
  - Use Twinkle.sharedDb for LOW-frequency durable shared multi-user state such as guestbooks, votes, room settings, submitted records, and append-only run history. It is NOT for high-frequency or per-frame/per-tick writes; keep live/realtime state in Twinkle.world or client memory. The server rate-limits writes and returns 429.
16
17
  - Use Twinkle.subjects.search for in-app subject pickers. Twinkle.mount remains an optional host-provided preselection/context shortcut, not a data API.
17
18
  - Use Twinkle.aiCards for read-only existing public AI Card words and example texts, including word levels for typing games.
@@ -21,6 +22,7 @@ Generated: 2026-07-23T05:57:27.486Z
21
22
  - Use Twinkle.world for realtime multiplayer rooms, avatar presence, movement, emotes, and lightweight actions; world sessions are disposable and durable MMO state belongs in sharedDb/privateDb.
22
23
  - Use Twinkle.characters.chat for real Zero/Ciel NPC dialogue with shared room context and AI Energy-aware thinking modes.
23
24
  - Twinkle.ai.chat history entries must use { role, content }; map local message.text fields to content before passing history.
25
+ - Live web search is enabled by default for Twinkle.ai.chat and for Medium/High Twinkle.ai.generateObject and Twinkle.characters.chat requests. App authors can pass webSearch: false to disable it for their app. Search uses the provider's live web-search tool and is included in AI Energy usage; structured and character Lite Mode remains tool-free.
24
26
  - Interface text must not be selectable on touch devices: apply user-select: none plus -webkit-user-select: none and -webkit-touch-callout: none to interface text (HUD, buttons, labels, menus, scores, game controls) so mobile long-press does not highlight UI. Keep text inputs and genuinely user-copyable content selectable.
25
27
  - Build app tab mute is enforced by the host runtime automatically for standard media elements and Web Audio connections to AudioContext.destination. Apps with custom audio engines can also observe Twinkle.onAudioMuteChange and check Twinkle.isAudioMuted.
26
28
 
@@ -281,31 +283,34 @@ console.log(analysis.bestMove, analysis.evaluation, analysis.mate);
281
283
  - async listPrompts() | scopes: none
282
284
  - Returns: Array<{ id, title, description }>
283
285
  - Legacy helper. Twinkle.ai.chat does not require promptId for default runtime text generation.
284
- - async chat({ promptId, message, history, systemPrompt, requestId, onText, onStatus } = {}) | scopes: none
285
- - Returns: { text, response, model, aiUsagePolicy }
286
- - Generate text with the default Lumine text model, optionally streaming text updates through onText.
286
+ - async chat({ promptId, message, history, systemPrompt, webSearch, requestId, onText, onStatus } = {}) | scopes: none
287
+ - Returns: { text, response, model, webSearch, aiUsagePolicy }
288
+ - Generate text with the default Lumine text model, optionally using live web search and streaming text updates through onText.
287
289
  - Signed-in viewers only.
288
290
  - Uses Grok 4.5 by default.
289
291
  - Each successful text generation consumes AI Energy from the signed-in viewer.
290
292
  - history must be an array of { role: 'user' | 'assistant', content: string }. Twinkle.ai.chat does not read a text field.
291
293
  - The server keeps the latest 12 valid history entries.
292
294
  - Pass systemPrompt to define the app AI's personality, tone, role, or response rules.
295
+ - Live web search is enabled by default, and the model decides whether searching is useful. Pass webSearch: false to disable it for the app.
296
+ - When streaming, onStatus may receive searching_web while the provider is searching.
293
297
  - Pass onText to receive streaming accumulated text before the final result resolves.
294
- - AI Energy is recorded after provider success when final token usage is available.
298
+ - AI Energy is recorded after provider success when final token and web-search tool usage are available.
295
299
  - Use this for in-app AI replies instead of creating or fetching app-local endpoints such as /api/chat.
296
300
  - Example: const chatHistory = conversation.slice(-12).map((entry) => ({ role: entry.role === 'assistant' ? 'assistant' : 'user', content: entry.text }));
297
301
  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') });
298
- - async generateObject({ prompt, expectedStructure, thinkingMode, mode, instructions, systemPrompt } = {}) | scopes: none
299
- - Returns: { object, result, model, provider, thinkingMode, requestedThinkingMode, aiUsagePolicy }
300
- - Generate a validated structured JSON object for app decisions, routing, grading, and game-state logic.
302
+ - async generateObject({ prompt, expectedStructure, thinkingMode, mode, instructions, systemPrompt, webSearch } = {}) | scopes: none
303
+ - Returns: { object, result, model, provider, thinkingMode, requestedThinkingMode, webSearch, aiUsagePolicy }
304
+ - Generate a validated structured JSON object for app decisions, routing, grading, and game-state logic, optionally using live web search.
301
305
  - Signed-in viewers only.
302
306
  - Use this instead of asking Twinkle.ai.chat to return JSON.
303
307
  - expectedStructure must be a JSON object that describes the exact returned object shape.
304
308
  - mode is accepted as an alias for thinkingMode, and mid is accepted as an alias for medium.
305
309
  - thinkingMode low uses GPT-5.6 Luna and records free low-energy usage.
306
- - thinkingMode medium uses GPT-5.6 Luna and normal AI Energy while AI Energy remains.
307
- - thinkingMode high uses Grok 4.5 with high reasoning and high AI Energy while AI Energy remains.
310
+ - thinkingMode medium uses Grok 4.5 with medium reasoning and normal AI Energy while AI Energy remains.
311
+ - thinkingMode high uses GPT-5.6 Sol with high reasoning and high AI Energy while AI Energy remains.
308
312
  - If medium or high is requested after AI Energy is empty, the server falls back to low and returns thinkingMode: low.
313
+ - Live web search is enabled by default in Medium and High modes. Pass webSearch: false to disable it for the app. Omitted/default search turns off automatically when the request falls back to tool-free Lite Mode; explicitly forcing webSearch: true in Lite Mode returns an error.
309
314
  - The SDK validates shape and retries malformed JSON, but app code should still validate business-specific enum values.
310
315
  - Example: const { object } = await Twinkle.ai.generateObject({ thinkingMode: 'medium', prompt: 'Classify the player intent from: ' + playerText, expectedStructure: { action: 'string', targetCharacter: 'string', confidence: 0, shouldAskFollowUp: false } });
311
316
  - onChatStatus(listener) | scopes: none
@@ -337,9 +342,9 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
337
342
  - Example: const unsubscribe = Twinkle.ai.onImageGenerationStatus((status) => console.log(status.stage));
338
343
 
339
344
  ### Twinkle.characters
340
- - async chat({ character, thinkingMode, message, history, roomContext, scene, systemPrompt, instructions, includeWebsiteContext, requestId, onText, onStatus } = {}) | scopes: none
341
- - Returns: { text, response, character, aiUsername, thinkingMode, requestedThinkingMode, includeWebsiteContext, model, provider, aiUsagePolicy }
342
- - Talk to Zero or Ciel from a Build app, either as a final-response call or streaming RPG-style dialogue text with onText/onStatus.
345
+ - async chat({ character, thinkingMode, message, history, roomContext, scene, systemPrompt, instructions, includeWebsiteContext, webSearch, requestId, onText, onStatus } = {}) | scopes: none
346
+ - Returns: { text, response, character, aiUsername, thinkingMode, requestedThinkingMode, includeWebsiteContext, webSearch, model, provider, aiUsagePolicy }
347
+ - Talk to Zero or Ciel from a Build app, optionally using live web search and streaming RPG-style dialogue text with onText/onStatus.
343
348
  - Signed-in viewers only.
344
349
  - character must be zero or ciel.
345
350
  - Recommended history shape is { role: 'user' | 'assistant', content: string, speaker?: string }; content is the canonical text field.
@@ -348,10 +353,13 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
348
353
  - Pass onText/onStatus for streaming dialogue. Omit callbacks for non-streaming dialogue where the promise resolves with the final response.
349
354
  - thinkingMode low uses Lite Mode: Zero uses Grok 4.5 with low reasoning and Ciel uses Claude Haiku 4.5; usage is recorded as free low-energy usage.
350
355
  - thinkingMode medium uses normal AI Energy: Zero uses Grok 4.5 with medium reasoning and Ciel uses Claude Sonnet 5 while AI Energy remains.
351
- - thinkingMode high uses high AI Energy: Zero uses Grok 4.5 with high reasoning and Ciel uses Claude Opus 4.8 with extended thinking while AI Energy remains.
356
+ - thinkingMode high uses high AI Energy: Zero uses Grok 4.5 with high reasoning and Ciel uses Claude Opus 5 with extended thinking while AI Energy remains.
352
357
  - If medium or high is requested after AI Energy is empty, the server falls back to low and returns thinkingMode: low.
353
358
  - Pass roomContext as a short shared scene transcript so Zero and Ciel can know what happened in the same room.
354
359
  - includeWebsiteContext defaults to true. Set includeWebsiteContext: false for in-world NPC dialogue that should only use Zero/Ciel's basic character identity plus your scene/instructions.
360
+ - Live web search is enabled by default in Medium and High modes. Pass webSearch: false to disable it for the app. Omitted/default search turns off automatically when the request falls back to tool-free Lite Mode; explicitly forcing webSearch: true in Lite Mode returns an error.
361
+ - includeWebsiteContext controls Twinkle persona context and is unrelated to webSearch.
362
+ - When streaming, onStatus may receive searching_web while the provider is searching.
355
363
  - Use this for real Zero/Ciel NPCs instead of pretending with Twinkle.ai.chat systemPrompt.
356
364
  - Example: const dialogueHistory = recentTurns.slice(-16).map((entry) => ({ role: entry.role === 'assistant' ? 'assistant' : 'user', content: entry.text, speaker: entry.speaker }));
357
365
  const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode: thinkHard ? 'high' : 'medium', message: playerText, history: dialogueHistory, roomContext, scene: { location: 'classroom', nearbyCharacters: ['zero', 'ciel'] }, includeWebsiteContext: false, onText: (text) => renderDialogue(text) });
@@ -519,6 +527,22 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
519
527
  - Atomic step 3: fetches likes/replies aggregates for provided IDs.
520
528
  - Accepts either an array of IDs or an object like { ids: [...] }.
521
529
 
530
+ ### Twinkle.news
531
+ - async getCurrentEdition() | scopes: none
532
+ - Returns: { dayIndex, nextEditionAt, generationStatus, edition: { id, dayIndex, status, coverageStartedAt, coverageEndedAt, sourceEventCount, edition, model, provider, generatedAt } | null, pendingEdition }
533
+ - Read today's shared Twinkle newspaper, or the latest ready edition while today's is being generated.
534
+ - Works for signed-in viewers and public-build guests.
535
+ - generationStatus is available, pending, generating, ready, or failed.
536
+ - While today's edition is pending, edition remains the latest ready shared edition and pendingEdition describes today's queued work.
537
+ - Poll gently while generation is pending; once every 5-10 seconds is sufficient.
538
+ - async generateCurrentEdition() | scopes: none
539
+ - Returns: { dayIndex, nextEditionAt, generationStatus, edition, pendingEdition }
540
+ - Atomically queue the current Twinkle day's globally shared edition.
541
+ - Requires a signed-in viewer.
542
+ - The first request for a Twinkle day creates the canonical pending edition; concurrent and later requests return that same server state.
543
+ - Generation is platform-sponsored and does not consume the requesting viewer's AI Energy.
544
+ - A failed attempt may be queued again on the same day. A ready edition is immutable until the next Twinkle day.
545
+
522
546
  ### Twinkle.leaderboards
523
547
  - async get({ boardKey = 'default', limit, cursor } = {}) | scopes: none
524
548
  - Returns: { entries: [{ rank, id, buildId, boardKey, viewerKind, userId, displayName, score, meta, achievedAt, createdAt, updatedAt }], scores, cursor, hasMore, personalBest: { id, buildId, boardKey, viewerKind, userId, displayName, score, meta, achievedAt, createdAt, updatedAt } | null }