@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 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(["noOpen", "open", "publish", "save"]);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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.24.0
4
- Updated: 2026-05-26
5
- Generated: 2026-05-26T10:20:06.718Z
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 readers.
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