@stage5/lumine 0.1.4 → 0.1.6
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 -0
- package/bin/lumine.js +154 -1
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +54 -6
package/README.md
CHANGED
|
@@ -49,6 +49,12 @@ Reference folders are marked `readOnly` in `.twinkle/lumine-project.json`.
|
|
|
49
49
|
Running `lumine save` from a reference folder is blocked; fork the source Build
|
|
50
50
|
first if you want an editable workspace.
|
|
51
51
|
|
|
52
|
+
The CLI checks npm for the latest `@stage5/lumine` version on normal commands.
|
|
53
|
+
If the installed copy is outdated, it prints an update warning and records the
|
|
54
|
+
version state in `.twinkle/lumine-project.json` so local agents can tell when
|
|
55
|
+
they should rerun with `npx @stage5/lumine@latest`. Use `--no-update-check` to
|
|
56
|
+
skip that advisory network check.
|
|
57
|
+
|
|
52
58
|
After pulling a project, run an agent from the pulled folder:
|
|
53
59
|
|
|
54
60
|
```bash
|
package/bin/lumine.js
CHANGED
|
@@ -8,12 +8,14 @@ import { stdin as input, stdout as output } from "process";
|
|
|
8
8
|
|
|
9
9
|
const DEFAULT_API_URL = "https://api.twinkle.network";
|
|
10
10
|
const DEFAULT_SITE_URL = "https://www.twin-kle.com";
|
|
11
|
+
const DEFAULT_NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
11
12
|
const DEFAULT_AUTH_FILE = path.join(
|
|
12
13
|
os.homedir(),
|
|
13
14
|
".twinkle",
|
|
14
15
|
"lumine-cli-auth.json",
|
|
15
16
|
);
|
|
16
17
|
const DEFAULT_TIMEOUT_MS = 20000;
|
|
18
|
+
const UPDATE_CHECK_TIMEOUT_MS = 1500;
|
|
17
19
|
const DEFAULT_PROJECT_LIMIT = 50;
|
|
18
20
|
const PROJECT_METADATA_DIR = ".twinkle";
|
|
19
21
|
const PROJECT_METADATA_FILE = "lumine-project.json";
|
|
@@ -35,6 +37,7 @@ const BUNDLED_SDK_REFERENCE_URL = new URL(
|
|
|
35
37
|
"../sdk/BUILD_SDK_INDEX.md",
|
|
36
38
|
import.meta.url,
|
|
37
39
|
);
|
|
40
|
+
const PACKAGE_METADATA_URL = new URL("../package.json", import.meta.url);
|
|
38
41
|
const SDK_REFERENCE_FALLBACK = `${LUMINE_SDK_REFERENCE_MARKER}
|
|
39
42
|
# Twinkle Build SDK Reference
|
|
40
43
|
|
|
@@ -47,6 +50,8 @@ Use these current source-of-truth rules:
|
|
|
47
50
|
- Use Twinkle.privateDb for simple private per-user preferences, drafts, settings, and small JSON state.
|
|
48
51
|
- Use Twinkle.userDb only for advanced private SQLite tables, indexes, many rows, filtered queries, or aggregates.
|
|
49
52
|
- Use Twinkle.sharedDb for shared multi-user JSON data.
|
|
53
|
+
- Use Twinkle.aiCards.list/search/get for existing public AI Card words, exampleText sentence material, and word levels.
|
|
54
|
+
- Use Twinkle.aiStories.list/search/get for existing AI Story passage text, story media, and questions.
|
|
50
55
|
- Use Twinkle.ai.chat with history entries shaped as { role, content }, not { text }.
|
|
51
56
|
- Use Twinkle.preview for canvas, WebGL, Three.js, fullscreen, and game layout.
|
|
52
57
|
- Prefer existing documented Twinkle.* methods over guessing names from old code.
|
|
@@ -61,6 +66,7 @@ Lumine CLI as the source of truth for saving this workspace back to Twinkle.
|
|
|
61
66
|
|
|
62
67
|
- Read .twinkle/lumine-project.json before changing files.
|
|
63
68
|
- Treat build.canWrite, build.canPublish, and build.contributionRootBuildId as authoritative.
|
|
69
|
+
- If lumineCli.updateAvailable is true, ask the user to rerun with npx @stage5/lumine@latest before saving.
|
|
64
70
|
- Read ${SDK_REFERENCE_FILE} before adding, removing, or changing any Twinkle.* SDK calls.
|
|
65
71
|
- If build.canWrite is false, do not save changes.
|
|
66
72
|
- 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.
|
|
@@ -102,6 +108,7 @@ the workspace to save.
|
|
|
102
108
|
## Source Of Truth
|
|
103
109
|
|
|
104
110
|
- Read .twinkle/lumine-project.json before using these files.
|
|
111
|
+
- If lumineCli.updateAvailable is true, ask the user to rerun with npx @stage5/lumine@latest before borrowing patterns.
|
|
105
112
|
- If metadata.readOnly is true or build.role is "reference", do not run lumine save from this directory.
|
|
106
113
|
- To start from this Build, run lumine fork with the source build id and edit the forked workspace.
|
|
107
114
|
- Do not edit another local checkout to bypass reference read-only semantics.
|
|
@@ -132,10 +139,14 @@ main().catch((error) => {
|
|
|
132
139
|
|
|
133
140
|
async function main() {
|
|
134
141
|
const options = parseArgs(process.argv.slice(2));
|
|
142
|
+
options.lumineCli = await loadLumineCliVersionInfo({ options });
|
|
135
143
|
if (options.help) {
|
|
136
144
|
printHelp();
|
|
137
145
|
return;
|
|
138
146
|
}
|
|
147
|
+
if (options.updateCheck) {
|
|
148
|
+
await maybeCheckForLumineCliUpdate({ options });
|
|
149
|
+
}
|
|
139
150
|
|
|
140
151
|
if (options.command === "workspace") {
|
|
141
152
|
await workspace(options);
|
|
@@ -1117,6 +1128,7 @@ async function writeProjectMetadata({
|
|
|
1117
1128
|
: null,
|
|
1118
1129
|
apiUrl: options.apiUrl,
|
|
1119
1130
|
siteUrl: options.siteUrl,
|
|
1131
|
+
lumineCli: serializeLumineCliMetadata(options),
|
|
1120
1132
|
manifest,
|
|
1121
1133
|
pulledAt,
|
|
1122
1134
|
lastSavedAt,
|
|
@@ -1164,6 +1176,7 @@ async function writeReferenceMetadata({
|
|
|
1164
1176
|
},
|
|
1165
1177
|
apiUrl: options.apiUrl,
|
|
1166
1178
|
siteUrl: options.siteUrl,
|
|
1179
|
+
lumineCli: serializeLumineCliMetadata(options),
|
|
1167
1180
|
manifest,
|
|
1168
1181
|
pulledAt,
|
|
1169
1182
|
},
|
|
@@ -1258,6 +1271,20 @@ async function saveSelectedBuild({ options, auth, build }) {
|
|
|
1258
1271
|
});
|
|
1259
1272
|
}
|
|
1260
1273
|
|
|
1274
|
+
function serializeLumineCliMetadata(options) {
|
|
1275
|
+
const info = options.lumineCli || {};
|
|
1276
|
+
return {
|
|
1277
|
+
packageName: info.packageName || "@stage5/lumine",
|
|
1278
|
+
version: info.version || null,
|
|
1279
|
+
latestVersion: info.latestVersion || null,
|
|
1280
|
+
updateAvailable: Boolean(info.updateAvailable),
|
|
1281
|
+
updateCommand: info.updateCommand || "npx @stage5/lumine@latest",
|
|
1282
|
+
checkedAt: info.checkedAt || null,
|
|
1283
|
+
checkFailed: Boolean(info.checkFailed),
|
|
1284
|
+
checkSkipped: Boolean(info.checkSkipped),
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1261
1288
|
function printBuildList(builds) {
|
|
1262
1289
|
if (!builds.length) {
|
|
1263
1290
|
console.log("No owned or team Twinkle builds found.");
|
|
@@ -1503,6 +1530,117 @@ async function request({ method = "GET", url, authToken, body, timeoutMs }) {
|
|
|
1503
1530
|
}
|
|
1504
1531
|
}
|
|
1505
1532
|
|
|
1533
|
+
async function loadLumineCliVersionInfo({ options }) {
|
|
1534
|
+
const packageMetadata = await loadLocalPackageMetadata();
|
|
1535
|
+
const packageName = packageMetadata.name || "@stage5/lumine";
|
|
1536
|
+
const version = packageMetadata.version || null;
|
|
1537
|
+
return {
|
|
1538
|
+
packageName,
|
|
1539
|
+
version,
|
|
1540
|
+
latestVersion: null,
|
|
1541
|
+
updateAvailable: false,
|
|
1542
|
+
updateCommand: `npx ${packageName}@latest`,
|
|
1543
|
+
checkedAt: null,
|
|
1544
|
+
checkFailed: false,
|
|
1545
|
+
checkSkipped: !options.updateCheck,
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
async function loadLocalPackageMetadata() {
|
|
1550
|
+
try {
|
|
1551
|
+
const rawPackage = await fs.readFile(PACKAGE_METADATA_URL, "utf8");
|
|
1552
|
+
const parsedPackage = JSON.parse(rawPackage);
|
|
1553
|
+
return {
|
|
1554
|
+
name: String(parsedPackage?.name || "").trim(),
|
|
1555
|
+
version: String(parsedPackage?.version || "").trim(),
|
|
1556
|
+
};
|
|
1557
|
+
} catch {
|
|
1558
|
+
return {
|
|
1559
|
+
name: "@stage5/lumine",
|
|
1560
|
+
version: null,
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
async function maybeCheckForLumineCliUpdate({ options }) {
|
|
1566
|
+
const info = options.lumineCli || (await loadLumineCliVersionInfo({ options }));
|
|
1567
|
+
const checkedAt = new Date().toISOString();
|
|
1568
|
+
if (!info.packageName || !info.version) {
|
|
1569
|
+
options.lumineCli = {
|
|
1570
|
+
...info,
|
|
1571
|
+
checkedAt,
|
|
1572
|
+
checkFailed: true,
|
|
1573
|
+
checkSkipped: false,
|
|
1574
|
+
};
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
try {
|
|
1579
|
+
const latestVersion = await loadLatestPackageVersion({
|
|
1580
|
+
packageName: info.packageName,
|
|
1581
|
+
registryUrl: options.npmRegistryUrl,
|
|
1582
|
+
});
|
|
1583
|
+
const updateAvailable = isNewerVersion(latestVersion, info.version);
|
|
1584
|
+
options.lumineCli = {
|
|
1585
|
+
...info,
|
|
1586
|
+
latestVersion,
|
|
1587
|
+
updateAvailable,
|
|
1588
|
+
checkedAt,
|
|
1589
|
+
checkFailed: false,
|
|
1590
|
+
checkSkipped: false,
|
|
1591
|
+
};
|
|
1592
|
+
if (updateAvailable) {
|
|
1593
|
+
printLumineCliUpdateWarning(options.lumineCli);
|
|
1594
|
+
}
|
|
1595
|
+
} catch {
|
|
1596
|
+
options.lumineCli = {
|
|
1597
|
+
...info,
|
|
1598
|
+
checkedAt,
|
|
1599
|
+
checkFailed: true,
|
|
1600
|
+
checkSkipped: false,
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
async function loadLatestPackageVersion({ packageName, registryUrl }) {
|
|
1606
|
+
const encodedPackageName = encodeURIComponent(packageName);
|
|
1607
|
+
const result = await requestJson({
|
|
1608
|
+
url: `${registryUrl}/${encodedPackageName}/latest`,
|
|
1609
|
+
timeoutMs: UPDATE_CHECK_TIMEOUT_MS,
|
|
1610
|
+
});
|
|
1611
|
+
const latestVersion = String(result?.version || "").trim();
|
|
1612
|
+
if (!latestVersion) {
|
|
1613
|
+
throw new Error("No latest package version returned");
|
|
1614
|
+
}
|
|
1615
|
+
return latestVersion;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function isNewerVersion(latestVersion, currentVersion) {
|
|
1619
|
+
const latestParts = parseSemverParts(latestVersion);
|
|
1620
|
+
const currentParts = parseSemverParts(currentVersion);
|
|
1621
|
+
if (!latestParts || !currentParts) return false;
|
|
1622
|
+
for (let index = 0; index < 3; index += 1) {
|
|
1623
|
+
if (latestParts[index] > currentParts[index]) return true;
|
|
1624
|
+
if (latestParts[index] < currentParts[index]) return false;
|
|
1625
|
+
}
|
|
1626
|
+
return false;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function parseSemverParts(value) {
|
|
1630
|
+
const match = String(value || "")
|
|
1631
|
+
.trim()
|
|
1632
|
+
.match(/^v?(\d+)\.(\d+)\.(\d+)/);
|
|
1633
|
+
if (!match) return null;
|
|
1634
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function printLumineCliUpdateWarning(info) {
|
|
1638
|
+
console.error(
|
|
1639
|
+
`lumine: update available for ${info.packageName}: ${info.version} -> ${info.latestVersion}.`,
|
|
1640
|
+
);
|
|
1641
|
+
console.error(`lumine: run \`${info.updateCommand}\` to use the latest CLI.`);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1506
1644
|
function parseArgs(args) {
|
|
1507
1645
|
const firstArg = args[0] || "";
|
|
1508
1646
|
const firstArgIsCommand =
|
|
@@ -1517,7 +1655,13 @@ function parseArgs(args) {
|
|
|
1517
1655
|
const rest = command === "workspace" ? args : args.slice(1);
|
|
1518
1656
|
const raw = {};
|
|
1519
1657
|
const positional = [];
|
|
1520
|
-
const booleanFlags = new Set([
|
|
1658
|
+
const booleanFlags = new Set([
|
|
1659
|
+
"noOpen",
|
|
1660
|
+
"open",
|
|
1661
|
+
"publish",
|
|
1662
|
+
"save",
|
|
1663
|
+
"noUpdateCheck",
|
|
1664
|
+
]);
|
|
1521
1665
|
|
|
1522
1666
|
for (let i = 0; i < rest.length; i += 1) {
|
|
1523
1667
|
const arg = rest[i];
|
|
@@ -1557,6 +1701,13 @@ function parseArgs(args) {
|
|
|
1557
1701
|
siteUrl: trimTrailingSlash(
|
|
1558
1702
|
String(raw.siteUrl || process.env.TWINKLE_SITE_URL || DEFAULT_SITE_URL),
|
|
1559
1703
|
),
|
|
1704
|
+
npmRegistryUrl: trimTrailingSlash(
|
|
1705
|
+
String(
|
|
1706
|
+
raw.npmRegistryUrl ||
|
|
1707
|
+
process.env.LUMINE_NPM_REGISTRY_URL ||
|
|
1708
|
+
DEFAULT_NPM_REGISTRY_URL,
|
|
1709
|
+
),
|
|
1710
|
+
),
|
|
1560
1711
|
authFile: String(
|
|
1561
1712
|
raw.authFile || process.env.TWINKLE_CLI_AUTH_FILE || DEFAULT_AUTH_FILE,
|
|
1562
1713
|
),
|
|
@@ -1579,6 +1730,7 @@ function parseArgs(args) {
|
|
|
1579
1730
|
openBrowser: parseBoolean(raw.noOpen, false)
|
|
1580
1731
|
? false
|
|
1581
1732
|
: parseBoolean(raw.open, true),
|
|
1733
|
+
updateCheck: parseBoolean(raw.noUpdateCheck, false) ? false : true,
|
|
1582
1734
|
timeoutMs: Math.max(
|
|
1583
1735
|
Number(raw.timeoutMs || process.env.TWINKLE_TIMEOUT_MS) ||
|
|
1584
1736
|
DEFAULT_TIMEOUT_MS,
|
|
@@ -1802,6 +1954,7 @@ Options:
|
|
|
1802
1954
|
--publish Publish after saving
|
|
1803
1955
|
--save Save local files before launch
|
|
1804
1956
|
--limit <number> Number of projects to show
|
|
1957
|
+
--no-update-check Skip the npm latest-version check
|
|
1805
1958
|
--no-open Print the approval URL without opening a browser
|
|
1806
1959
|
`);
|
|
1807
1960
|
}
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build SDK Index
|
|
2
2
|
|
|
3
|
-
Version: 1.
|
|
4
|
-
Updated: 2026-05-
|
|
5
|
-
Generated: 2026-05-
|
|
3
|
+
Version: 1.25.0
|
|
4
|
+
Updated: 2026-05-28
|
|
5
|
+
Generated: 2026-05-28T09:34:12.631Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -13,6 +13,7 @@ Generated: 2026-05-26T10:20:06.718Z
|
|
|
13
13
|
- Use Twinkle.leaderboards for public Build scoreboards. Signed-in viewers are ranked by Twinkle username; guests can submit with a display name.
|
|
14
14
|
- Use Twinkle.sharedDb for custom shared multi-user structured data, guestbooks, votes, and append-only run history.
|
|
15
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.aiCards for read-only existing public AI Card words and example texts, including word levels for typing games.
|
|
16
17
|
- Use Twinkle.aiStories for read-only existing AI Story galleries, readers, quizzes, and remix tools.
|
|
17
18
|
- Use Twinkle.grammarbles for public Grammarbles question-bank trainer apps and optional signed-in viewer attempt-history filtering.
|
|
18
19
|
- Use Twinkle.chess for chess engine play and analysis; app code still owns chess rules, legal moves, board state, and UI.
|
|
@@ -200,18 +201,32 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
200
201
|
- async getSubjectComments(subjectId, { limit, cursor } = {}) | scopes: content:read
|
|
201
202
|
- Returns: { comments: [{ id, content, filePath, fileName, fileSize, thumbUrl, timeStamp }], cursor? }
|
|
202
203
|
|
|
204
|
+
### Twinkle.aiCards
|
|
205
|
+
- async list({ limit, cursor, level, minLevel, maxLevel, quality, userId, hasImage, hasExample } = {}) | scopes: content:read
|
|
206
|
+
- Returns: { cards: [{ id, contentType, contentId, word, text, exampleText, prompt, level, wordLevel, quality, style, imagePath, imageUrl, creatorId, ownerId, username, profilePicUrl, timeStamp, lastInteraction }], cursor?, pagination: { limit, hasMore, nextCursor }, filters }
|
|
207
|
+
- List existing public AI Cards newest first, including each card word, example sentence text, word level, and quality.
|
|
208
|
+
- Example: const { cards } = await Twinkle.aiCards.list({ level: 2, hasExample: true, limit: 20 });
|
|
209
|
+
- async search({ query, limit, cursor, level, minLevel, maxLevel, quality, userId, hasImage, hasExample } = {}) | scopes: content:read
|
|
210
|
+
- Returns: { cards: [{ id, contentType, contentId, word, text, exampleText, prompt, level, wordLevel, quality, style, imagePath, imageUrl, creatorId, ownerId, username, profilePicUrl, timeStamp, lastInteraction }], cursor?, pagination: { limit, hasMore, nextCursor }, filters }
|
|
211
|
+
- Search existing public AI Cards by word, with optional level and quality filters.
|
|
212
|
+
- Example: const { cards } = await Twinkle.aiCards.search({ query: searchText, minLevel: 1, maxLevel: 3, hasExample: true, limit: 12 });
|
|
213
|
+
- async get(cardId) | scopes: content:read
|
|
214
|
+
- Returns: { card: { id, contentType, contentId, word, text, exampleText, prompt, level, wordLevel, quality, style, imagePath, imageUrl, creatorId, ownerId, username, profilePicUrl, timeStamp, lastInteraction } }
|
|
215
|
+
- Fetch one existing public AI Card by id, including word, example text, and level metadata.
|
|
216
|
+
- Example: const { card } = await Twinkle.aiCards.get(cardId);
|
|
217
|
+
|
|
203
218
|
### Twinkle.aiStories
|
|
204
219
|
- async list({ limit, cursor, difficulty, type, isListening, userId, hasImage, hasQuestions } = {}) | scopes: content:read
|
|
205
220
|
- 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 }
|
|
206
|
-
- List completed existing user-generated AI Stories newest first for galleries, quiz apps, readers, and image/story collections.
|
|
221
|
+
- List completed existing user-generated AI Stories newest first for galleries, quiz apps, readers, passage typing, and image/story collections.
|
|
207
222
|
- Example: const { stories } = await Twinkle.aiStories.list({ hasImage: true, hasQuestions: true, limit: 12 });
|
|
208
223
|
- async search({ query, limit, cursor, difficulty, type, isListening, userId, hasImage, hasQuestions } = {}) | scopes: content:read
|
|
209
224
|
- 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 }
|
|
210
|
-
- Search completed existing user-generated AI Stories by topic or story text for galleries, quizzes, and
|
|
225
|
+
- Search completed existing user-generated AI Stories by topic or story text for galleries, quizzes, readers, and passage typing games.
|
|
211
226
|
- Example: const { stories } = await Twinkle.aiStories.search({ query: searchText, hasQuestions: true, limit: 12 });
|
|
212
227
|
- async get(storyId) | scopes: content:read
|
|
213
228
|
- Returns: { story: { id, contentType, contentId, topic, topicKey, type, story, explanation, difficulty, isListening, imagePath, imageUrl, audioPath, audioUrl, questions, questionsBy, hasImage, hasQuestions, userId, username, profilePicUrl, timeStamp } }
|
|
214
|
-
- Fetch one completed existing AI Story by id, including story text, media URLs, and normalized questions when available.
|
|
229
|
+
- Fetch one completed existing AI Story by id, including story text for passage typing, media URLs, and normalized questions when available.
|
|
215
230
|
- Example: const { story } = await Twinkle.aiStories.get(storyId);
|
|
216
231
|
|
|
217
232
|
### Twinkle.grammarbles
|
|
@@ -302,8 +317,10 @@ world.updatePresence({ x, y, z, facing });
|
|
|
302
317
|
### Twinkle.reflections
|
|
303
318
|
- async getDailyReflections({ userIds, cursor, lastId, limit } = {}) | scopes: dailyReflections:read
|
|
304
319
|
- Returns: { reflections: [{ id, userId, response, questionId, submittedAt, sharedAt, username, profilePicUrl, question }], cursor? }
|
|
320
|
+
- Read only currently public Daily Reflection shares. response is the exact text the author chose to share publicly, never an unshared raw/private answer.
|
|
305
321
|
- async getDailyReflectionsByUser(userId, { cursor, lastId, limit } = {}) | scopes: dailyReflections:read
|
|
306
322
|
- Returns: { reflections: [{ id, userId, response, questionId, submittedAt, sharedAt, username, profilePicUrl, question }], cursor? }
|
|
323
|
+
- Read one user's currently public Daily Reflection shares. response is the exact text the author chose to share publicly, never an unshared raw/private answer.
|
|
307
324
|
|
|
308
325
|
### Twinkle.privateDb
|
|
309
326
|
- async get(key) | scopes: privateDb:read
|
|
@@ -385,6 +402,37 @@ const { comments, pagination } = await Twinkle.subjectComments.list(subjectId, {
|
|
|
385
402
|
console.log('Title:', subject.title, 'Pages:', comments.length, 'hasMore:', pagination?.hasMore);
|
|
386
403
|
```
|
|
387
404
|
|
|
405
|
+
### Typing game content from AI Cards and AI Stories
|
|
406
|
+
Use AI Card words for word mode, AI Card exampleText for sentence mode, and AI Story story text for passage mode. Keep leaderboards separate per mode.
|
|
407
|
+
Keywords: typing, ai cards, ai stories, words, sentences, passages, leaderboard
|
|
408
|
+
|
|
409
|
+
```js
|
|
410
|
+
const wordPage = await Twinkle.aiCards.list({ level: 1, hasExample: true, limit: 20 });
|
|
411
|
+
const wordTargets = wordPage.cards.map((card) => ({
|
|
412
|
+
text: card.word,
|
|
413
|
+
level: card.level,
|
|
414
|
+
sourceId: card.id
|
|
415
|
+
}));
|
|
416
|
+
const sentenceTargets = wordPage.cards.map((card) => ({
|
|
417
|
+
text: card.exampleText,
|
|
418
|
+
level: card.level,
|
|
419
|
+
sourceId: card.id
|
|
420
|
+
}));
|
|
421
|
+
|
|
422
|
+
const passagePage = await Twinkle.aiStories.list({ difficulty: 2, limit: 10 });
|
|
423
|
+
const passageTargets = passagePage.stories.map((story) => ({
|
|
424
|
+
text: story.story,
|
|
425
|
+
level: story.difficulty,
|
|
426
|
+
sourceId: story.id
|
|
427
|
+
}));
|
|
428
|
+
|
|
429
|
+
await Twinkle.leaderboards.submit({
|
|
430
|
+
boardKey: 'arcade-typing-words',
|
|
431
|
+
score: finalScore,
|
|
432
|
+
meta: { mode: 'words', level }
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
388
436
|
### Search AI Stories for a quiz app
|
|
389
437
|
Use existing user-generated AI Stories as source material for visual galleries, readers, and question-based games.
|
|
390
438
|
Keywords: ai stories, story, quiz, questions, image, gallery
|