@stage5/lumine 0.1.0 → 0.1.1

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
@@ -28,11 +28,12 @@ unpublished changes. For projects you own, run `lumine launch` to publish the
28
28
  saved changes, or `lumine save --publish` to save and publish in one step.
29
29
 
30
30
  Pulled workspaces include `AGENTS.md` and `CLAUDE.md` guides for local coding
31
- agents, plus `.twinkle/lumine-project.json` metadata that tells agents whether
32
- the checkout is writable, publishable, or a contribution branch. These guide
33
- files are not uploaded by `lumine save`. Build apps run in sandboxed iframes
34
- without native form submission, so use JavaScript-handled inputs and buttons
35
- instead of `<form>` elements.
31
+ agents, `TWINKLE_BUILD_SDK.md` with the current Build SDK reference, plus
32
+ `.twinkle/lumine-project.json` metadata that tells agents whether the checkout
33
+ is writable, publishable, or a contribution branch. These guide files are not
34
+ uploaded by `lumine save`. Build apps run in sandboxed iframes without native
35
+ form submission, so use JavaScript-handled inputs and buttons instead of
36
+ `<form>` elements.
36
37
 
37
38
  After pulling a project, run an agent from the pulled folder:
38
39
 
package/bin/lumine.js CHANGED
@@ -19,9 +19,36 @@ const PROJECT_METADATA_DIR = ".twinkle";
19
19
  const PROJECT_METADATA_FILE = "lumine-project.json";
20
20
  const DEFAULT_SAVE_SUMMARY = "Saved from Lumine CLI.";
21
21
  const EXCLUDED_UPLOAD_DIRS = new Set([".git", ".twinkle", "node_modules"]);
22
- const EXCLUDED_UPLOAD_FILES = new Set([".DS_Store", "AGENTS.md", "CLAUDE.md"]);
22
+ const SDK_REFERENCE_FILE = "TWINKLE_BUILD_SDK.md";
23
+ const EXCLUDED_UPLOAD_FILES = new Set([
24
+ ".DS_Store",
25
+ "AGENTS.md",
26
+ "CLAUDE.md",
27
+ SDK_REFERENCE_FILE,
28
+ ]);
23
29
  const LUMINE_AGENT_INSTRUCTIONS_MARKER =
24
30
  "<!-- Lumine CLI Agent Instructions -->";
31
+ const LUMINE_SDK_REFERENCE_MARKER = "<!-- Lumine CLI SDK Reference -->";
32
+ const BUNDLED_SDK_REFERENCE_URL = new URL(
33
+ "../sdk/BUILD_SDK_INDEX.md",
34
+ import.meta.url,
35
+ );
36
+ const SDK_REFERENCE_FALLBACK = `${LUMINE_SDK_REFERENCE_MARKER}
37
+ # Twinkle Build SDK Reference
38
+
39
+ The bundled SDK reference could not be loaded from this Lumine CLI package.
40
+
41
+ Use these current source-of-truth rules:
42
+
43
+ - Read .twinkle/lumine-project.json before editing.
44
+ - Use Twinkle.capabilities.get() or Twinkle.capabilities.can(actionName) before relying on gated SDK calls.
45
+ - Use Twinkle.privateDb for simple private per-user preferences, drafts, settings, and small JSON state.
46
+ - Use Twinkle.userDb only for advanced private SQLite tables, indexes, many rows, filtered queries, or aggregates.
47
+ - Use Twinkle.sharedDb for shared multi-user JSON data.
48
+ - Use Twinkle.ai.chat with history entries shaped as { role, content }, not { text }.
49
+ - Use Twinkle.preview for canvas, WebGL, Three.js, fullscreen, and game layout.
50
+ - Prefer existing documented Twinkle.* methods over guessing names from old code.
51
+ `;
25
52
  const LUMINE_AGENT_INSTRUCTIONS = `${LUMINE_AGENT_INSTRUCTIONS_MARKER}
26
53
  # Lumine Project Agent Guide
27
54
 
@@ -32,6 +59,7 @@ Lumine CLI as the source of truth for saving this workspace back to Twinkle.
32
59
 
33
60
  - Read .twinkle/lumine-project.json before changing files.
34
61
  - Treat build.canWrite, build.canPublish, and build.contributionRootBuildId as authoritative.
62
+ - Read ${SDK_REFERENCE_FILE} before adding, removing, or changing any Twinkle.* SDK calls.
35
63
  - If build.canWrite is false, do not save changes.
36
64
  - If build.canPublish is false or contributionRootBuildId is set, this checkout is a contribution branch. Save only to this branch and do not run lumine launch or lumine save --publish.
37
65
  - Do not edit another local checkout to bypass branch rules.
@@ -55,11 +83,12 @@ lumine save --summary "Describe the change"
55
83
  - Build apps run in sandboxed iframes without allow-forms. Do not use <form> elements, native form submission, requestSubmit(), or browser form navigation. Build input flows with JavaScript-handled inputs and buttons instead.
56
84
  - For canvas, WebGL, Three.js, fullscreen, or game builds, use Twinkle.preview for layout. Do not size roots from 100vh, 100vw, 100dvh, 100dvw, window.innerWidth, window.innerHeight, visualViewport, or document viewport dimensions.
57
85
  - For Three.js, use import * as THREE from '/build/vendor/three/0.160.0/three.module.min.js';.
86
+ - Do not invent or guess Twinkle.* SDK method names. Use ${SDK_REFERENCE_FILE} as the local SDK reference and prefer Twinkle.capabilities checks for gated features.
58
87
 
59
88
  ## Completion Report
60
89
 
61
- Report the changed files, the lumine save result, the build or branch id, and
62
- whether the result is published or unpublished changes.
90
+ Report the changed files, any SDK methods used, the lumine save result, the
91
+ build or branch id, and whether the result is published or unpublished changes.
63
92
  `;
64
93
  const AGENT_INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md"];
65
94
  const COMMANDS = new Set([
@@ -709,6 +738,7 @@ async function pullBuildFiles({ options, auth, buildId }) {
709
738
  const dir = path.resolve(options.dir || defaultWorkspaceDir(build));
710
739
  await writeProjectFiles({ dir, files });
711
740
  await writeAgentInstructions({ dir });
741
+ await writeSdkReference({ dir });
712
742
  await writeProjectMetadata({
713
743
  dir,
714
744
  options,
@@ -739,6 +769,30 @@ async function writeAgentInstructions({ dir }) {
739
769
  }
740
770
  }
741
771
 
772
+ async function writeSdkReference({ dir }) {
773
+ const filePath = path.join(dir, SDK_REFERENCE_FILE);
774
+ try {
775
+ const existing = await fs.readFile(filePath, "utf8");
776
+ if (!existing.includes(LUMINE_SDK_REFERENCE_MARKER)) {
777
+ return;
778
+ }
779
+ } catch (error) {
780
+ if (error.code !== "ENOENT") throw error;
781
+ }
782
+ await fs.writeFile(filePath, await loadSdkReference(), "utf8");
783
+ }
784
+
785
+ async function loadSdkReference() {
786
+ try {
787
+ const rawReference = await fs.readFile(BUNDLED_SDK_REFERENCE_URL, "utf8");
788
+ const reference = rawReference.trim();
789
+ if (reference) return `${LUMINE_SDK_REFERENCE_MARKER}\n${reference}\n`;
790
+ } catch {
791
+ // Fall through to the compact reference so pulled workspaces still guide agents.
792
+ }
793
+ return SDK_REFERENCE_FALLBACK;
794
+ }
795
+
742
796
  async function collectProjectFiles(dir) {
743
797
  const root = path.resolve(dir);
744
798
  const files = [];
@@ -1023,6 +1077,7 @@ function printPullResult(result) {
1023
1077
  `Pulled ${result.fileCount} file${result.fileCount === 1 ? "" : "s"} to ${result.dir}`,
1024
1078
  );
1025
1079
  console.log(`Entry: ${entryPath}`);
1080
+ console.log(`SDK reference: ${SDK_REFERENCE_FILE}`);
1026
1081
  console.log(`Next: cd ${shellQuote(result.dir)}`);
1027
1082
  if (build.canWrite === false) {
1028
1083
  console.log("This checkout is read-only for the current CLI login.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "bin",
11
+ "sdk",
11
12
  "README.md"
12
13
  ],
13
14
  "publishConfig": {
@@ -0,0 +1,454 @@
1
+ # Build SDK Index
2
+
3
+ Version: 1.23.0
4
+ Updated: 2026-05-24
5
+ Generated: 2026-05-25T00:43:05.370Z
6
+
7
+ ## Notes
8
+ - This SDK is injected into Build iframes via the Build preview/runtime.
9
+ - Widgets call SDK methods; the parent proxies to the API.
10
+ - Data API methods require scoped tokens handled by the parent; some namespaces include write methods.
11
+ - Use Twinkle.privateDb as the default private per-user persistence layer for preferences, drafts, settings, and small JSON state.
12
+ - Use Twinkle.userDb only for advanced private SQLite needs such as tables, indexes, many rows, filtered queries, or aggregates.
13
+ - Use Twinkle.leaderboards for public Build scoreboards. Signed-in viewers are ranked by Twinkle username; guests can submit with a display name.
14
+ - Use Twinkle.sharedDb for custom shared multi-user structured data, guestbooks, votes, and append-only run history.
15
+ - Use Twinkle.subjects.search for in-app subject pickers. Twinkle.mount remains an optional host-provided preselection/context shortcut, not a data API.
16
+ - Use Twinkle.aiStories for read-only existing AI Story galleries, readers, quizzes, and remix tools.
17
+ - Use Twinkle.grammarbles for public Grammarbles question-bank trainer apps and optional signed-in viewer attempt-history filtering.
18
+ - Use Twinkle.chess for chess engine play and analysis; app code still owns chess rules, legal moves, board state, and UI.
19
+ - Use Twinkle.world for realtime multiplayer rooms, avatar presence, movement, emotes, and lightweight actions; keep durable MMO state in sharedDb/privateDb.
20
+ - Use Twinkle.characters.chat for real Zero/Ciel NPC dialogue with shared room context and AI Energy-aware thinking modes.
21
+ - Twinkle.ai.chat history entries must use { role, content }; map local message.text fields to content before passing history.
22
+
23
+ ## Token Scopes
24
+ files:read, user:read, users:read, dailyReflections:read, content:read, sharedDb:read, sharedDb:write, privateDb:read, privateDb:write, files:write, chat:read, chat:write, notifications:read, notifications:write, reminders:read, reminders:write
25
+
26
+ ## Namespaces
27
+
28
+ ### Twinkle.capabilities
29
+ - async get() | scopes: none
30
+ - async can(actionName) | scopes: none
31
+ - async listActions() | scopes: none
32
+ - async refresh() | scopes: none
33
+
34
+ ### Twinkle.viewer
35
+ - async get() | scopes: none
36
+ - async refresh() | scopes: none
37
+
38
+ ### Twinkle.preview
39
+ - getLayout() | scopes: none
40
+ - Read the host preview layout and derive a fixed-world scale from layout.playfield before sizing a canvas, sprites, or mobile game UI.
41
+ - Example: const WORLD = { width: 360, height: 640 }; const layout = Twinkle.preview.getLayout(); const scale = Math.min(layout.playfield.width / WORLD.width, layout.playfield.height / WORLD.height);
42
+ - reserveInsets({ top, right, bottom, left }) | scopes: none
43
+ - Reserve host-aware safe space for HUD bars, touch controls, or other overlays before clamping gameplay.
44
+ - Example: Twinkle.preview.reserveInsets({ top: 72, bottom: 120, left: 0, right: 0 });
45
+ - setPlayfield({ x, y, width, height } | null) | scopes: none
46
+ - Declare the actual playable rectangle when the game area is smaller than the raw canvas.
47
+ - Example: Twinkle.preview.setPlayfield({ x: layout.playfield.x, y: layout.playfield.y, width: layout.playfield.width, height: layout.playfield.height });
48
+ - reportGameplayState({ playfieldBounds?, playerBounds? } | null) | scopes: none
49
+ - Report live player or avatar bounds so the preview host can detect floor, wall, or out-of-bounds issues.
50
+ - Example: Twinkle.preview.reportGameplayState({ playerBounds: { x: player.x, y: player.y, width: player.width, height: player.height } });
51
+ - getGameplayTelemetry() | scopes: none
52
+ - Read the latest preview-side gameplay telemetry snapshot.
53
+ - clearGameplayState() | scopes: none
54
+ - clearReservedInsets() | scopes: none
55
+ - subscribe(listener, { immediate } = {}) | scopes: none
56
+ - Listen for host layout changes so a fixed-world game scale or canvas surface stays synced after resize, mobile viewport changes, or embedded runtime layout shifts.
57
+ - Example: const unsubscribe = Twinkle.preview.subscribe((layout) => syncGameLayout(layout), { immediate: true });
58
+
59
+ ### Twinkle.mount
60
+ - async get() | scopes: none
61
+ - async refresh() | scopes: none
62
+
63
+ ### Twinkle.notifications
64
+ - getLaunchTarget() | scopes: none
65
+ - Read the current notification launch target, if the app was opened from a Build notification.
66
+ - onLaunchTarget(listener, { immediate } = {}) | scopes: none
67
+ - Listen for notification launch targets while the Build app is already open.
68
+ - Example: const off = Twinkle.notifications.onLaunchTarget((launchTarget) => focusEntry(launchTarget?.target?.focus?.entryId));
69
+ - async getSubjectUpdateSubscription(subjectId) | scopes: notifications:read
70
+ - Read whether the current viewer is subscribed to Build notifications for updates to a subject.
71
+ - Example: const { subscription } = await Twinkle.notifications.getSubjectUpdateSubscription(subjectId);
72
+ - async subscribeToSubjectUpdates(subjectId, { target } = {}) | scopes: notifications:write
73
+ - Subscribe the current viewer to notifications when the original subject author adds a new page or update.
74
+ - Example: await Twinkle.notifications.subscribeToSubjectUpdates(subjectId, { target: { view: 'book', subjectId } });
75
+ - async unsubscribeFromSubjectUpdates(subjectId) | scopes: notifications:write
76
+ - Unsubscribe the current viewer from Build notifications for a subject's new pages or updates.
77
+
78
+ ### Twinkle.chess
79
+ - async bestMove({ fen, depth?, skillLevel?, maxTimeMs?, timeoutMs? }) | scopes: none
80
+ - Ask the parent-hosted Stockfish engine for the best move from a FEN position.
81
+ - Example: const result = await Twinkle.chess.bestMove({ fen: game.fen(), skillLevel: 8, maxTimeMs: 1000 });
82
+ if (result.success) game.move({ from: result.from, to: result.to, promotion: result.promotion || undefined });
83
+ - async evaluate({ fen, depth?, skillLevel?, maxTimeMs?, timeoutMs? }) | scopes: none
84
+ - Analyze a FEN position and return Stockfish's current best move plus centipawn or mate evaluation.
85
+ - Example: const analysis = await Twinkle.chess.evaluate({ fen: game.fen(), depth: 12 });
86
+ console.log(analysis.bestMove, analysis.evaluation, analysis.mate);
87
+
88
+ ### Twinkle.files
89
+ - async saveAs({ fileName, url, dataUrl, data, text, json, bytes, blob, file, mimeType } = {}) | scopes: none
90
+ - Download a generated or remote file to the viewer's local device through the parent frame without opening a popup.
91
+ - Example: await Twinkle.files.saveAs({ fileName: 'fashion-guide.png', dataUrl: imageUrl, mimeType: 'image/png' });
92
+ - async uploadGenerated({ fileName, url, dataUrl, data, text, json, bytes, blob, file, mimeType } = {}) | scopes: files:write
93
+ - Upload an app-generated file to Twinkle-hosted cloud storage without opening a picker, then store the returned asset refs in sharedDb/privateDb/userDb.
94
+ - Example: const { assets } = await Twinkle.files.uploadGenerated({ fileName: 'fashion-guide.png', dataUrl: generatedImageUrl, mimeType: 'image/png' });
95
+ - async pickAndUpload({ accept, multiple } = {}) | scopes: files:write
96
+ - Pick supported local files and upload them to Twinkle-hosted cloud storage, then store the returned asset refs in sharedDb/privateDb/userDb.
97
+ - Example: const { assets, canceled } = await Twinkle.files.pickAndUpload({ accept: 'image/*,.pdf', multiple: true });
98
+ - async list({ cursor, limit } = {}) | scopes: files:read
99
+ - List the current viewer's uploaded runtime files for this build.
100
+ - Example: const { assets, usage } = await Twinkle.files.list({ limit: 20 });
101
+ - async delete(assetId) | scopes: files:write
102
+ - Delete one of the current viewer's uploaded runtime files and free up Twinkle.files quota.
103
+ - Example: await Twinkle.files.delete(assetId);
104
+
105
+ ### Twinkle.ai
106
+ - async listPrompts() | scopes: none
107
+ - async chat({ promptId, message, history, systemPrompt, requestId, onText, onStatus } = {}) | scopes: none
108
+ - Generate text with the default Lumine text model, optionally streaming text updates through onText.
109
+ - Example: const chatHistory = conversation.slice(-12).map((entry) => ({ role: entry.role === 'assistant' ? 'assistant' : 'user', content: entry.text }));
110
+ 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') });
111
+ - async generateObject({ prompt, expectedStructure, thinkingMode, mode, instructions, systemPrompt } = {}) | scopes: none
112
+ - Generate a validated structured JSON object for app decisions, routing, grading, and game-state logic.
113
+ - 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 } });
114
+ - onChatStatus(listener) | scopes: none
115
+ - Listen to shared runtime AI chat stream events.
116
+ - async generateImage({ prompt, referenceImageB64, previousResponseId, previousImageId, engine, quality, requestId, onStatus, timeoutMs } = {}) | scopes: none
117
+ - Generate or edit an image from a prompt and optional base64/data-URL reference image.
118
+ - 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) });
119
+ - onImageGenerationStatus(listener) | scopes: none
120
+ - Subscribe to real-time image generation status events forwarded into the build iframe.
121
+ - Example: const unsubscribe = Twinkle.ai.onImageGenerationStatus((status) => console.log(status.stage));
122
+
123
+ ### Twinkle.characters
124
+ - async chat({ character, thinkingMode, message, history, roomContext, scene, systemPrompt, instructions, includeWebsiteContext, requestId, onText, onStatus } = {}) | scopes: none
125
+ - Talk to Zero or Ciel from a Build app, either as a final-response call or streaming RPG-style dialogue text with onText/onStatus.
126
+ - Example: const dialogueHistory = recentTurns.slice(-16).map((entry) => ({ role: entry.role === 'assistant' ? 'assistant' : 'user', content: entry.text, speaker: entry.speaker }));
127
+ 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) });
128
+ - onChatStatus(listener) | scopes: none
129
+ - Listen to shared Zero/Ciel runtime chat stream events.
130
+
131
+ ### Twinkle.userDb
132
+ - async query(sql, params) | scopes: none
133
+ - Run a SELECT against advanced private per-user SQLite. Use Twinkle.privateDb instead for simple preferences, drafts, settings, or small JSON state.
134
+ - async exec(sql, params) | scopes: none
135
+ - Run a write or schema statement against advanced private per-user SQLite.
136
+
137
+ ### Twinkle.subjects
138
+ - async getMySubjects({ limit, cursor } = {}) | scopes: content:read
139
+ - async search({ query, limit, cursor } = {}) | scopes: content:read
140
+ - Search Twinkle subjects by text for subject picker UIs, book apps, scrapbooks, and galleries.
141
+ - Example: const { subjects } = await Twinkle.subjects.search({ query: searchText, limit: 12 });
142
+ - async getSubject(subjectId) | scopes: content:read
143
+ - async getSubjectComments(subjectId, { limit, cursor } = {}) | scopes: content:read
144
+
145
+ ### Twinkle.aiStories
146
+ - async list({ limit, cursor, difficulty, type, isListening, userId, hasImage, hasQuestions } = {}) | scopes: content:read
147
+ - List completed existing user-generated AI Stories newest first for galleries, quiz apps, readers, and image/story collections.
148
+ - Example: const { stories } = await Twinkle.aiStories.list({ hasImage: true, hasQuestions: true, limit: 12 });
149
+ - async search({ query, limit, cursor, difficulty, type, isListening, userId, hasImage, hasQuestions } = {}) | scopes: content:read
150
+ - Search completed existing user-generated AI Stories by topic or story text for galleries, quizzes, and readers.
151
+ - Example: const { stories } = await Twinkle.aiStories.search({ query: searchText, hasQuestions: true, limit: 12 });
152
+ - async get(storyId) | scopes: content:read
153
+ - Fetch one completed existing AI Story by id, including story text, media URLs, and normalized questions when available.
154
+ - Example: const { story } = await Twinkle.aiStories.get(storyId);
155
+
156
+ ### Twinkle.grammarbles
157
+ - async listQuestions({ level, limit, cursor } = {}) | scopes: content:read
158
+ - Read public Grammarbles questions and answers by level with rating/id cursor pagination.
159
+ - Example: const page = await Twinkle.grammarbles.listQuestions({ level: 3, limit: 100 }); const question = page.questions[Math.floor(Math.random() * page.questions.length)];
160
+ - async getMyQuestionHistory({ level, limit, cursor } = {}) | scopes: content:read
161
+ - Read the signed-in viewer's real Grammarbles attempt rows for trainer filtering.
162
+ - Example: const history = await Twinkle.grammarbles.getMyQuestionHistory({ level: selectedLevel, limit: 500 }); const answeredIds = new Set(history.attempts.map((attempt) => attempt.questionId));
163
+
164
+ ### Twinkle.subjectComments
165
+ - async list(subjectId, { limit, cursor, sortBy, includeReplies, author, authorUserId, replyScope } = {}) | scopes: content:read
166
+ - Read a subject's comment stream with stable keyset pagination, oldest/newest ordering, author filters, and optional same-author reply scoping.
167
+ - Example: const { subjects } = await Twinkle.subjects.search({ query: searchText, limit: 12 }); const subjectId = pickedSubject.id; const page = await Twinkle.subjectComments.list(subjectId, { sortBy: 'oldest', author: 'subjectPoster', includeReplies: true, replyScope: 'ownThread', limit: 50 });
168
+
169
+ ### Twinkle.profileComments
170
+ - async getProfileComments({ profileUserId, limit, offset, sortBy, includeReplies, range, since, until } = {}) | scopes: content:read
171
+ - async getProfileCommentIds({ profileUserId, limit, offset, sortBy, includeReplies, range, since, until } = {}) | scopes: content:read
172
+ - async getCommentsByIds(idsOrOpts) | scopes: content:read
173
+ - async getProfileCommentCounts(idsOrOpts) | scopes: content:read
174
+
175
+ ### Twinkle.leaderboards
176
+ - async get({ boardKey = 'default', limit, cursor } = {}) | scopes: none
177
+ - Read score-sorted personal-best leaderboard rows for this Build app.
178
+ - async submit({ boardKey = 'default', score, displayName, meta } = {}) | scopes: none
179
+ - Submit a score to a public Build leaderboard using server-owned viewer identity.
180
+
181
+ ### Twinkle.sharedDb
182
+ - async getTopics() | scopes: sharedDb:read
183
+ - async createTopic(name) | scopes: sharedDb:write
184
+ - async getEntries(topicName, { limit, pageSize, cursor, order, sort, direction } = {}) | scopes: sharedDb:read
185
+ - Read shared topic rows with cursor pagination.
186
+ - async loadMoreEntries(topicName, { limit, pageSize, cursor, order, sort, direction } = {}) | scopes: sharedDb:read
187
+ - Fetch the next sharedDb page.
188
+ - async addEntry(topicName, data, { notify } = {}) | scopes: sharedDb:write
189
+ - Append a shared JSON row, optionally creating a Twinkle notification from the canonical write.
190
+ - async updateEntry(entryId, data, { notify } = {}) | scopes: sharedDb:write
191
+ - Update a viewer-owned shared row, optionally notifying safe recipients from the canonical write.
192
+ - async deleteEntry(entryId) | scopes: sharedDb:write
193
+
194
+ ### Twinkle.chat
195
+ - async listRooms() | scopes: chat:read
196
+ - async createRoom({ roomKey, name }) | scopes: chat:write
197
+ - async listMessages(roomKey, { cursor, limit } = {}) | scopes: chat:read
198
+ - async sendMessage(roomKey, textOrOptions, options) | scopes: chat:write
199
+ - async deleteMessage(messageId) | scopes: chat:write
200
+ - subscribe(roomKey, listener) | scopes: chat:read
201
+
202
+ ### Twinkle.world
203
+ - async join({ worldKey = 'default', roomKey = 'main', instanceId = 'main', presence, player } = {}) | scopes: none
204
+ - Join a realtime Build world room and receive a snapshot plus a session handle for presence updates, actions, and room events.
205
+ - Example: const world = await Twinkle.world.join({ roomKey: 'town-square', presence: { x: 0, y: 0, z: 0, facing: 'south' }, player: { name: avatarName } });
206
+ world.subscribe((event) => updateRemotePlayers(event.players));
207
+ world.updatePresence({ x, y, z, facing });
208
+ - leaveAll() | scopes: none
209
+ - Leave every active world session in the current iframe.
210
+
211
+ ### Twinkle.users
212
+ - async getUser(userId) | scopes: user:read
213
+ - async getUsers({ search, userIds, cursor, limit } = {}) | scopes: users:read
214
+
215
+ ### Twinkle.reflections
216
+ - async getDailyReflections({ userIds, cursor, lastId, limit } = {}) | scopes: dailyReflections:read
217
+ - async getDailyReflectionsByUser(userId, { cursor, lastId, limit } = {}) | scopes: dailyReflections:read
218
+
219
+ ### Twinkle.privateDb
220
+ - async get(key) | scopes: privateDb:read
221
+ - Read one key from the default private per-user JSON store.
222
+ - async list({ prefix, limit, cursor } = {}) | scopes: privateDb:read
223
+ - List keys from the default private per-user JSON store.
224
+ - async set(key, value) | scopes: privateDb:write
225
+ - Upsert one JSON-serializable value in the default private per-user store.
226
+ - async remove(key) | scopes: privateDb:write
227
+ - Delete one key from the default private per-user JSON store.
228
+
229
+ ### Twinkle.reminders
230
+ - async list({ includeDisabled, limit } = {}) | scopes: reminders:read
231
+ - async create({ title, body, targetPath, payload, schedule, isEnabled }) | scopes: reminders:write
232
+ - async update(reminderId, patch) | scopes: reminders:write
233
+ - async remove(reminderId) | scopes: reminders:write
234
+ - async getDue({ now, autoAcknowledge, limit } = {}) | scopes: reminders:read
235
+
236
+ ## Examples
237
+
238
+ ### Daily reflection feed
239
+
240
+ ```js
241
+ const feed = await Twinkle.reflections.getDailyReflections({ limit: 20 });
242
+ ```
243
+
244
+ ### Advanced per-user SQLite
245
+ Use Twinkle.userDb only when the app needs private relational tables, indexes, filtered queries, or aggregates. Use Twinkle.privateDb for simple settings and small JSON state.
246
+ Keywords: sqlite, userDb, advanced private data, relational, tables, indexes
247
+
248
+ ```js
249
+ await Twinkle.userDb.exec('CREATE TABLE IF NOT EXISTS follows (userId INT PRIMARY KEY, followedAt INTEGER)');
250
+ ```
251
+
252
+ ### List my subjects
253
+
254
+ ```js
255
+ const { subjects } = await Twinkle.subjects.getMySubjects({ limit: 10 });
256
+ subjects.forEach(s => console.log(s.id, s.title));
257
+ ```
258
+
259
+ ### Search subjects for a picker
260
+ Use this when the app should ask the viewer which Twinkle subject to turn into a book, scrapbook, or gallery.
261
+ Keywords: subject, search, picker, book, mount
262
+
263
+ ```js
264
+ const { subjects } = await Twinkle.subjects.search({ query: searchText, limit: 12 });
265
+ // Let the viewer pick one, then use picked.id with getSubject and subjectComments.list.
266
+ ```
267
+
268
+ ### Build a book from a subject
269
+ Ask the viewer to choose a subject with Twinkle.subjects.search, optionally preselecting Twinkle.mount.get() when the host provides a subject. Then fetch metadata and read the subject comment stream oldest-first. Filter to author:'subjectPoster' when only the poster's comments should become pages, and use replyScope:'ownThread' when including poster replies.
270
+ Keywords: subject, comments, book, pages, search, picker, mount, subject poster
271
+
272
+ ```js
273
+ const mount = await Twinkle.mount.get();
274
+ const initialSubjectId = mount?.type === 'subject' ? mount.id : null;
275
+ const { subjects } = initialSubjectId
276
+ ? { subjects: [] }
277
+ : await Twinkle.subjects.search({ query: searchText, limit: 12 });
278
+ const subjectId = initialSubjectId || pickedSubject.id;
279
+ const { subject } = await Twinkle.subjects.getSubject(subjectId);
280
+ const { comments, pagination } = await Twinkle.subjectComments.list(subjectId, {
281
+ sortBy: 'oldest',
282
+ author: 'subjectPoster',
283
+ includeReplies: true,
284
+ replyScope: 'ownThread',
285
+ limit: 50
286
+ });
287
+ console.log('Title:', subject.title, 'Pages:', comments.length, 'hasMore:', pagination?.hasMore);
288
+ ```
289
+
290
+ ### Search AI Stories for a quiz app
291
+ Use existing user-generated AI Stories as source material for visual galleries, readers, and question-based games.
292
+ Keywords: ai stories, story, quiz, questions, image, gallery
293
+
294
+ ```js
295
+ const { stories } = await Twinkle.aiStories.search({
296
+ query: searchText,
297
+ hasQuestions: true,
298
+ hasImage: true,
299
+ limit: 12
300
+ });
301
+ const picked = stories[0];
302
+ console.log(picked.topic, picked.imageUrl, picked.questions.length);
303
+ ```
304
+
305
+ ### Recent profile comments
306
+
307
+ ```js
308
+ const { comments, pagination } = await Twinkle.profileComments.getProfileComments({
309
+ sortBy: 'newest',
310
+ includeReplies: true,
311
+ limit: 20
312
+ });
313
+ console.log('Loaded comments:', comments.length, 'hasMore:', pagination?.hasMore);
314
+ ```
315
+
316
+ ### Build leaderboard
317
+ Submit personal-best scores for signed-in viewers and guests, then read score-sorted rankings.
318
+ Keywords: leaderboard, leaderboards, scoreboard, scores, rankings, personal best, guest scores
319
+
320
+ ```js
321
+ const boardKey = 'main';
322
+ const finalScore = computeFinalScoreForFinishedRun();
323
+ const viewer = await Twinkle.viewer.get();
324
+ const guestName = savedGuestName || readGuestNameFromInput();
325
+ if (viewer.isGuest && !guestName) {
326
+ showGuestNameForm();
327
+ return;
328
+ }
329
+
330
+ await Twinkle.leaderboards.submit({
331
+ boardKey,
332
+ score: finalScore,
333
+ displayName: viewer.isGuest ? guestName : undefined,
334
+ meta: { mode: 'classic' }
335
+ });
336
+
337
+ const page = await Twinkle.leaderboards.get({ boardKey, limit: 10 });
338
+ renderLeaderboard(page.entries, page.personalBest);
339
+ ```
340
+
341
+ ### Paginated shared feed
342
+
343
+ ```js
344
+ const pageSize = 12;
345
+ let nextCursor = null;
346
+
347
+ async function loadFirstPage() {
348
+ const page = await Twinkle.sharedDb.getEntries('posts', { pageSize });
349
+ nextCursor = page.cursor;
350
+ renderPosts(page.entries, { append: false, hasMore: page.hasMore });
351
+ }
352
+
353
+ async function loadMorePosts() {
354
+ if (!nextCursor) return;
355
+ const page = await Twinkle.sharedDb.loadMoreEntries('posts', { pageSize, cursor: nextCursor });
356
+ nextCursor = page.cursor;
357
+ renderPosts(page.entries, { append: true, hasMore: page.hasMore });
358
+ }
359
+ ```
360
+
361
+ ### Update a shared entry
362
+
363
+ ```js
364
+ const { entry } = await Twinkle.sharedDb.updateEntry(entryId, { name: 'Alice', score: 99 });
365
+ console.log('Updated:', entry.data);
366
+ ```
367
+
368
+ ### Store private user settings
369
+ Default private per-user key/value storage for preferences, drafts, settings, and small JSON state.
370
+ Keywords: storage, private storage, privateDb, settings, preferences, drafts, small JSON
371
+
372
+ ```js
373
+ await Twinkle.privateDb.set('prefs.theme', { mode: 'mint' });
374
+ const { item } = await Twinkle.privateDb.get('prefs.theme');
375
+ console.log('Theme:', item?.value?.mode);
376
+ ```
377
+
378
+ ### Build a lobby chat
379
+
380
+ ```js
381
+ await Twinkle.chat.createRoom({ roomKey: 'lobby', name: 'Lobby' });
382
+ const unsubscribe = Twinkle.chat.subscribe('lobby', (event) => console.log('chat event', event));
383
+ await Twinkle.chat.sendMessage('lobby', 'hello');
384
+ ```
385
+
386
+ ### Realtime MMO town room
387
+ Use Twinkle.world for live avatar presence and lightweight room actions, while durable state like inventory and quests stays in sharedDb/privateDb.
388
+ Keywords: multiplayer, mmo, town, presence, avatars, movement, three.js, realtime
389
+
390
+ ```js
391
+ const world = await Twinkle.world.join({
392
+ worldKey: 'town',
393
+ roomKey: 'square',
394
+ presence: { x: 0, y: 0, z: 0, facing: 'south', animation: 'idle' },
395
+ player: { name: avatarName }
396
+ });
397
+
398
+ world.subscribe((event) => {
399
+ renderPlayers(event.players);
400
+ if (event.type === 'action.received' && event.action?.type === 'emote') {
401
+ showEmote(event.sessionId, event.action.data.emote);
402
+ }
403
+ });
404
+
405
+ // Throttle this in the game loop, for example 5-15 times per second.
406
+ await world.updatePresence({ x: player.x, y: player.y, z: player.z, facing });
407
+ await world.send('emote', { emote: 'wave' });
408
+ ```
409
+
410
+ ### Play chess against the computer
411
+ Use the parent-managed Stockfish helper for the computer move while app code owns the board, legal moves, and game-over state.
412
+ Keywords: chess, stockfish, computer opponent, board game, fen
413
+
414
+ ```js
415
+ const result = await Twinkle.chess.bestMove({ fen: game.fen(), skillLevel: 7, maxTimeMs: 1000 });
416
+ if (result.success) {
417
+ game.move({ from: result.from, to: result.to, promotion: result.promotion || undefined });
418
+ renderBoard(game);
419
+ }
420
+
421
+ const strongest = await Twinkle.chess.bestMove({ fen: game.fen(), skillLevel: 20, maxTimeMs: 60000 });
422
+ ```
423
+
424
+ ### Upload files and store shared metadata
425
+
426
+ ```js
427
+ const { assets, canceled } = await Twinkle.files.pickAndUpload({ accept: 'image/*,.pdf', multiple: true });
428
+ if (!canceled) {
429
+ for (const asset of assets) {
430
+ await Twinkle.sharedDb.addEntry('uploads', { assetId: asset.id, url: asset.url, thumbUrl: asset.thumbUrl, fileName: asset.fileName, mimeType: asset.mimeType });
431
+ }
432
+ }
433
+ ```
434
+
435
+ ### List and remove uploaded files
436
+
437
+ ```js
438
+ const { assets, usage } = await Twinkle.files.list({ limit: 20 });
439
+ if (assets[0]) {
440
+ await Twinkle.files.delete(assets[0].id);
441
+ }
442
+ console.log('Remaining quota bytes:', usage?.remainingBytes);
443
+ ```
444
+
445
+ ### Create a daily focus reminder
446
+
447
+ ```js
448
+ await Twinkle.reminders.create({
449
+ title: 'Pick your top 3',
450
+ body: 'Choose your focus tasks for today.',
451
+ schedule: { type: 'daily', timeZone: 'America/Los_Angeles', hour: 9, minute: 0 },
452
+ targetPath: '/focus'
453
+ });
454
+ ```