@stage5/lumine 0.1.7 → 0.1.9
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 +9 -0
- package/bin/lumine.js +102 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +22 -10
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@ Launch Twinkle Lumine builds from any terminal.
|
|
|
5
5
|
```bash
|
|
6
6
|
npx @stage5/lumine@latest
|
|
7
7
|
npx @stage5/lumine@latest login
|
|
8
|
+
npx @stage5/lumine@latest new "Daily Reflection App"
|
|
9
|
+
npx @stage5/lumine@latest new --title "Daily Reflection App" --description "Private journal with streaks"
|
|
8
10
|
npx @stage5/lumine@latest projects
|
|
9
11
|
npx @stage5/lumine@latest explore --sort forks
|
|
10
12
|
npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
|
|
@@ -19,6 +21,13 @@ Run `lumine` with no subcommand for the easiest flow: sign in when needed,
|
|
|
19
21
|
choose one of your owned or team projects, and pull the saved project files into
|
|
20
22
|
a local folder.
|
|
21
23
|
|
|
24
|
+
Use `lumine new` to create a new Twinkle Build with the same website create
|
|
25
|
+
route used by Build Studio. The CLI asks for a title when one is not passed and
|
|
26
|
+
asks for an optional description. It creates the Build and pulls an editable
|
|
27
|
+
local workspace, but it does not auto-start a Lumine greeting or prompt run, so
|
|
28
|
+
creating a project from the CLI does not spend AI battery. Add project files
|
|
29
|
+
locally, including `/index.html`, then run `lumine save`.
|
|
30
|
+
|
|
22
31
|
For team projects, Lumine mirrors the website workspace flow: choosing or
|
|
23
32
|
pulling the owner's main project creates or reuses your contribution branch and
|
|
24
33
|
checks out that branch locally. Saves go to your branch, so the project owner
|
package/bin/lumine.js
CHANGED
|
@@ -119,6 +119,7 @@ const COMMANDS = new Set([
|
|
|
119
119
|
"login",
|
|
120
120
|
"logout",
|
|
121
121
|
"whoami",
|
|
122
|
+
"new",
|
|
122
123
|
"projects",
|
|
123
124
|
"explore",
|
|
124
125
|
"select",
|
|
@@ -167,6 +168,10 @@ async function main() {
|
|
|
167
168
|
await whoami(options);
|
|
168
169
|
return;
|
|
169
170
|
}
|
|
171
|
+
if (options.command === "new") {
|
|
172
|
+
await newBuild(options);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
170
175
|
if (options.command === "projects") {
|
|
171
176
|
await projects(options);
|
|
172
177
|
return;
|
|
@@ -314,6 +319,26 @@ async function whoami(options) {
|
|
|
314
319
|
);
|
|
315
320
|
}
|
|
316
321
|
|
|
322
|
+
async function newBuild(options) {
|
|
323
|
+
const title = await resolveNewBuildTitle(options);
|
|
324
|
+
const description = await resolveNewBuildDescription(options);
|
|
325
|
+
const auth = await ensureAuth(options);
|
|
326
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
327
|
+
const createResult = await createBuild({
|
|
328
|
+
options,
|
|
329
|
+
auth,
|
|
330
|
+
title,
|
|
331
|
+
description,
|
|
332
|
+
});
|
|
333
|
+
const buildId = Number(createResult.build?.id || 0);
|
|
334
|
+
if (!buildId) {
|
|
335
|
+
throw new Error("Twinkle did not return a created Build.");
|
|
336
|
+
}
|
|
337
|
+
const result = await pullBuildFiles({ options, auth, buildId });
|
|
338
|
+
await saveSelectedBuild({ options, auth, build: result.build });
|
|
339
|
+
printNewBuildResult({ createResult, pullResult: result });
|
|
340
|
+
}
|
|
341
|
+
|
|
317
342
|
async function workspace(options) {
|
|
318
343
|
const auth = await ensureAuth(options);
|
|
319
344
|
const selectedBuild = options.target
|
|
@@ -774,6 +799,19 @@ async function forkBuild({ options, auth, buildId }) {
|
|
|
774
799
|
});
|
|
775
800
|
}
|
|
776
801
|
|
|
802
|
+
async function createBuild({ options, auth, title, description }) {
|
|
803
|
+
return await requestJson({
|
|
804
|
+
method: "POST",
|
|
805
|
+
url: `${options.apiUrl}/build/create`,
|
|
806
|
+
authToken: auth.token,
|
|
807
|
+
body: {
|
|
808
|
+
title,
|
|
809
|
+
...(description ? { description } : {}),
|
|
810
|
+
},
|
|
811
|
+
timeoutMs: options.timeoutMs,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
777
815
|
async function saveProjectFiles({ options, auth, buildId, files, summary }) {
|
|
778
816
|
return await requestJson({
|
|
779
817
|
method: "PUT",
|
|
@@ -1517,6 +1555,9 @@ function printPullResult(result) {
|
|
|
1517
1555
|
}
|
|
1518
1556
|
return;
|
|
1519
1557
|
}
|
|
1558
|
+
if (result.fileCount === 0) {
|
|
1559
|
+
console.log("No project files yet. Create /index.html before your first save.");
|
|
1560
|
+
}
|
|
1520
1561
|
console.log('Codex: codex "Read AGENTS.md, then make the requested change."');
|
|
1521
1562
|
console.log(
|
|
1522
1563
|
'Claude Code: claude "Read CLAUDE.md, then make the requested change."',
|
|
@@ -1529,6 +1570,12 @@ function printPullResult(result) {
|
|
|
1529
1570
|
}
|
|
1530
1571
|
}
|
|
1531
1572
|
|
|
1573
|
+
function printNewBuildResult({ createResult, pullResult }) {
|
|
1574
|
+
const build = pullResult.build || createResult.build || {};
|
|
1575
|
+
console.log(`Created ${formatBuildTitle(build)}.`);
|
|
1576
|
+
printPullResult(pullResult);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1532
1579
|
function printReferenceResult(result) {
|
|
1533
1580
|
const build = result.build || {};
|
|
1534
1581
|
const sourceBuildId =
|
|
@@ -1868,6 +1915,7 @@ function parseArgs(args) {
|
|
|
1868
1915
|
const booleanFlags = new Set([
|
|
1869
1916
|
"noOpen",
|
|
1870
1917
|
"open",
|
|
1918
|
+
"noDescription",
|
|
1871
1919
|
"publish",
|
|
1872
1920
|
"save",
|
|
1873
1921
|
"noUpdateCheck",
|
|
@@ -1898,6 +1946,20 @@ function parseArgs(args) {
|
|
|
1898
1946
|
return {
|
|
1899
1947
|
command,
|
|
1900
1948
|
target: raw.url || raw.target || positional[0] || "",
|
|
1949
|
+
title:
|
|
1950
|
+
String(
|
|
1951
|
+
raw.title ||
|
|
1952
|
+
(command === "new" ? positional.join(" ") : ""),
|
|
1953
|
+
).trim() || "",
|
|
1954
|
+
description:
|
|
1955
|
+
Object.prototype.hasOwnProperty.call(raw, "description")
|
|
1956
|
+
? String(raw.description || "").trim()
|
|
1957
|
+
: null,
|
|
1958
|
+
descriptionProvided: Object.prototype.hasOwnProperty.call(
|
|
1959
|
+
raw,
|
|
1960
|
+
"description",
|
|
1961
|
+
),
|
|
1962
|
+
noDescription: parseBoolean(raw.noDescription, false),
|
|
1901
1963
|
searchQuery:
|
|
1902
1964
|
String(
|
|
1903
1965
|
raw.search ||
|
|
@@ -1990,6 +2052,40 @@ async function resolveRequiredBuildIdOrSelected(
|
|
|
1990
2052
|
);
|
|
1991
2053
|
}
|
|
1992
2054
|
|
|
2055
|
+
async function resolveNewBuildTitle(options) {
|
|
2056
|
+
const title = String(options.title || "").trim();
|
|
2057
|
+
if (title) return title;
|
|
2058
|
+
if (!input.isTTY || !output.isTTY) {
|
|
2059
|
+
throw new Error('Pass a title: `lumine new "My Build"` or `lumine new --title "My Build"`.');
|
|
2060
|
+
}
|
|
2061
|
+
const rl = readline.createInterface({ input, output });
|
|
2062
|
+
try {
|
|
2063
|
+
const answer = await rl.question("Build title: ");
|
|
2064
|
+
const normalized = String(answer || "").trim();
|
|
2065
|
+
if (!normalized) {
|
|
2066
|
+
throw new Error("Title is required.");
|
|
2067
|
+
}
|
|
2068
|
+
return normalized;
|
|
2069
|
+
} finally {
|
|
2070
|
+
rl.close();
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
async function resolveNewBuildDescription(options) {
|
|
2075
|
+
if (options.noDescription) return null;
|
|
2076
|
+
if (options.descriptionProvided) {
|
|
2077
|
+
return String(options.description || "").trim() || null;
|
|
2078
|
+
}
|
|
2079
|
+
if (!input.isTTY || !output.isTTY) return null;
|
|
2080
|
+
const rl = readline.createInterface({ input, output });
|
|
2081
|
+
try {
|
|
2082
|
+
const answer = await rl.question("Description (optional): ");
|
|
2083
|
+
return String(answer || "").trim() || null;
|
|
2084
|
+
} finally {
|
|
2085
|
+
rl.close();
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
|
|
1993
2089
|
function resolveRequiredBuildId(value) {
|
|
1994
2090
|
const buildId = resolveBuildReference(value).buildId;
|
|
1995
2091
|
if (buildId > 0) return buildId;
|
|
@@ -2170,6 +2266,7 @@ function printHelp() {
|
|
|
2170
2266
|
lumine login
|
|
2171
2267
|
lumine whoami
|
|
2172
2268
|
lumine logout
|
|
2269
|
+
lumine new [title]
|
|
2173
2270
|
lumine projects
|
|
2174
2271
|
lumine explore [search terms]
|
|
2175
2272
|
lumine select [twinkle-build-url]
|
|
@@ -2186,6 +2283,8 @@ function printHelp() {
|
|
|
2186
2283
|
Examples:
|
|
2187
2284
|
npx @stage5/lumine@latest
|
|
2188
2285
|
npx @stage5/lumine@latest login
|
|
2286
|
+
npx @stage5/lumine@latest new "Daily Reflection App"
|
|
2287
|
+
npx @stage5/lumine@latest new --title "Daily Reflection App" --description "Private journal with streaks"
|
|
2189
2288
|
npx @stage5/lumine@latest explore --sort forks
|
|
2190
2289
|
npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
|
|
2191
2290
|
npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
|
|
@@ -2203,6 +2302,9 @@ Options:
|
|
|
2203
2302
|
--auth-file <path> Saved login path
|
|
2204
2303
|
--auth-token <token> Override saved login
|
|
2205
2304
|
--dir <path> Directory for pulled project files
|
|
2305
|
+
--title <text> New Build title
|
|
2306
|
+
--description <text> Optional New Build description
|
|
2307
|
+
--no-description Skip the New Build description prompt
|
|
2206
2308
|
--summary <text> Save summary
|
|
2207
2309
|
--search <text> Search public open-source Builds
|
|
2208
2310
|
--sort <sort> Sort open-source Builds: forks, popular, recent
|
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-
|
|
5
|
-
Generated: 2026-
|
|
3
|
+
Version: 1.26.1
|
|
4
|
+
Updated: 2026-06-08
|
|
5
|
+
Generated: 2026-06-08T05:37:13.752Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -14,7 +14,7 @@ Generated: 2026-05-28T10:24:18.784Z
|
|
|
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
16
|
- Use Twinkle.aiCards for read-only existing public AI Card words and example texts, including word levels for typing games.
|
|
17
|
-
- Use Twinkle.aiStories for read-only existing AI Story galleries, readers, quizzes, and remix tools.
|
|
17
|
+
- Use Twinkle.aiStories for read-only existing AI Story galleries, readers, quizzes, topic chapter indexes, and remix tools.
|
|
18
18
|
- Use Twinkle.grammarbles for public Grammarbles question-bank trainer apps and optional signed-in viewer attempt-history filtering.
|
|
19
19
|
- Use Twinkle.chess for chess engine play and analysis; app code still owns chess rules, legal moves, board state, and UI.
|
|
20
20
|
- Use Twinkle.world for realtime multiplayer rooms, avatar presence, movement, emotes, and lightweight actions; keep durable MMO state in sharedDb/privateDb.
|
|
@@ -93,10 +93,18 @@ files:read, user:read, users:read, dailyReflections:read, content:read, sharedDb
|
|
|
93
93
|
- Returns: { subscription }
|
|
94
94
|
- Subscribe the current viewer to an app-defined notification channel target.
|
|
95
95
|
- Example: await Twinkle.notifications.subscribe('room.message', { targetKey: 'room:lobby', launchTarget: { view: 'room', roomId: 'lobby' } });
|
|
96
|
+
- async subscribeMany([{ channelKey, targetKey, launchTarget }]) | scopes: notifications:write
|
|
97
|
+
- Returns: { subscriptions }
|
|
98
|
+
- Subscribe the current viewer to multiple app-defined notification channel targets in one request.
|
|
99
|
+
- Example: await Twinkle.notifications.subscribeMany([{ channelKey: 'room.message', targetKey: 'room:lobby', launchTarget: { view: 'room', roomId: 'lobby' } }]);
|
|
96
100
|
- async unsubscribe(channelKey, { targetKey }) | scopes: notifications:write
|
|
97
101
|
- Returns: { subscription: null }
|
|
98
102
|
- Unsubscribe the current viewer from an app-defined notification channel target.
|
|
99
103
|
- Example: await Twinkle.notifications.unsubscribe('room.message', { targetKey: 'room:lobby' });
|
|
104
|
+
- async unsubscribeMany([{ channelKey, targetKey }]) | scopes: notifications:write
|
|
105
|
+
- Returns: { subscriptions: [], removed }
|
|
106
|
+
- Unsubscribe the current viewer from multiple app-defined notification channel targets in one request.
|
|
107
|
+
- Example: await Twinkle.notifications.unsubscribeMany([{ channelKey: 'room.message', targetKey: 'room:lobby' }]);
|
|
100
108
|
- async notifySubscribers(channelKey, { targetKey, eventKey, label, summary, launchTarget, payload }) | scopes: notifications:emit
|
|
101
109
|
- Returns: { sent }
|
|
102
110
|
- Notify viewers who opted into an app-defined channel target, without requiring a sharedDb write.
|
|
@@ -216,14 +224,18 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
216
224
|
- Example: const { card } = await Twinkle.aiCards.get(cardId);
|
|
217
225
|
|
|
218
226
|
### Twinkle.aiStories
|
|
219
|
-
- async list({ limit, cursor, difficulty, type, isListening, userId, hasImage, hasQuestions } = {}) | scopes: content:read
|
|
227
|
+
- async list({ limit, cursor, order, difficulty, type, topicKey, isListening, userId, hasImage, hasQuestions } = {}) | scopes: content:read
|
|
220
228
|
- 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 }
|
|
221
|
-
- List completed existing user-generated AI Stories
|
|
222
|
-
- Example: const { stories } = await Twinkle.aiStories.list({
|
|
223
|
-
- async
|
|
229
|
+
- List completed existing user-generated AI Stories, optionally filtered by exact level/type/topicKey book and ordered newest or oldest first.
|
|
230
|
+
- Example: const { stories } = await Twinkle.aiStories.list({ difficulty: 1, type: 'science', topicKey: 'Astronomy', order: 'oldest', limit: 20 });
|
|
231
|
+
- async chapters({ limit, cursor, difficulty, type, topicKey, isListening, userId, hasImage, hasQuestions } = {}) | scopes: content:read
|
|
232
|
+
- Returns: { chapters: [{ difficulty, type, topicKey, title, sampleTopic, storyCount, readingCount, listeningCount, imageCount, questionCount, latestStoryId, latestTimeStamp }], cursor?, pagination: { limit, hasMore, nextCursor }, filters }
|
|
233
|
+
- List server-built AI Story books grouped by level, type, and topic, with counts and navigation metadata but no story bodies.
|
|
234
|
+
- Example: const page = await Twinkle.aiStories.chapters({ limit: 200 });
|
|
235
|
+
- async search({ query, limit, cursor, order, difficulty, type, topicKey, isListening, userId, hasImage, hasQuestions } = {}) | scopes: content:read
|
|
224
236
|
- 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 }
|
|
225
|
-
- Search completed existing user-generated AI Stories by topic or story text
|
|
226
|
-
- Example: const { stories } = await Twinkle.aiStories.search({ query: searchText,
|
|
237
|
+
- Search completed existing user-generated AI Stories by topic or story text, optionally within an exact level/type/topicKey book.
|
|
238
|
+
- Example: const { stories } = await Twinkle.aiStories.search({ query: searchText, difficulty: 2, type: 'history', topicKey: 'Ancient Rome', order: 'oldest', limit: 12 });
|
|
227
239
|
- async get(storyId) | scopes: content:read
|
|
228
240
|
- Returns: { story: { id, contentType, contentId, topic, topicKey, type, story, explanation, difficulty, isListening, imagePath, imageUrl, audioPath, audioUrl, questions, questionsBy, hasImage, hasQuestions, userId, username, profilePicUrl, timeStamp } }
|
|
229
241
|
- Fetch one completed existing AI Story by id, including story text for passage typing, media URLs, and normalized questions when available.
|