@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 +6 -5
- package/bin/lumine.js +58 -3
- package/package.json +2 -1
- package/sdk/BUILD_SDK_INDEX.md +454 -0
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,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
instead of
|
|
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
|
|
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,
|
|
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.
|
|
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
|
+
```
|