@stage5/lumine 0.1.1 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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.23.0
4
- Updated: 2026-05-24
5
- Generated: 2026-05-25T00:43:05.370Z
3
+ Version: 1.24.0
4
+ Updated: 2026-05-26
5
+ Generated: 2026-05-26T10:20:06.718Z
6
6
 
7
7
  ## Notes
8
8
  - This SDK is injected into Build iframes via the Build preview/runtime.
@@ -21,217 +21,315 @@ Generated: 2026-05-25T00:43:05.370Z
21
21
  - Twinkle.ai.chat history entries must use { role, content }; map local message.text fields to content before passing history.
22
22
 
23
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
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, notifications:emit, reminders:read, reminders:write
25
25
 
26
26
  ## Namespaces
27
27
 
28
28
  ### Twinkle.capabilities
29
29
  - async get() | scopes: none
30
+ - Returns: Capability snapshot
30
31
  - async can(actionName) | scopes: none
32
+ - Returns: boolean
31
33
  - async listActions() | scopes: none
34
+ - Returns: { available, blocked, details }
32
35
  - async refresh() | scopes: none
36
+ - Returns: Capability snapshot
33
37
 
34
38
  ### Twinkle.viewer
35
39
  - async get() | scopes: none
40
+ - Returns: { id, username, profilePicUrl, isLoggedIn, isOwner, isGuest }
36
41
  - async refresh() | scopes: none
42
+ - Returns: Viewer info
37
43
 
38
44
  ### Twinkle.preview
39
45
  - getLayout() | scopes: none
46
+ - Returns: { mode, viewport, stage, safeInsets, playfield }
40
47
  - Read the host preview layout and derive a fixed-world scale from layout.playfield before sizing a canvas, sprites, or mobile game UI.
41
48
  - 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
49
  - reserveInsets({ top, right, bottom, left }) | scopes: none
50
+ - Returns: { mode, viewport, stage, safeInsets, playfield }
43
51
  - Reserve host-aware safe space for HUD bars, touch controls, or other overlays before clamping gameplay.
44
52
  - Example: Twinkle.preview.reserveInsets({ top: 72, bottom: 120, left: 0, right: 0 });
45
53
  - setPlayfield({ x, y, width, height } | null) | scopes: none
54
+ - Returns: { playfieldBounds, playerBounds, overflowTop, overflowRight, overflowBottom, overflowLeft, status, reportedAt } | null
46
55
  - Declare the actual playable rectangle when the game area is smaller than the raw canvas.
47
56
  - Example: Twinkle.preview.setPlayfield({ x: layout.playfield.x, y: layout.playfield.y, width: layout.playfield.width, height: layout.playfield.height });
48
57
  - reportGameplayState({ playfieldBounds?, playerBounds? } | null) | scopes: none
58
+ - Returns: { playfieldBounds, playerBounds, overflowTop, overflowRight, overflowBottom, overflowLeft, status, reportedAt } | null
49
59
  - Report live player or avatar bounds so the preview host can detect floor, wall, or out-of-bounds issues.
50
60
  - Example: Twinkle.preview.reportGameplayState({ playerBounds: { x: player.x, y: player.y, width: player.width, height: player.height } });
51
61
  - getGameplayTelemetry() | scopes: none
62
+ - Returns: { playfieldBounds, playerBounds, overflowTop, overflowRight, overflowBottom, overflowLeft, status, reportedAt } | null
52
63
  - Read the latest preview-side gameplay telemetry snapshot.
53
64
  - clearGameplayState() | scopes: none
65
+ - Returns: { playfieldBounds, playerBounds, overflowTop, overflowRight, overflowBottom, overflowLeft, status, reportedAt } | null
54
66
  - clearReservedInsets() | scopes: none
67
+ - Returns: { mode, viewport, stage, safeInsets, playfield }
55
68
  - subscribe(listener, { immediate } = {}) | scopes: none
69
+ - Returns: unsubscribe()
56
70
  - 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
71
  - Example: const unsubscribe = Twinkle.preview.subscribe((layout) => syncGameLayout(layout), { immediate: true });
58
72
 
59
73
  ### Twinkle.mount
60
74
  - async get() | scopes: none
75
+ - Returns: { type: 'subject', id: number } | null
61
76
  - async refresh() | scopes: none
77
+ - Returns: { type: 'subject', id: number } | null
62
78
 
63
79
  ### Twinkle.notifications
64
80
  - getLaunchTarget() | scopes: none
81
+ - Returns: { notificationId, buildId, eventKey, eventLabel, target, payload? } | null
65
82
  - Read the current notification launch target, if the app was opened from a Build notification.
66
83
  - onLaunchTarget(listener, { immediate } = {}) | scopes: none
84
+ - Returns: unsubscribe()
67
85
  - Listen for notification launch targets while the Build app is already open.
68
86
  - Example: const off = Twinkle.notifications.onLaunchTarget((launchTarget) => focusEntry(launchTarget?.target?.focus?.entryId));
87
+ - async getSubscription(channelKey, { targetKey }) | scopes: notifications:read
88
+ - Returns: { subscription }
89
+ - Read whether the current viewer is subscribed to an app-defined notification channel target.
90
+ - Example: const { subscription } = await Twinkle.notifications.getSubscription('room.message', { targetKey: 'room:lobby' });
91
+ - async subscribe(channelKey, { targetKey, launchTarget }) | scopes: notifications:write
92
+ - Returns: { subscription }
93
+ - Subscribe the current viewer to an app-defined notification channel target.
94
+ - Example: await Twinkle.notifications.subscribe('room.message', { targetKey: 'room:lobby', launchTarget: { view: 'room', roomId: 'lobby' } });
95
+ - async unsubscribe(channelKey, { targetKey }) | scopes: notifications:write
96
+ - Returns: { subscription: null }
97
+ - Unsubscribe the current viewer from an app-defined notification channel target.
98
+ - Example: await Twinkle.notifications.unsubscribe('room.message', { targetKey: 'room:lobby' });
99
+ - async notifySubscribers(channelKey, { targetKey, eventKey, label, summary, launchTarget, payload }) | scopes: notifications:emit
100
+ - Returns: { sent }
101
+ - Notify viewers who opted into an app-defined channel target, without requiring a sharedDb write.
102
+ - Example: await Twinkle.notifications.notifySubscribers('room.message', { targetKey: 'room:lobby', eventKey: 'room.message.created', label: 'Room messages', summary: 'posted in Lobby', launchTarget: { view: 'room', roomId: 'lobby', messageId } });
69
103
  - async getSubjectUpdateSubscription(subjectId) | scopes: notifications:read
104
+ - Returns: { subscription }
70
105
  - Read whether the current viewer is subscribed to Build notifications for updates to a subject.
71
106
  - Example: const { subscription } = await Twinkle.notifications.getSubjectUpdateSubscription(subjectId);
72
107
  - async subscribeToSubjectUpdates(subjectId, { target } = {}) | scopes: notifications:write
108
+ - Returns: { subscription }
73
109
  - Subscribe the current viewer to notifications when the original subject author adds a new page or update.
74
110
  - Example: await Twinkle.notifications.subscribeToSubjectUpdates(subjectId, { target: { view: 'book', subjectId } });
75
111
  - async unsubscribeFromSubjectUpdates(subjectId) | scopes: notifications:write
112
+ - Returns: { subscription: null }
76
113
  - Unsubscribe the current viewer from Build notifications for a subject's new pages or updates.
77
114
 
78
115
  ### Twinkle.chess
79
116
  - async bestMove({ fen, depth?, skillLevel?, maxTimeMs?, timeoutMs? }) | scopes: none
117
+ - Returns: { success, move, bestMove, from, to, promotion, evaluation, depth, mate, error, engine }
80
118
  - Ask the parent-hosted Stockfish engine for the best move from a FEN position.
81
119
  - Example: const result = await Twinkle.chess.bestMove({ fen: game.fen(), skillLevel: 8, maxTimeMs: 1000 });
82
120
  if (result.success) game.move({ from: result.from, to: result.to, promotion: result.promotion || undefined });
83
121
  - async evaluate({ fen, depth?, skillLevel?, maxTimeMs?, timeoutMs? }) | scopes: none
122
+ - Returns: { success, move, bestMove, from, to, promotion, evaluation, depth, mate, error, engine }
84
123
  - Analyze a FEN position and return Stockfish's current best move plus centipawn or mate evaluation.
85
124
  - Example: const analysis = await Twinkle.chess.evaluate({ fen: game.fen(), depth: 12 });
86
125
  console.log(analysis.bestMove, analysis.evaluation, analysis.mate);
87
126
 
88
127
  ### Twinkle.files
89
128
  - async saveAs({ fileName, url, dataUrl, data, text, json, bytes, blob, file, mimeType } = {}) | scopes: none
129
+ - Returns: { success, fileName, size?, mimeType?, method }
90
130
  - Download a generated or remote file to the viewer's local device through the parent frame without opening a popup.
91
131
  - Example: await Twinkle.files.saveAs({ fileName: 'fashion-guide.png', dataUrl: imageUrl, mimeType: 'image/png' });
92
132
  - async uploadGenerated({ fileName, url, dataUrl, data, text, json, bytes, blob, file, mimeType } = {}) | scopes: files:write
133
+ - Returns: { assets: [{ id, buildId, fileName, originalFileName, mimeType, sizeBytes, filePath, url, thumbUrl, fileType, uploadedByUserId, createdAt }], failed?: [{ fileName, message }], canceled }
93
134
  - 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
135
  - Example: const { assets } = await Twinkle.files.uploadGenerated({ fileName: 'fashion-guide.png', dataUrl: generatedImageUrl, mimeType: 'image/png' });
95
136
  - async pickAndUpload({ accept, multiple } = {}) | scopes: files:write
137
+ - Returns: { assets: [{ id, buildId, fileName, originalFileName, mimeType, sizeBytes, filePath, url, thumbUrl, fileType, uploadedByUserId, createdAt }], failed?: [{ fileName, message }], canceled }
96
138
  - Pick supported local files and upload them to Twinkle-hosted cloud storage, then store the returned asset refs in sharedDb/privateDb/userDb.
97
139
  - Example: const { assets, canceled } = await Twinkle.files.pickAndUpload({ accept: 'image/*,.pdf', multiple: true });
98
140
  - async list({ cursor, limit } = {}) | scopes: files:read
141
+ - Returns: { assets: [{ id, buildId, fileName, originalFileName, mimeType, sizeBytes, filePath, url, thumbUrl, fileType, uploadedByUserId, createdAt }], nextCursor, usage: { totalBytes, fileCount, maxRuntimeFileStorageBytes, remainingBytes } | null }
99
142
  - List the current viewer's uploaded runtime files for this build.
100
143
  - Example: const { assets, usage } = await Twinkle.files.list({ limit: 20 });
101
144
  - async delete(assetId) | scopes: files:write
145
+ - Returns: { success, deletedAssetId, usage: { totalBytes, fileCount, maxRuntimeFileStorageBytes, remainingBytes } | null }
102
146
  - Delete one of the current viewer's uploaded runtime files and free up Twinkle.files quota.
103
147
  - Example: await Twinkle.files.delete(assetId);
104
148
 
105
149
  ### Twinkle.ai
106
150
  - async listPrompts() | scopes: none
151
+ - Returns: Array<{ id, title, description }>
107
152
  - async chat({ promptId, message, history, systemPrompt, requestId, onText, onStatus } = {}) | scopes: none
153
+ - Returns: { text, response, model, aiUsagePolicy }
108
154
  - Generate text with the default Lumine text model, optionally streaming text updates through onText.
109
155
  - Example: const chatHistory = conversation.slice(-12).map((entry) => ({ role: entry.role === 'assistant' ? 'assistant' : 'user', content: entry.text }));
110
156
  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
157
  - async generateObject({ prompt, expectedStructure, thinkingMode, mode, instructions, systemPrompt } = {}) | scopes: none
158
+ - Returns: { object, result, model, provider, thinkingMode, requestedThinkingMode, aiUsagePolicy }
112
159
  - Generate a validated structured JSON object for app decisions, routing, grading, and game-state logic.
113
160
  - 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
161
  - onChatStatus(listener) | scopes: none
162
+ - Returns: unsubscribe function
115
163
  - Listen to shared runtime AI chat stream events.
116
164
  - async generateImage({ prompt, referenceImageB64, previousResponseId, previousImageId, engine, quality, requestId, onStatus, timeoutMs } = {}) | scopes: none
165
+ - Returns: { success, imageUrl, responseId, imageId, engine, quality, aiUsagePolicy } or { success: false, error, reason, code, aiUsagePolicy }
117
166
  - Generate or edit an image from a prompt and optional base64/data-URL reference image.
118
167
  - 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
168
  - onImageGenerationStatus(listener) | scopes: none
169
+ - Returns: unsubscribe function
120
170
  - Subscribe to real-time image generation status events forwarded into the build iframe.
121
171
  - Example: const unsubscribe = Twinkle.ai.onImageGenerationStatus((status) => console.log(status.stage));
122
172
 
123
173
  ### Twinkle.characters
124
174
  - async chat({ character, thinkingMode, message, history, roomContext, scene, systemPrompt, instructions, includeWebsiteContext, requestId, onText, onStatus } = {}) | scopes: none
175
+ - Returns: { text, response, character, aiUsername, thinkingMode, requestedThinkingMode, includeWebsiteContext, model, provider, aiUsagePolicy }
125
176
  - 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
177
  - Example: const dialogueHistory = recentTurns.slice(-16).map((entry) => ({ role: entry.role === 'assistant' ? 'assistant' : 'user', content: entry.text, speaker: entry.speaker }));
127
178
  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
179
  - onChatStatus(listener) | scopes: none
180
+ - Returns: unsubscribe function
129
181
  - Listen to shared Zero/Ciel runtime chat stream events.
130
182
 
131
183
  ### Twinkle.userDb
132
184
  - async query(sql, params) | scopes: none
185
+ - Returns: { rows, rowCount, truncated }
133
186
  - Run a SELECT against advanced private per-user SQLite. Use Twinkle.privateDb instead for simple preferences, drafts, settings, or small JSON state.
134
187
  - async exec(sql, params) | scopes: none
188
+ - Returns: { changes, lastInsertRowid }
135
189
  - Run a write or schema statement against advanced private per-user SQLite.
136
190
 
137
191
  ### Twinkle.subjects
138
192
  - async getMySubjects({ limit, cursor } = {}) | scopes: content:read
193
+ - Returns: { subjects: [{ id, title, description, filePath, fileName, fileSize, thumbUrl, timeStamp, rootType, rootId, rewardLevel }], cursor? }
139
194
  - async search({ query, limit, cursor } = {}) | scopes: content:read
195
+ - Returns: { subjects: [{ id, contentType, contentId, title, description, filePath, fileName, fileSize, thumbUrl, timeStamp, userId, username, profilePicUrl, rootType, rootId, rewardLevel, numComments }], cursor?, pagination: { limit, hasMore, nextCursor }, filters: { query } }
140
196
  - Search Twinkle subjects by text for subject picker UIs, book apps, scrapbooks, and galleries.
141
197
  - Example: const { subjects } = await Twinkle.subjects.search({ query: searchText, limit: 12 });
142
198
  - async getSubject(subjectId) | scopes: content:read
199
+ - Returns: { subject: { id, title, description, filePath, fileName, fileSize, thumbUrl, secretAnswer, secretAttachment, timeStamp, userId, username, profilePicUrl, rootType, rootId, rewardLevel } }
143
200
  - async getSubjectComments(subjectId, { limit, cursor } = {}) | scopes: content:read
201
+ - Returns: { comments: [{ id, content, filePath, fileName, fileSize, thumbUrl, timeStamp }], cursor? }
144
202
 
145
203
  ### Twinkle.aiStories
146
204
  - async list({ limit, cursor, difficulty, type, isListening, userId, hasImage, hasQuestions } = {}) | scopes: content:read
205
+ - Returns: { stories: [{ id, contentType, contentId, topic, topicKey, type, story, explanation, difficulty, isListening, imagePath, imageUrl, audioPath, audioUrl, questions, questionsBy, hasImage, hasQuestions, userId, username, profilePicUrl, timeStamp }], cursor?, pagination: { limit, hasMore, nextCursor }, filters }
147
206
  - List completed existing user-generated AI Stories newest first for galleries, quiz apps, readers, and image/story collections.
148
207
  - Example: const { stories } = await Twinkle.aiStories.list({ hasImage: true, hasQuestions: true, limit: 12 });
149
208
  - async search({ query, limit, cursor, difficulty, type, isListening, userId, hasImage, hasQuestions } = {}) | scopes: content:read
209
+ - Returns: { stories: [{ id, contentType, contentId, topic, topicKey, type, story, explanation, difficulty, isListening, imagePath, imageUrl, audioPath, audioUrl, questions, questionsBy, hasImage, hasQuestions, userId, username, profilePicUrl, timeStamp }], cursor?, pagination: { limit, hasMore, nextCursor }, filters }
150
210
  - Search completed existing user-generated AI Stories by topic or story text for galleries, quizzes, and readers.
151
211
  - Example: const { stories } = await Twinkle.aiStories.search({ query: searchText, hasQuestions: true, limit: 12 });
152
212
  - async get(storyId) | scopes: content:read
213
+ - Returns: { story: { id, contentType, contentId, topic, topicKey, type, story, explanation, difficulty, isListening, imagePath, imageUrl, audioPath, audioUrl, questions, questionsBy, hasImage, hasQuestions, userId, username, profilePicUrl, timeStamp } }
153
214
  - Fetch one completed existing AI Story by id, including story text, media URLs, and normalized questions when available.
154
215
  - Example: const { story } = await Twinkle.aiStories.get(storyId);
155
216
 
156
217
  ### Twinkle.grammarbles
157
218
  - async listQuestions({ level, limit, cursor } = {}) | scopes: content:read
219
+ - Returns: { questions: [{ id, level, rating, question, choices, answerIndex, correctChoice, correctChoiceKey, isChecked, explanation }], cursor?, pagination: { level, limit, hasMore, nextCursor } }
158
220
  - Read public Grammarbles questions and answers by level with rating/id cursor pagination.
159
221
  - Example: const page = await Twinkle.grammarbles.listQuestions({ level: 3, limit: 100 }); const question = page.questions[Math.floor(Math.random() * page.questions.length)];
160
222
  - async getMyQuestionHistory({ level, limit, cursor } = {}) | scopes: content:read
223
+ - Returns: { attempts: [{ id, questionId, level, grade, gradeRank, isCorrect, attemptNumber, timeStamp }], cursor?, pagination: { level, limit, hasMore, nextCursor } }
161
224
  - Read the signed-in viewer's real Grammarbles attempt rows for trainer filtering.
162
225
  - Example: const history = await Twinkle.grammarbles.getMyQuestionHistory({ level: selectedLevel, limit: 500 }); const answeredIds = new Set(history.attempts.map((attempt) => attempt.questionId));
163
226
 
164
227
  ### Twinkle.subjectComments
165
228
  - async list(subjectId, { limit, cursor, sortBy, includeReplies, author, authorUserId, replyScope } = {}) | scopes: content:read
229
+ - Returns: { comments: [{ id, content, filePath, fileName, fileSize, thumbUrl, timeStamp, userId, username, profilePicUrl, commentId, replyId }], cursor?, pagination: { limit, hasMore, nextCursor }, filters: { subjectId, sortBy, includeReplies, author, replyScope, authorUserId } }
166
230
  - Read a subject's comment stream with stable keyset pagination, oldest/newest ordering, author filters, and optional same-author reply scoping.
167
231
  - 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
232
 
169
233
  ### Twinkle.profileComments
170
234
  - async getProfileComments({ profileUserId, limit, offset, sortBy, includeReplies, range, since, until } = {}) | scopes: content:read
235
+ - Returns: { comments: [{ id, content, filePath, fileName, fileSize, thumbUrl, timeStamp, userId, username, profilePicUrl, likes, replies, commentId, replyId }], pagination: { limit, offset, hasMore, nextOffset }, filters: { profileUserId, sortBy, includeReplies, since, until } }
171
236
  - async getProfileCommentIds({ profileUserId, limit, offset, sortBy, includeReplies, range, since, until } = {}) | scopes: content:read
237
+ - Returns: { ids: number[], pagination: { limit, offset, hasMore, nextOffset }, filters: { profileUserId, sortBy, includeReplies, since, until } }
172
238
  - async getCommentsByIds(idsOrOpts) | scopes: content:read
239
+ - Returns: { comments: [{ id, content, filePath, fileName, fileSize, thumbUrl, timeStamp, userId, username, profilePicUrl, commentId, replyId }] }
173
240
  - async getProfileCommentCounts(idsOrOpts) | scopes: content:read
241
+ - Returns: { countsById: { [commentId]: { likes, replies } } }
174
242
 
175
243
  ### Twinkle.leaderboards
176
244
  - async get({ boardKey = 'default', limit, cursor } = {}) | scopes: none
245
+ - 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 }
177
246
  - Read score-sorted personal-best leaderboard rows for this Build app.
178
247
  - async submit({ boardKey = 'default', score, displayName, meta } = {}) | scopes: none
248
+ - Returns: { entry: { id, buildId, boardKey, viewerKind, userId, displayName, score, meta, achievedAt, createdAt, updatedAt } | null, personalBest: { id, buildId, boardKey, viewerKind, userId, displayName, score, meta, achievedAt, createdAt, updatedAt } | null, improved, previousScore }
179
249
  - Submit a score to a public Build leaderboard using server-owned viewer identity.
180
250
 
181
251
  ### Twinkle.sharedDb
182
252
  - async getTopics() | scopes: sharedDb:read
253
+ - Returns: { topics: [{ id, name, createdBy, createdAt }] }
183
254
  - async createTopic(name) | scopes: sharedDb:write
255
+ - Returns: { topic: { id, name, createdBy, createdAt } }
184
256
  - async getEntries(topicName, { limit, pageSize, cursor, order, sort, direction } = {}) | scopes: sharedDb:read
257
+ - Returns: { entries: [{ id, topicId, userId, username, profilePicUrl, data, createdAt, updatedAt }], cursor?, hasMore }
185
258
  - Read shared topic rows with cursor pagination.
186
259
  - async loadMoreEntries(topicName, { limit, pageSize, cursor, order, sort, direction } = {}) | scopes: sharedDb:read
260
+ - Returns: { entries: [{ id, topicId, userId, username, profilePicUrl, data, createdAt, updatedAt }], cursor?, hasMore }
187
261
  - Fetch the next sharedDb page.
188
262
  - async addEntry(topicName, data, { notify } = {}) | scopes: sharedDb:write
263
+ - Returns: { entry: { id, topicId, userId, username, profilePicUrl, data, createdAt, updatedAt } }
189
264
  - Append a shared JSON row, optionally creating a Twinkle notification from the canonical write.
190
265
  - async updateEntry(entryId, data, { notify } = {}) | scopes: sharedDb:write
266
+ - Returns: { entry: { id, topicId, userId, username, profilePicUrl, data, createdAt, updatedAt } }
191
267
  - Update a viewer-owned shared row, optionally notifying safe recipients from the canonical write.
192
268
  - async deleteEntry(entryId) | scopes: sharedDb:write
269
+ - Returns: { success: true }
193
270
 
194
271
  ### Twinkle.chat
195
272
  - async listRooms() | scopes: chat:read
273
+ - Returns: { rooms: [{ id, buildId, key, name, createdByUserId, createdAt, updatedAt }] }
196
274
  - async createRoom({ roomKey, name }) | scopes: chat:write
275
+ - Returns: { room: { id, buildId, key, name, createdByUserId, createdAt, updatedAt } }
197
276
  - async listMessages(roomKey, { cursor, limit } = {}) | scopes: chat:read
277
+ - Returns: { messages: [{ id, roomId, roomKey, userId, username, profilePicUrl, role, status, text, metadata, clientMessageId, createdAt, updatedAt }], cursor? }
198
278
  - async sendMessage(roomKey, textOrOptions, options) | scopes: chat:write
279
+ - Returns: { message: { id, buildId, roomId, roomKey, userId, username, profilePicUrl, role, status, text, metadata, clientMessageId, createdAt, updatedAt }, room: { id, buildId, key, name, createdByUserId, createdAt, updatedAt }, created }
199
280
  - async deleteMessage(messageId) | scopes: chat:write
281
+ - Returns: { success: true, messageId }
200
282
  - subscribe(roomKey, listener) | scopes: chat:read
283
+ - Returns: unsubscribe function
201
284
 
202
285
  ### Twinkle.world
203
286
  - async join({ worldKey = 'default', roomKey = 'main', instanceId = 'main', presence, player } = {}) | scopes: none
287
+ - Returns: { sessionId, session, room, players, snapshot, subscribe(listener), updatePresence(patch), send(actionOrType, data), leave() }
204
288
  - Join a realtime Build world room and receive a snapshot plus a session handle for presence updates, actions, and room events.
205
289
  - Example: const world = await Twinkle.world.join({ roomKey: 'town-square', presence: { x: 0, y: 0, z: 0, facing: 'south' }, player: { name: avatarName } });
206
290
  world.subscribe((event) => updateRemotePlayers(event.players));
207
291
  world.updatePresence({ x, y, z, facing });
208
292
  - leaveAll() | scopes: none
293
+ - Returns: void
209
294
  - Leave every active world session in the current iframe.
210
295
 
211
296
  ### Twinkle.users
212
297
  - async getUser(userId) | scopes: user:read
298
+ - Returns: { id, username, profilePicUrl, realName } | null
213
299
  - async getUsers({ search, userIds, cursor, limit } = {}) | scopes: users:read
300
+ - Returns: { users: [{ id, username, profilePicUrl, realName }], cursor? }
214
301
 
215
302
  ### Twinkle.reflections
216
303
  - async getDailyReflections({ userIds, cursor, lastId, limit } = {}) | scopes: dailyReflections:read
304
+ - Returns: { reflections: [{ id, userId, response, questionId, submittedAt, sharedAt, username, profilePicUrl, question }], cursor? }
217
305
  - async getDailyReflectionsByUser(userId, { cursor, lastId, limit } = {}) | scopes: dailyReflections:read
306
+ - Returns: { reflections: [{ id, userId, response, questionId, submittedAt, sharedAt, username, profilePicUrl, question }], cursor? }
218
307
 
219
308
  ### Twinkle.privateDb
220
309
  - async get(key) | scopes: privateDb:read
310
+ - Returns: { item: { id, key, value, updatedAt } | null }
221
311
  - Read one key from the default private per-user JSON store.
222
312
  - async list({ prefix, limit, cursor } = {}) | scopes: privateDb:read
313
+ - Returns: { items: [{ id, key, value, updatedAt }], cursor? }
223
314
  - List keys from the default private per-user JSON store.
224
315
  - async set(key, value) | scopes: privateDb:write
316
+ - Returns: { item: { id, key, value, updatedAt } }
225
317
  - Upsert one JSON-serializable value in the default private per-user store.
226
318
  - async remove(key) | scopes: privateDb:write
319
+ - Returns: { success: true, deleted: boolean }
227
320
  - Delete one key from the default private per-user JSON store.
228
321
 
229
322
  ### Twinkle.reminders
230
323
  - async list({ includeDisabled, limit } = {}) | scopes: reminders:read
324
+ - Returns: { reminders: [{ id, buildId, userId, title, body, targetPath, payload, isEnabled, schedule, lastTriggeredAt, createdAt, updatedAt }] }
231
325
  - async create({ title, body, targetPath, payload, schedule, isEnabled }) | scopes: reminders:write
326
+ - Returns: { reminder: { id, buildId, userId, title, body, targetPath, payload, isEnabled, schedule, lastTriggeredAt, createdAt, updatedAt } | null }
232
327
  - async update(reminderId, patch) | scopes: reminders:write
328
+ - Returns: { reminder: { id, buildId, userId, title, body, targetPath, payload, isEnabled, schedule, lastTriggeredAt, createdAt, updatedAt } | null }
233
329
  - async remove(reminderId) | scopes: reminders:write
330
+ - Returns: { success: true, deleted: boolean }
234
331
  - async getDue({ now, autoAcknowledge, limit } = {}) | scopes: reminders:read
332
+ - Returns: { now, reminders: [{ id, buildId, userId, title, body, targetPath, payload, isEnabled, schedule, lastTriggeredAt, createdAt, updatedAt }] }
235
333
 
236
334
  ## Examples
237
335