@stage5/lumine 0.2.16 → 0.2.18
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 +20 -0
- package/lib/api.js +68 -0
- package/lib/commands.js +425 -19
- package/lib/constants.js +14 -1
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +61 -21
package/README.md
CHANGED
|
@@ -9,8 +9,11 @@ npx @stage5/lumine@latest new "Daily Reflection App"
|
|
|
9
9
|
npx @stage5/lumine@latest new --title "Daily Reflection App" --description "Private journal with streaks"
|
|
10
10
|
npx @stage5/lumine@latest rename "My New Build Title"
|
|
11
11
|
npx @stage5/lumine@latest rename "New Title" --target 123
|
|
12
|
+
npx @stage5/lumine@latest describe "A welcoming place to build together"
|
|
13
|
+
npx @stage5/lumine@latest describe --no-description --target 123
|
|
12
14
|
npx @stage5/lumine@latest projects
|
|
13
15
|
npx @stage5/lumine@latest branches 884
|
|
16
|
+
npx @stage5/lumine@latest suggestions 884
|
|
14
17
|
npx @stage5/lumine@latest explore --sort forks
|
|
15
18
|
npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
|
|
16
19
|
npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
|
|
@@ -31,6 +34,12 @@ local workspace, but it does not auto-start a Lumine greeting or prompt run, so
|
|
|
31
34
|
creating a project from the CLI does not spend AI battery. Add project files
|
|
32
35
|
locally, including `/index.html`, then run `lumine save`.
|
|
33
36
|
|
|
37
|
+
Use `lumine rename` to change an existing Build title and `lumine describe` to
|
|
38
|
+
change its description through the same canonical metadata route as the
|
|
39
|
+
website. Both commands target the current workspace or selected Build; pass
|
|
40
|
+
`--target <build-url-or-id>` to update another Build you own. Run
|
|
41
|
+
`lumine describe --no-description` to clear a description explicitly.
|
|
42
|
+
|
|
34
43
|
For team projects, Lumine mirrors the website workspace flow: choosing or
|
|
35
44
|
pulling the owner's main project creates or reuses your contribution branch and
|
|
36
45
|
checks out that branch locally. Saves go to your branch, so the project owner
|
|
@@ -40,6 +49,17 @@ Use `lumine branches <build-url-or-id>` to list the contribution branches you
|
|
|
40
49
|
can review, including each contributor, branch number, status, and URL. Then use
|
|
41
50
|
`lumine diff <branch-url>` to inspect one branch.
|
|
42
51
|
|
|
52
|
+
Branch contributors can nudge the project owner from their pulled branch with
|
|
53
|
+
`lumine suggest branch [message]` or `lumine suggest thumbnail`. The thumbnail
|
|
54
|
+
command offers the thumbnail currently saved on that branch. Project owners can
|
|
55
|
+
run `lumine suggestions <build-url-or-id>` to see their open suggestion inbox.
|
|
56
|
+
The inbox prints canonical follow-up commands for merging or replacing Main and
|
|
57
|
+
for applying the exact frozen thumbnail shown in a suggestion. The same actions
|
|
58
|
+
are also available directly as `lumine suggestions merge <id>`,
|
|
59
|
+
`lumine suggestions replace-main <id>`, and
|
|
60
|
+
`lumine suggestions adopt-thumbnail <id>`. Large inboxes are cursor-paginated;
|
|
61
|
+
the CLI prints the exact `--cursor` command for the next page.
|
|
62
|
+
|
|
43
63
|
Use `lumine explore` to list public open-source Build apps that can be used as
|
|
44
64
|
examples or starting points. It supports `--search` and `--sort forks`,
|
|
45
65
|
`--sort popular`, or `--sort recent`. Use `lumine reference <build-url-or-id>`
|
package/lib/api.js
CHANGED
|
@@ -189,6 +189,74 @@ export async function loadContributionDiff({
|
|
|
189
189
|
});
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
export async function listBuildSuggestions({
|
|
193
|
+
options,
|
|
194
|
+
auth,
|
|
195
|
+
rootBuildId,
|
|
196
|
+
cursor,
|
|
197
|
+
suggestionId,
|
|
198
|
+
}) {
|
|
199
|
+
const query = new URLSearchParams();
|
|
200
|
+
if (Number(cursor || 0) > 0) query.set("cursor", String(cursor));
|
|
201
|
+
if (Number(suggestionId || 0) > 0) {
|
|
202
|
+
query.set("suggestionId", String(suggestionId));
|
|
203
|
+
}
|
|
204
|
+
const queryString = query.toString();
|
|
205
|
+
return await requestJson({
|
|
206
|
+
url: `${options.apiUrl}/build/${rootBuildId}/suggestions${queryString ? `?${queryString}` : ""}`,
|
|
207
|
+
authToken: auth.token,
|
|
208
|
+
timeoutMs: options.timeoutMs,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function notifyBuildOwnerOfContribution({
|
|
213
|
+
options,
|
|
214
|
+
auth,
|
|
215
|
+
rootBuildId,
|
|
216
|
+
contributionBuildId,
|
|
217
|
+
note,
|
|
218
|
+
}) {
|
|
219
|
+
return await requestJson({
|
|
220
|
+
method: "POST",
|
|
221
|
+
url: `${options.apiUrl}/build/${rootBuildId}/contributions/${contributionBuildId}/notify-owner`,
|
|
222
|
+
authToken: auth.token,
|
|
223
|
+
body: { note },
|
|
224
|
+
timeoutMs: options.timeoutMs,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function suggestBuildThumbnailToOwner({
|
|
229
|
+
options,
|
|
230
|
+
auth,
|
|
231
|
+
rootBuildId,
|
|
232
|
+
contributionBuildId,
|
|
233
|
+
}) {
|
|
234
|
+
return await requestJson({
|
|
235
|
+
method: "POST",
|
|
236
|
+
url: `${options.apiUrl}/build/${rootBuildId}/contributions/${contributionBuildId}/suggest-thumbnail`,
|
|
237
|
+
authToken: auth.token,
|
|
238
|
+
body: {},
|
|
239
|
+
timeoutMs: options.timeoutMs,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function adoptBuildThumbnailSuggestion({
|
|
244
|
+
options,
|
|
245
|
+
auth,
|
|
246
|
+
rootBuildId,
|
|
247
|
+
contributionBuildId,
|
|
248
|
+
suggestionMessageId,
|
|
249
|
+
thumbnailUrl,
|
|
250
|
+
}) {
|
|
251
|
+
return await requestJson({
|
|
252
|
+
method: "POST",
|
|
253
|
+
url: `${options.apiUrl}/build/${rootBuildId}/contributions/${contributionBuildId}/adopt-thumbnail`,
|
|
254
|
+
authToken: auth.token,
|
|
255
|
+
body: { suggestionMessageId, thumbnailUrl },
|
|
256
|
+
timeoutMs: options.timeoutMs,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
192
260
|
export async function mergeContributionIntoMain({
|
|
193
261
|
options,
|
|
194
262
|
auth,
|
package/lib/commands.js
CHANGED
|
@@ -21,10 +21,12 @@ import {
|
|
|
21
21
|
SDK_REFERENCE_FILE,
|
|
22
22
|
} from "./constants.js";
|
|
23
23
|
import {
|
|
24
|
+
adoptBuildThumbnailSuggestion,
|
|
24
25
|
createBuild,
|
|
25
26
|
fetchAllRuntimeAssets,
|
|
26
27
|
forkBuild,
|
|
27
28
|
listBuilds,
|
|
29
|
+
listBuildSuggestions,
|
|
28
30
|
listOpenSourceBuilds,
|
|
29
31
|
loadBuildFiles,
|
|
30
32
|
loadBuildMetadata,
|
|
@@ -37,13 +39,19 @@ import {
|
|
|
37
39
|
maybeCheckForLumineCliUpdate,
|
|
38
40
|
mergeContributionIntoMain,
|
|
39
41
|
mintBuildApiToken,
|
|
42
|
+
notifyBuildOwnerOfContribution,
|
|
40
43
|
publishBuild,
|
|
41
44
|
replaceMainWithContribution,
|
|
42
45
|
resolveBranchBuild,
|
|
43
46
|
saveProjectFiles,
|
|
47
|
+
suggestBuildThumbnailToOwner,
|
|
44
48
|
updateBuildMetadata,
|
|
45
49
|
} from "./api.js";
|
|
46
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
assetsCommand,
|
|
52
|
+
confirmPrompt,
|
|
53
|
+
writeAssetsManifest,
|
|
54
|
+
} from "./assets.js";
|
|
47
55
|
import { thumbnailCommand } from "./thumbnail.js";
|
|
48
56
|
import {
|
|
49
57
|
assertAuthScope,
|
|
@@ -128,6 +136,10 @@ export async function main() {
|
|
|
128
136
|
await renameBuild(options);
|
|
129
137
|
return;
|
|
130
138
|
}
|
|
139
|
+
if (options.command === "describe") {
|
|
140
|
+
await describeBuild(options);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
131
143
|
if (options.command === "projects") {
|
|
132
144
|
await projects(options);
|
|
133
145
|
return;
|
|
@@ -136,6 +148,14 @@ export async function main() {
|
|
|
136
148
|
await branches(options);
|
|
137
149
|
return;
|
|
138
150
|
}
|
|
151
|
+
if (options.command === "suggest") {
|
|
152
|
+
await sendSuggestion(options);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (options.command === "suggestions") {
|
|
156
|
+
await suggestions(options);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
139
159
|
if (options.command === "explore") {
|
|
140
160
|
await explore(options);
|
|
141
161
|
return;
|
|
@@ -239,6 +259,36 @@ export async function renameBuild(options) {
|
|
|
239
259
|
'Pass a title: `lumine rename "My New Build Title"` or `lumine rename --title "My New Build Title"`.',
|
|
240
260
|
);
|
|
241
261
|
}
|
|
262
|
+
const { buildId, updatedBuild } = await updateOwnedBuildDetails({
|
|
263
|
+
options,
|
|
264
|
+
patch: { title },
|
|
265
|
+
permissionError: (id) => `You cannot rename Build #${id}.`,
|
|
266
|
+
failureMessage: "Failed to rename the Build.",
|
|
267
|
+
});
|
|
268
|
+
console.log(`Renamed Build #${buildId} to "${updatedBuild.title}".`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function describeBuild(options) {
|
|
272
|
+
const description = await resolveBuildDescriptionUpdate(options);
|
|
273
|
+
const { updatedBuild } = await updateOwnedBuildDetails({
|
|
274
|
+
options,
|
|
275
|
+
patch: { description },
|
|
276
|
+
permissionError: (id) => `You cannot update Build #${id}.`,
|
|
277
|
+
failureMessage: "Failed to update the Build description.",
|
|
278
|
+
});
|
|
279
|
+
console.log(
|
|
280
|
+
description
|
|
281
|
+
? `Updated description for ${formatBuildTitle(updatedBuild)}.`
|
|
282
|
+
: `Cleared description for ${formatBuildTitle(updatedBuild)}.`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function updateOwnedBuildDetails({
|
|
287
|
+
options,
|
|
288
|
+
patch,
|
|
289
|
+
permissionError,
|
|
290
|
+
failureMessage,
|
|
291
|
+
}) {
|
|
242
292
|
const auth = await ensureAuth(options);
|
|
243
293
|
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
244
294
|
const localProject = await findLocalProjectMetadata(
|
|
@@ -249,21 +299,21 @@ export async function renameBuild(options) {
|
|
|
249
299
|
});
|
|
250
300
|
const currentBuild = await loadBuildMetadata({ options, auth, buildId });
|
|
251
301
|
if (currentBuild.canWrite === false) {
|
|
252
|
-
throw new Error(
|
|
302
|
+
throw new Error(permissionError(buildId));
|
|
253
303
|
}
|
|
254
304
|
if (Number(currentBuild.contributionRootBuildId || 0) > 0) {
|
|
255
305
|
throw new Error(
|
|
256
|
-
"Contribution branches use the original Build
|
|
306
|
+
"Contribution branches use the original Build details and cannot change them.",
|
|
257
307
|
);
|
|
258
308
|
}
|
|
259
309
|
const result = await updateBuildMetadata({
|
|
260
310
|
options,
|
|
261
311
|
auth,
|
|
262
312
|
buildId,
|
|
263
|
-
patch
|
|
313
|
+
patch,
|
|
264
314
|
});
|
|
265
315
|
if (result?.success !== true || !result?.build) {
|
|
266
|
-
throw new Error(result?.error ||
|
|
316
|
+
throw new Error(result?.error || failureMessage);
|
|
267
317
|
}
|
|
268
318
|
const updatedBuild = { ...currentBuild, ...result.build };
|
|
269
319
|
await saveSelectedBuild({ options, auth, build: updatedBuild });
|
|
@@ -282,7 +332,7 @@ export async function renameBuild(options) {
|
|
|
282
332
|
filesHash: localProject.metadata.filesHash || null,
|
|
283
333
|
});
|
|
284
334
|
}
|
|
285
|
-
|
|
335
|
+
return { buildId, updatedBuild };
|
|
286
336
|
}
|
|
287
337
|
|
|
288
338
|
export async function workspace(options) {
|
|
@@ -337,6 +387,262 @@ export async function branches(options) {
|
|
|
337
387
|
printContributionBranches({ result, rootBuild, options });
|
|
338
388
|
}
|
|
339
389
|
|
|
390
|
+
export async function sendSuggestion(options) {
|
|
391
|
+
const suggestionType = String(options.suggestionAction || "").trim();
|
|
392
|
+
if (suggestionType !== "branch" && suggestionType !== "thumbnail") {
|
|
393
|
+
throw new Error(
|
|
394
|
+
"Usage: lumine suggest branch [--note <message>] | lumine suggest thumbnail",
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const auth = await resolveAuth(options);
|
|
398
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
399
|
+
const build = await loadTargetBuildMetadata({ options, auth });
|
|
400
|
+
const { rootBuildId, contributionBuildId } =
|
|
401
|
+
resolveContributionActionBuildIds(build);
|
|
402
|
+
|
|
403
|
+
if (suggestionType === "branch") {
|
|
404
|
+
await notifyBuildOwnerOfContribution({
|
|
405
|
+
options,
|
|
406
|
+
auth,
|
|
407
|
+
rootBuildId,
|
|
408
|
+
contributionBuildId,
|
|
409
|
+
note: options.note,
|
|
410
|
+
});
|
|
411
|
+
console.log(
|
|
412
|
+
`Sent branch #${contributionBuildId} to the owner of Build #${rootBuildId} for review.`,
|
|
413
|
+
);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
await suggestBuildThumbnailToOwner({
|
|
418
|
+
options,
|
|
419
|
+
auth,
|
|
420
|
+
rootBuildId,
|
|
421
|
+
contributionBuildId,
|
|
422
|
+
});
|
|
423
|
+
console.log(
|
|
424
|
+
`Suggested branch #${contributionBuildId}'s thumbnail to the owner of Build #${rootBuildId}.`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export async function suggestions(options) {
|
|
429
|
+
const auth = await resolveAuth(options);
|
|
430
|
+
const rootBuildId = await resolveSuggestionRootBuildId(options, auth);
|
|
431
|
+
const action = String(options.suggestionAction || "list");
|
|
432
|
+
const requestedSuggestionId = Number(options.suggestionId || 0);
|
|
433
|
+
if (
|
|
434
|
+
action !== "list" &&
|
|
435
|
+
(!Number.isSafeInteger(requestedSuggestionId) || requestedSuggestionId <= 0)
|
|
436
|
+
) {
|
|
437
|
+
throw new Error(
|
|
438
|
+
`Pass a suggestion ID: lumine suggestions ${action} <suggestion-id>`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const result = await listBuildSuggestions({
|
|
442
|
+
options,
|
|
443
|
+
auth,
|
|
444
|
+
rootBuildId,
|
|
445
|
+
cursor: action === "list" ? options.cursor : 0,
|
|
446
|
+
suggestionId: action === "list" ? 0 : requestedSuggestionId,
|
|
447
|
+
});
|
|
448
|
+
const items = Array.isArray(result?.suggestions) ? result.suggestions : [];
|
|
449
|
+
|
|
450
|
+
if (action === "list") {
|
|
451
|
+
if (options.json) {
|
|
452
|
+
console.log(
|
|
453
|
+
JSON.stringify({
|
|
454
|
+
buildId: rootBuildId,
|
|
455
|
+
suggestions: items,
|
|
456
|
+
hasMore: Boolean(result?.hasMore),
|
|
457
|
+
nextCursor: Number(result?.nextCursor || 0) || null,
|
|
458
|
+
}),
|
|
459
|
+
);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
printBuildSuggestions({
|
|
463
|
+
rootBuildId,
|
|
464
|
+
suggestions: items,
|
|
465
|
+
hasMore: Boolean(result?.hasMore),
|
|
466
|
+
nextCursor: Number(result?.nextCursor || 0),
|
|
467
|
+
});
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const suggestionId = requestedSuggestionId;
|
|
472
|
+
const suggestion = items.find(
|
|
473
|
+
(item) => Number(item?.id || 0) === suggestionId,
|
|
474
|
+
);
|
|
475
|
+
if (!suggestion) {
|
|
476
|
+
throw new Error(
|
|
477
|
+
`Open suggestion #${suggestionId} was not found for Build #${rootBuildId}. Run \`lumine suggestions --build ${rootBuildId}\` to refresh the inbox.`,
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
481
|
+
const contributionBuildId = Number(suggestion.branchBuildId || 0);
|
|
482
|
+
if (!contributionBuildId) {
|
|
483
|
+
throw new Error(`Suggestion #${suggestionId} has no active branch.`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (action === "merge") {
|
|
487
|
+
if (suggestion.type !== "branch") {
|
|
488
|
+
throw new Error(`Suggestion #${suggestionId} is not a branch suggestion.`);
|
|
489
|
+
}
|
|
490
|
+
const mergeResult = await mergeContributionIntoMain({
|
|
491
|
+
options,
|
|
492
|
+
auth,
|
|
493
|
+
rootBuildId,
|
|
494
|
+
contributionBuildId,
|
|
495
|
+
});
|
|
496
|
+
printContributionActionResult({
|
|
497
|
+
action: "Merged",
|
|
498
|
+
result: mergeResult,
|
|
499
|
+
rootBuildId,
|
|
500
|
+
contributionBuildId,
|
|
501
|
+
});
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (action === "replace-main") {
|
|
506
|
+
if (suggestion.type !== "branch") {
|
|
507
|
+
throw new Error(`Suggestion #${suggestionId} is not a branch suggestion.`);
|
|
508
|
+
}
|
|
509
|
+
const replaceResult = await replaceMainWithContribution({
|
|
510
|
+
options,
|
|
511
|
+
auth,
|
|
512
|
+
rootBuildId,
|
|
513
|
+
contributionBuildId,
|
|
514
|
+
});
|
|
515
|
+
printContributionActionResult({
|
|
516
|
+
action: "Replaced main with",
|
|
517
|
+
result: replaceResult,
|
|
518
|
+
rootBuildId,
|
|
519
|
+
contributionBuildId,
|
|
520
|
+
});
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (action === "adopt-thumbnail") {
|
|
525
|
+
if (suggestion.type !== "thumbnail") {
|
|
526
|
+
throw new Error(
|
|
527
|
+
`Suggestion #${suggestionId} is not a thumbnail suggestion.`,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
if (suggestion.currentThumbnailUrl && !options.assumeYes) {
|
|
531
|
+
const confirmed = await confirmPrompt(
|
|
532
|
+
`Replace Build #${rootBuildId}'s thumbnail with suggestion #${suggestionId}? [y/N] `,
|
|
533
|
+
);
|
|
534
|
+
if (confirmed === null) {
|
|
535
|
+
console.log("Not a TTY — re-run with --yes to replace the thumbnail.");
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (!confirmed) {
|
|
539
|
+
console.log("Aborted. Thumbnail unchanged.");
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const adoptResult = await adoptBuildThumbnailSuggestion({
|
|
544
|
+
options,
|
|
545
|
+
auth,
|
|
546
|
+
rootBuildId,
|
|
547
|
+
contributionBuildId,
|
|
548
|
+
suggestionMessageId: suggestionId,
|
|
549
|
+
thumbnailUrl: suggestion.suggestedThumbnailUrl,
|
|
550
|
+
});
|
|
551
|
+
const canonicalThumbnailUrl = String(
|
|
552
|
+
adoptResult?.build?.thumbnailUrl || "",
|
|
553
|
+
);
|
|
554
|
+
console.log(
|
|
555
|
+
`Applied thumbnail suggestion #${suggestionId} to Build #${rootBuildId}.`,
|
|
556
|
+
);
|
|
557
|
+
if (canonicalThumbnailUrl) console.log(` ${canonicalThumbnailUrl}`);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
throw new Error(
|
|
562
|
+
"Usage: lumine suggestions [build] | lumine suggestions merge|replace-main|adopt-thumbnail <suggestion-id> [--build <id>]",
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function resolveSuggestionRootBuildId(options, auth) {
|
|
567
|
+
const requestedBuildId = options.buildIdFlag
|
|
568
|
+
? resolveRequiredBuildId(options.buildIdFlag)
|
|
569
|
+
: await resolveRequiredBuildIdOrSelected(options, auth);
|
|
570
|
+
const build = await loadBuildMetadata({
|
|
571
|
+
options,
|
|
572
|
+
auth,
|
|
573
|
+
buildId: requestedBuildId,
|
|
574
|
+
});
|
|
575
|
+
return (
|
|
576
|
+
Number(build?.contributionRootBuildId || 0) || Number(build?.id || 0)
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export function printBuildSuggestions({
|
|
581
|
+
rootBuildId,
|
|
582
|
+
suggestions,
|
|
583
|
+
hasMore = false,
|
|
584
|
+
nextCursor = 0,
|
|
585
|
+
}) {
|
|
586
|
+
console.log(`Open suggestions for Build #${rootBuildId}:`);
|
|
587
|
+
if (!suggestions.length) {
|
|
588
|
+
console.log(
|
|
589
|
+
hasMore
|
|
590
|
+
? "No open branch or thumbnail suggestions on this page."
|
|
591
|
+
: "No open branch or thumbnail suggestions.",
|
|
592
|
+
);
|
|
593
|
+
if (hasMore && nextCursor > 0) {
|
|
594
|
+
console.log(
|
|
595
|
+
`Next page: lumine suggestions --build ${rootBuildId} --cursor ${nextCursor}`,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
for (const suggestion of suggestions) {
|
|
601
|
+
const suggestionId = Number(suggestion.id || 0);
|
|
602
|
+
const branchNumber = Number(suggestion.branchNumber || 0);
|
|
603
|
+
const branchLabel = branchNumber
|
|
604
|
+
? `branch ${branchNumber}`
|
|
605
|
+
: `branch #${suggestion.branchBuildId}`;
|
|
606
|
+
const contributor = suggestion.contributorUsername || "a contributor";
|
|
607
|
+
const createdAt = formatVersionTimestamp(suggestion.createdAt);
|
|
608
|
+
if (suggestion.type === "branch") {
|
|
609
|
+
console.log(
|
|
610
|
+
`[#${suggestionId}] ${contributor} submitted ${branchLabel} (${createdAt})`,
|
|
611
|
+
);
|
|
612
|
+
if (suggestion.note) console.log(` ${suggestion.note}`);
|
|
613
|
+
const summary = suggestion.diffSummary || {};
|
|
614
|
+
console.log(
|
|
615
|
+
` files: ${Number(summary.total || suggestion.changedFiles?.length || 0)} changed`,
|
|
616
|
+
);
|
|
617
|
+
if (suggestion.hasNewerWorkSinceSubmission) {
|
|
618
|
+
console.log(" note: the branch changed after this suggestion");
|
|
619
|
+
}
|
|
620
|
+
console.log(
|
|
621
|
+
` merge: lumine suggestions merge ${suggestionId} --build ${rootBuildId}`,
|
|
622
|
+
);
|
|
623
|
+
console.log(
|
|
624
|
+
` replace: lumine suggestions replace-main ${suggestionId} --build ${rootBuildId}`,
|
|
625
|
+
);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
console.log(
|
|
629
|
+
`[#${suggestionId}] ${contributor} suggested a thumbnail from ${branchLabel} (${createdAt})`,
|
|
630
|
+
);
|
|
631
|
+
console.log(` ${suggestion.suggestedThumbnailUrl}`);
|
|
632
|
+
if (suggestion.hasNewerThumbnailSinceSuggestion) {
|
|
633
|
+
console.log(" note: the branch has a newer thumbnail now");
|
|
634
|
+
}
|
|
635
|
+
console.log(
|
|
636
|
+
` apply: lumine suggestions adopt-thumbnail ${suggestionId} --build ${rootBuildId}`,
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
if (hasMore && nextCursor > 0) {
|
|
640
|
+
console.log(
|
|
641
|
+
`Next page: lumine suggestions --build ${rootBuildId} --cursor ${nextCursor}`,
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
340
646
|
export async function explore(options) {
|
|
341
647
|
const auth = await resolveAuth(options);
|
|
342
648
|
const builds = await listOpenSourceBuilds({ options, auth });
|
|
@@ -1557,9 +1863,15 @@ export function printPullResult(result) {
|
|
|
1557
1863
|
);
|
|
1558
1864
|
console.log('Save after edits: lumine save --summary "Describe the change"');
|
|
1559
1865
|
if (isContributionBranch(build) && build.canPublish === false) {
|
|
1560
|
-
console.log(
|
|
1866
|
+
console.log(
|
|
1867
|
+
'Notify the owner when ready: lumine suggest branch "Ready for review"',
|
|
1868
|
+
);
|
|
1869
|
+
console.log(
|
|
1870
|
+
"Offer this branch's thumbnail: lumine suggest thumbnail",
|
|
1871
|
+
);
|
|
1561
1872
|
} else {
|
|
1562
1873
|
console.log("Run `lumine check` or `lumine launch --save` when ready.");
|
|
1874
|
+
console.log("Review team nudges: lumine suggestions");
|
|
1563
1875
|
}
|
|
1564
1876
|
}
|
|
1565
1877
|
|
|
@@ -1616,12 +1928,16 @@ export function printSaveResult({ result, build, dir, files }) {
|
|
|
1616
1928
|
console.log(`Release status: ${releaseState}`);
|
|
1617
1929
|
if (isContributionBranch(build) && build.canPublish === false) {
|
|
1618
1930
|
console.log(
|
|
1619
|
-
|
|
1931
|
+
'Next: notify the project owner with `lumine suggest branch "Ready for review"`.',
|
|
1932
|
+
);
|
|
1933
|
+
console.log(
|
|
1934
|
+
"To offer this branch's thumbnail, run `lumine suggest thumbnail`.",
|
|
1620
1935
|
);
|
|
1621
1936
|
} else {
|
|
1622
1937
|
console.log(
|
|
1623
1938
|
"Next: run `lumine launch` to publish, or `lumine save --publish` next time.",
|
|
1624
1939
|
);
|
|
1940
|
+
console.log("Team suggestions: run `lumine suggestions` to review them.");
|
|
1625
1941
|
}
|
|
1626
1942
|
}
|
|
1627
1943
|
|
|
@@ -1791,14 +2107,55 @@ export function parseArgs(args) {
|
|
|
1791
2107
|
} else if (booleanFlags.has(camelKey)) {
|
|
1792
2108
|
raw[camelKey] = true;
|
|
1793
2109
|
} else {
|
|
1794
|
-
|
|
2110
|
+
const nextValue = rest[i + 1];
|
|
2111
|
+
if (
|
|
2112
|
+
nextValue === undefined ||
|
|
2113
|
+
nextValue === "-h" ||
|
|
2114
|
+
nextValue.startsWith("--")
|
|
2115
|
+
) {
|
|
2116
|
+
throw new Error(`Missing value for --${key}.`);
|
|
2117
|
+
}
|
|
2118
|
+
raw[camelKey] = nextValue;
|
|
1795
2119
|
i += 1;
|
|
1796
2120
|
}
|
|
1797
2121
|
}
|
|
1798
2122
|
|
|
2123
|
+
const suggestionInboxActions = new Set([
|
|
2124
|
+
"list",
|
|
2125
|
+
"merge",
|
|
2126
|
+
"replace-main",
|
|
2127
|
+
"adopt-thumbnail",
|
|
2128
|
+
]);
|
|
2129
|
+
const suggestionAction =
|
|
2130
|
+
command === "suggest"
|
|
2131
|
+
? String(positional[0] || "")
|
|
2132
|
+
: command === "suggestions" &&
|
|
2133
|
+
suggestionInboxActions.has(String(positional[0] || ""))
|
|
2134
|
+
? String(positional[0])
|
|
2135
|
+
: "list";
|
|
2136
|
+
const suggestionListTarget =
|
|
2137
|
+
command === "suggestions" && suggestionAction === "list"
|
|
2138
|
+
? String(
|
|
2139
|
+
positional[0] === "list" ? positional[1] || "" : positional[0] || "",
|
|
2140
|
+
)
|
|
2141
|
+
: "";
|
|
2142
|
+
|
|
1799
2143
|
return {
|
|
1800
2144
|
command,
|
|
1801
2145
|
positional,
|
|
2146
|
+
suggestionAction,
|
|
2147
|
+
suggestionId:
|
|
2148
|
+
command === "suggestions" && suggestionAction !== "list"
|
|
2149
|
+
? String(positional[1] || "")
|
|
2150
|
+
: "",
|
|
2151
|
+
note:
|
|
2152
|
+
String(
|
|
2153
|
+
raw.note ||
|
|
2154
|
+
(command === "suggest" && suggestionAction === "branch"
|
|
2155
|
+
? positional.slice(1).join(" ")
|
|
2156
|
+
: ""),
|
|
2157
|
+
).trim() || "",
|
|
2158
|
+
cursor: Math.max(0, Math.floor(Number(raw.cursor) || 0)),
|
|
1802
2159
|
repeat: Math.min(Math.max(Math.floor(Number(raw.repeat) || 1), 1), 20),
|
|
1803
2160
|
allowWrite: parseBoolean(raw.allowWrite, false),
|
|
1804
2161
|
assumeYes: parseBoolean(raw.yes, false),
|
|
@@ -1815,7 +2172,13 @@ export function parseArgs(args) {
|
|
|
1815
2172
|
target:
|
|
1816
2173
|
raw.url ||
|
|
1817
2174
|
raw.target ||
|
|
1818
|
-
(command === "rename"
|
|
2175
|
+
(command === "rename" ||
|
|
2176
|
+
command === "describe" ||
|
|
2177
|
+
command === "suggest"
|
|
2178
|
+
? ""
|
|
2179
|
+
: command === "suggestions"
|
|
2180
|
+
? suggestionListTarget
|
|
2181
|
+
: positional[0] || ""),
|
|
1819
2182
|
title:
|
|
1820
2183
|
String(
|
|
1821
2184
|
raw.title ||
|
|
@@ -1825,11 +2188,12 @@ export function parseArgs(args) {
|
|
|
1825
2188
|
).trim() || "",
|
|
1826
2189
|
description: Object.prototype.hasOwnProperty.call(raw, "description")
|
|
1827
2190
|
? String(raw.description || "").trim()
|
|
1828
|
-
:
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
2191
|
+
: command === "describe" && positional.length > 0
|
|
2192
|
+
? positional.join(" ").trim()
|
|
2193
|
+
: null,
|
|
2194
|
+
descriptionProvided:
|
|
2195
|
+
Object.prototype.hasOwnProperty.call(raw, "description") ||
|
|
2196
|
+
(command === "describe" && positional.length > 0),
|
|
1833
2197
|
noDescription: parseBoolean(raw.noDescription, false),
|
|
1834
2198
|
searchQuery:
|
|
1835
2199
|
String(
|
|
@@ -2009,6 +2373,32 @@ export async function resolveNewBuildDescription(options) {
|
|
|
2009
2373
|
}
|
|
2010
2374
|
}
|
|
2011
2375
|
|
|
2376
|
+
export async function resolveBuildDescriptionUpdate(options) {
|
|
2377
|
+
if (options.noDescription && options.descriptionProvided) {
|
|
2378
|
+
throw new Error(
|
|
2379
|
+
"Pass either a description or `--no-description`, not both.",
|
|
2380
|
+
);
|
|
2381
|
+
}
|
|
2382
|
+
if (options.noDescription) return null;
|
|
2383
|
+
if (options.descriptionProvided) {
|
|
2384
|
+
return String(options.description || "").trim() || null;
|
|
2385
|
+
}
|
|
2386
|
+
if (!input.isTTY || !output.isTTY) {
|
|
2387
|
+
throw new Error(
|
|
2388
|
+
'Pass a description: `lumine describe "What this app does"`, or clear it with `lumine describe --no-description`.',
|
|
2389
|
+
);
|
|
2390
|
+
}
|
|
2391
|
+
const rl = readline.createInterface({ input, output });
|
|
2392
|
+
try {
|
|
2393
|
+
const answer = await rl.question(
|
|
2394
|
+
"Build description (leave blank to clear): ",
|
|
2395
|
+
);
|
|
2396
|
+
return String(answer || "").trim() || null;
|
|
2397
|
+
} finally {
|
|
2398
|
+
rl.close();
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2012
2402
|
export async function resolveBuildReferenceBuildId({
|
|
2013
2403
|
options,
|
|
2014
2404
|
auth,
|
|
@@ -2042,8 +2432,15 @@ export function printHelp() {
|
|
|
2042
2432
|
lumine logout
|
|
2043
2433
|
lumine new [title]
|
|
2044
2434
|
lumine rename [title] [--target <twinkle-build-url-or-id>]
|
|
2435
|
+
lumine describe [description] [--target <twinkle-build-url-or-id>]
|
|
2045
2436
|
lumine projects
|
|
2046
2437
|
lumine branches [twinkle-build-url-or-id] [--limit <n>]
|
|
2438
|
+
lumine suggest branch [message] [--target <twinkle-branch-url>]
|
|
2439
|
+
lumine suggest thumbnail [--target <twinkle-branch-url>]
|
|
2440
|
+
lumine suggestions [twinkle-build-url-or-id]
|
|
2441
|
+
lumine suggestions merge <suggestion-id> [--build <id>]
|
|
2442
|
+
lumine suggestions replace-main <suggestion-id> [--build <id>]
|
|
2443
|
+
lumine suggestions adopt-thumbnail <suggestion-id> [--build <id>] [--yes]
|
|
2047
2444
|
lumine explore [search terms]
|
|
2048
2445
|
lumine select [twinkle-build-url]
|
|
2049
2446
|
lumine pull [twinkle-build-url]
|
|
@@ -2078,8 +2475,15 @@ Examples:
|
|
|
2078
2475
|
npx @stage5/lumine@latest new "Daily Reflection App"
|
|
2079
2476
|
npx @stage5/lumine@latest rename "My New Build Title"
|
|
2080
2477
|
npx @stage5/lumine@latest rename "New Title" --target 123
|
|
2478
|
+
npx @stage5/lumine@latest describe "A welcoming place to build together"
|
|
2479
|
+
npx @stage5/lumine@latest describe --no-description --target 123
|
|
2081
2480
|
npx @stage5/lumine@latest new --title "Daily Reflection App" --description "Private journal with streaks"
|
|
2082
2481
|
npx @stage5/lumine@latest branches 884
|
|
2482
|
+
npx @stage5/lumine@latest suggest branch "Ready for review"
|
|
2483
|
+
npx @stage5/lumine@latest suggest thumbnail
|
|
2484
|
+
npx @stage5/lumine@latest suggestions 884
|
|
2485
|
+
npx @stage5/lumine@latest suggestions merge 12345 --build 884
|
|
2486
|
+
npx @stage5/lumine@latest suggestions adopt-thumbnail 12346 --build 884 --yes
|
|
2083
2487
|
npx @stage5/lumine@latest explore --sort forks
|
|
2084
2488
|
npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
|
|
2085
2489
|
npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
|
|
@@ -2113,13 +2517,15 @@ Options:
|
|
|
2113
2517
|
--auth-file <path> Saved login path
|
|
2114
2518
|
--auth-token <token> Override saved login
|
|
2115
2519
|
--dir <path> Directory for pulled project files
|
|
2116
|
-
--target <build> Explicit Build URL or ID for rename
|
|
2520
|
+
--target <build> Explicit Build URL or ID for rename/describe
|
|
2117
2521
|
--main With pull/versions/restore: target the team project's main
|
|
2118
2522
|
--version <n> With pull: read-only checkout of previous save v<n>
|
|
2119
2523
|
--title <text> Build title for new/rename
|
|
2120
|
-
--description <text>
|
|
2121
|
-
--no-description Skip
|
|
2524
|
+
--description <text> Build description for new/describe
|
|
2525
|
+
--no-description Skip New description or clear with describe
|
|
2122
2526
|
--summary <text> Save summary
|
|
2527
|
+
--note <text> Message attached to a branch suggestion
|
|
2528
|
+
--cursor <id> Continue an owner suggestion inbox listing
|
|
2123
2529
|
--force Overwrite server files even if this workspace is stale or missing filesHash
|
|
2124
2530
|
--search <text> Search public open-source Builds
|
|
2125
2531
|
--sort <sort> Sort open-source Builds: forks, popular, recent
|
|
@@ -2128,7 +2534,7 @@ Options:
|
|
|
2128
2534
|
--limit <number> Number of projects to show
|
|
2129
2535
|
--no-update-check Skip the npm latest-version check
|
|
2130
2536
|
--no-open Print the approval URL without opening a browser
|
|
2131
|
-
--build <id> Build id for sdk/assets/thumbnail calls outside a workspace
|
|
2537
|
+
--build <id> Build id for sdk/assets/thumbnail/suggestion calls outside a workspace
|
|
2132
2538
|
--repeat <n> Repeat an sdk call (1-20) and print latency stats
|
|
2133
2539
|
--allow-write Permit sdk methods that mutate app data
|
|
2134
2540
|
--path <api/...> Call an sdk endpoint not in the curated list
|
package/lib/constants.js
CHANGED
|
@@ -118,7 +118,7 @@ Use these current source-of-truth rules:
|
|
|
118
118
|
- Use Twinkle.sharedDb for shared multi-user JSON data.
|
|
119
119
|
- Use Twinkle.aiCards.list/search/get for existing public AI Card words, exampleText sentence material, and word levels.
|
|
120
120
|
- Use Twinkle.aiStories.list/search/get for existing AI Story passage text, story media, and questions.
|
|
121
|
-
- Use Twinkle.ai.chat with history entries shaped as { role, content }, not { text }.
|
|
121
|
+
- Use Twinkle.ai.chat with history entries shaped as { role, content }, not { text }. Live web search is enabled by default; pass webSearch: false to disable it for the app.
|
|
122
122
|
- Use Twinkle.preview for canvas, WebGL, Three.js, fullscreen, and game layout.
|
|
123
123
|
- Prefer existing documented Twinkle.* methods over guessing names from old code.
|
|
124
124
|
`;
|
|
@@ -170,6 +170,16 @@ lumine save --summary "Describe the change"
|
|
|
170
170
|
need --allow-write and mutate real app data.
|
|
171
171
|
- Owned canonical builds may be published only when the user explicitly asks.
|
|
172
172
|
|
|
173
|
+
## Team Suggestions
|
|
174
|
+
|
|
175
|
+
- After saving contribution-branch work, use \`lumine suggest branch "Ready for review"\`
|
|
176
|
+
when the user wants to notify the project owner. Use \`lumine suggest thumbnail\`
|
|
177
|
+
when the branch's current thumbnail should be offered to the owner.
|
|
178
|
+
- On an owned canonical team project, \`lumine suggestions\` lists the owner's
|
|
179
|
+
currently open branch and thumbnail suggestions. Act on the exact suggestion
|
|
180
|
+
id it prints with \`lumine suggestions merge\`, \`replace-main\`, or
|
|
181
|
+
\`adopt-thumbnail\`; do not infer an action from stale local branch state.
|
|
182
|
+
|
|
173
183
|
## Assets (Runtime Media)
|
|
174
184
|
|
|
175
185
|
- Binary files are NOT project files. Never place bundled media in this
|
|
@@ -326,8 +336,11 @@ export const COMMANDS = new Set([
|
|
|
326
336
|
"whoami",
|
|
327
337
|
"new",
|
|
328
338
|
"rename",
|
|
339
|
+
"describe",
|
|
329
340
|
"projects",
|
|
330
341
|
"branches",
|
|
342
|
+
"suggest",
|
|
343
|
+
"suggestions",
|
|
331
344
|
"explore",
|
|
332
345
|
"select",
|
|
333
346
|
"pull",
|
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.32.0
|
|
4
|
+
Updated: 2026-08-02
|
|
5
|
+
Generated: 2026-08-02T03:03:30.002Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -12,6 +12,7 @@ Generated: 2026-07-23T05:57:27.486Z
|
|
|
12
12
|
- Match storage to update frequency: privateDb and sharedDb are for LOW-frequency durable state that changes on a user action. NEVER write per-frame/per-tick state to them (camera or cursor position, animation, live movement, presence, autosave every frame/tick). Keep live state in client memory, broadcast realtime/presence via Twinkle.world, and flush only occasional durable snapshots (on an interval or on exit, never per frame). The server enforces per-key write rate limits and returns 429 on excess; never retry-loop a 429.
|
|
13
13
|
- Use Twinkle.userDb only for advanced private SQLite needs such as tables, indexes, many rows, filtered queries, or aggregates.
|
|
14
14
|
- Use Twinkle.leaderboards for public Build scoreboards. Signed-in viewers are ranked by Twinkle username; guests can submit with a display name.
|
|
15
|
+
- Use Twinkle.news to read the globally shared Twinkle Daily edition, browse canonical daily archives and preserved successful press runs, or let a signed-in viewer queue today's edition. The server permits only one canonical edition per Twinkle day for ordinary viewers. In the canonical Twinkle Newspaper app, its current owner may explicitly refresh today's ready edition, appending a revision while keeping the newest successful press run canonical. A model-backed edition consumes AI Energy from the signed-in viewer whose request creates, retries, or refreshes that job; deduplicated observers and quiet editions with no editorial model call do not consume Energy.
|
|
15
16
|
- Use Twinkle.sharedDb for LOW-frequency durable shared multi-user state such as guestbooks, votes, room settings, submitted records, and append-only run history. It is NOT for high-frequency or per-frame/per-tick writes; keep live/realtime state in Twinkle.world or client memory. The server rate-limits writes and returns 429.
|
|
16
17
|
- Use Twinkle.subjects.search for in-app subject pickers. Twinkle.mount remains an optional host-provided preselection/context shortcut, not a data API.
|
|
17
18
|
- Use Twinkle.aiCards for read-only existing public AI Card words and example texts, including word levels for typing games.
|
|
@@ -21,6 +22,7 @@ Generated: 2026-07-23T05:57:27.486Z
|
|
|
21
22
|
- Use Twinkle.world for realtime multiplayer rooms, avatar presence, movement, emotes, and lightweight actions; world sessions are disposable and durable MMO state belongs in sharedDb/privateDb.
|
|
22
23
|
- Use Twinkle.characters.chat for real Zero/Ciel NPC dialogue with shared room context and AI Energy-aware thinking modes.
|
|
23
24
|
- Twinkle.ai.chat history entries must use { role, content }; map local message.text fields to content before passing history.
|
|
25
|
+
- Live web search is enabled by default for Twinkle.ai.chat and for Medium/High Twinkle.ai.generateObject and Twinkle.characters.chat requests. App authors can pass webSearch: false to disable it for their app. Search uses the provider's live web-search tool and is included in AI Energy usage; structured and character Lite Mode remains tool-free.
|
|
24
26
|
- Interface text must not be selectable on touch devices: apply user-select: none plus -webkit-user-select: none and -webkit-touch-callout: none to interface text (HUD, buttons, labels, menus, scores, game controls) so mobile long-press does not highlight UI. Keep text inputs and genuinely user-copyable content selectable.
|
|
25
27
|
- Build app tab mute is enforced by the host runtime automatically for standard media elements and Web Audio connections to AudioContext.destination. Apps with custom audio engines can also observe Twinkle.onAudioMuteChange and check Twinkle.isAudioMuted.
|
|
26
28
|
|
|
@@ -281,31 +283,34 @@ console.log(analysis.bestMove, analysis.evaluation, analysis.mate);
|
|
|
281
283
|
- async listPrompts() | scopes: none
|
|
282
284
|
- Returns: Array<{ id, title, description }>
|
|
283
285
|
- Legacy helper. Twinkle.ai.chat does not require promptId for default runtime text generation.
|
|
284
|
-
- async chat({ promptId, message, history, systemPrompt, requestId, onText, onStatus } = {}) | scopes: none
|
|
285
|
-
- Returns: { text, response, model, aiUsagePolicy }
|
|
286
|
-
- Generate text with the default Lumine text model, optionally streaming text updates through onText.
|
|
286
|
+
- async chat({ promptId, message, history, systemPrompt, webSearch, requestId, onText, onStatus } = {}) | scopes: none
|
|
287
|
+
- Returns: { text, response, model, webSearch, aiUsagePolicy }
|
|
288
|
+
- Generate text with the default Lumine text model, optionally using live web search and streaming text updates through onText.
|
|
287
289
|
- Signed-in viewers only.
|
|
288
290
|
- Uses Grok 4.5 by default.
|
|
289
291
|
- Each successful text generation consumes AI Energy from the signed-in viewer.
|
|
290
292
|
- history must be an array of { role: 'user' | 'assistant', content: string }. Twinkle.ai.chat does not read a text field.
|
|
291
293
|
- The server keeps the latest 12 valid history entries.
|
|
292
294
|
- Pass systemPrompt to define the app AI's personality, tone, role, or response rules.
|
|
295
|
+
- Live web search is enabled by default, and the model decides whether searching is useful. Pass webSearch: false to disable it for the app.
|
|
296
|
+
- When streaming, onStatus may receive searching_web while the provider is searching.
|
|
293
297
|
- Pass onText to receive streaming accumulated text before the final result resolves.
|
|
294
|
-
- AI Energy is recorded after provider success when final token usage
|
|
298
|
+
- AI Energy is recorded after provider success when final token and web-search tool usage are available.
|
|
295
299
|
- Use this for in-app AI replies instead of creating or fetching app-local endpoints such as /api/chat.
|
|
296
300
|
- Example: const chatHistory = conversation.slice(-12).map((entry) => ({ role: entry.role === 'assistant' ? 'assistant' : 'user', content: entry.text }));
|
|
297
301
|
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') });
|
|
298
|
-
- async generateObject({ prompt, expectedStructure, thinkingMode, mode, instructions, systemPrompt } = {}) | scopes: none
|
|
299
|
-
- Returns: { object, result, model, provider, thinkingMode, requestedThinkingMode, aiUsagePolicy }
|
|
300
|
-
- Generate a validated structured JSON object for app decisions, routing, grading, and game-state logic.
|
|
302
|
+
- async generateObject({ prompt, expectedStructure, thinkingMode, mode, instructions, systemPrompt, webSearch } = {}) | scopes: none
|
|
303
|
+
- Returns: { object, result, model, provider, thinkingMode, requestedThinkingMode, webSearch, aiUsagePolicy }
|
|
304
|
+
- Generate a validated structured JSON object for app decisions, routing, grading, and game-state logic, optionally using live web search.
|
|
301
305
|
- Signed-in viewers only.
|
|
302
306
|
- Use this instead of asking Twinkle.ai.chat to return JSON.
|
|
303
307
|
- expectedStructure must be a JSON object that describes the exact returned object shape.
|
|
304
308
|
- mode is accepted as an alias for thinkingMode, and mid is accepted as an alias for medium.
|
|
305
|
-
- thinkingMode low uses GPT-5.6 Luna and
|
|
306
|
-
- thinkingMode medium uses
|
|
307
|
-
- thinkingMode high uses
|
|
308
|
-
-
|
|
309
|
+
- thinkingMode low uses GPT-5.6 Luna and consumes the viewer's AI Energy from confirmed provider usage; its smaller model is usually cheaper than Medium or High.
|
|
310
|
+
- thinkingMode medium uses Grok 4.5 with medium reasoning and consumes normal AI Energy.
|
|
311
|
+
- thinkingMode high uses GPT-5.6 Sol with high reasoning and consumes high AI Energy.
|
|
312
|
+
- When AI Energy is empty, Low, Medium, and High all reject before new provider work; there is no free fallback mode.
|
|
313
|
+
- Live web search is enabled by default in Medium and High modes. Pass webSearch: false to disable it for the app. Low/Lite Mode remains tool-free; explicitly forcing webSearch: true in Low Mode returns an error.
|
|
309
314
|
- The SDK validates shape and retries malformed JSON, but app code should still validate business-specific enum values.
|
|
310
315
|
- 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 } });
|
|
311
316
|
- onChatStatus(listener) | scopes: none
|
|
@@ -337,21 +342,24 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
|
|
|
337
342
|
- Example: const unsubscribe = Twinkle.ai.onImageGenerationStatus((status) => console.log(status.stage));
|
|
338
343
|
|
|
339
344
|
### Twinkle.characters
|
|
340
|
-
- async chat({ character, thinkingMode, message, history, roomContext, scene, systemPrompt, instructions, includeWebsiteContext, requestId, onText, onStatus } = {}) | scopes: none
|
|
341
|
-
- Returns: { text, response, character, aiUsername, thinkingMode, requestedThinkingMode, includeWebsiteContext, model, provider, aiUsagePolicy }
|
|
342
|
-
- Talk to Zero or Ciel from a Build app,
|
|
345
|
+
- async chat({ character, thinkingMode, message, history, roomContext, scene, systemPrompt, instructions, includeWebsiteContext, webSearch, requestId, onText, onStatus } = {}) | scopes: none
|
|
346
|
+
- Returns: { text, response, character, aiUsername, thinkingMode, requestedThinkingMode, includeWebsiteContext, webSearch, model, provider, aiUsagePolicy }
|
|
347
|
+
- Talk to Zero or Ciel from a Build app, optionally using live web search and streaming RPG-style dialogue text with onText/onStatus.
|
|
343
348
|
- Signed-in viewers only.
|
|
344
349
|
- character must be zero or ciel.
|
|
345
350
|
- Recommended history shape is { role: 'user' | 'assistant', content: string, speaker?: string }; content is the canonical text field.
|
|
346
351
|
- The character route also accepts text or message fields for compatibility, but generated apps should use content.
|
|
347
352
|
- The server keeps the latest 16 valid character history entries.
|
|
348
353
|
- Pass onText/onStatus for streaming dialogue. Omit callbacks for non-streaming dialogue where the promise resolves with the final response.
|
|
349
|
-
- thinkingMode low uses Lite Mode: Zero uses Grok 4.5 with low reasoning and Ciel uses Claude Haiku 4.5; usage is
|
|
350
|
-
- thinkingMode medium
|
|
351
|
-
- thinkingMode high
|
|
352
|
-
-
|
|
354
|
+
- thinkingMode low uses Lite Mode: Zero uses Grok 4.5 with low reasoning and Ciel uses Claude Haiku 4.5; confirmed provider usage consumes the viewer's AI Energy and is usually cheaper than Medium or High.
|
|
355
|
+
- thinkingMode medium consumes normal AI Energy: Zero uses Grok 4.5 with medium reasoning and Ciel uses Claude Sonnet 5.
|
|
356
|
+
- thinkingMode high consumes high AI Energy: Zero uses Grok 4.5 with high reasoning and Ciel uses Claude Opus 5 with extended thinking.
|
|
357
|
+
- When AI Energy is empty, Low, Medium, and High all reject before new provider work; there is no free fallback mode.
|
|
353
358
|
- Pass roomContext as a short shared scene transcript so Zero and Ciel can know what happened in the same room.
|
|
354
359
|
- includeWebsiteContext defaults to true. Set includeWebsiteContext: false for in-world NPC dialogue that should only use Zero/Ciel's basic character identity plus your scene/instructions.
|
|
360
|
+
- Live web search is enabled by default in Medium and High modes. Pass webSearch: false to disable it for the app. Low/Lite Mode remains tool-free; explicitly forcing webSearch: true in Low Mode returns an error.
|
|
361
|
+
- includeWebsiteContext controls Twinkle persona context and is unrelated to webSearch.
|
|
362
|
+
- When streaming, onStatus may receive searching_web while the provider is searching.
|
|
355
363
|
- Use this for real Zero/Ciel NPCs instead of pretending with Twinkle.ai.chat systemPrompt.
|
|
356
364
|
- Example: const dialogueHistory = recentTurns.slice(-16).map((entry) => ({ role: entry.role === 'assistant' ? 'assistant' : 'user', content: entry.text, speaker: entry.speaker }));
|
|
357
365
|
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) });
|
|
@@ -519,6 +527,38 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
519
527
|
- Atomic step 3: fetches likes/replies aggregates for provided IDs.
|
|
520
528
|
- Accepts either an array of IDs or an object like { ids: [...] }.
|
|
521
529
|
|
|
530
|
+
### Twinkle.news
|
|
531
|
+
- async getCurrentEdition() | scopes: none
|
|
532
|
+
- Returns: { dayIndex, nextEditionAt, generationStatus, edition: { id, dayIndex, status, coverageStartedAt, coverageEndedAt, sourceEventCount, edition, model, provider, generatedAt, revisionNumber, revisionCount } | null, pendingEdition }
|
|
533
|
+
- Read today's shared Twinkle newspaper, or the latest ready edition while today's is being generated.
|
|
534
|
+
- Works for signed-in viewers and public-build guests.
|
|
535
|
+
- generationStatus is available, pending, generating, ready, or failed.
|
|
536
|
+
- While today's first edition is pending, edition remains the latest ready shared edition. During an owner refresh, edition remains the earlier same-day edition. pendingEdition describes the canonical queued work in both cases.
|
|
537
|
+
- Poll gently while generation is pending; once every 5-10 seconds is sufficient.
|
|
538
|
+
- async listEditions({ limit = 12, cursor } = {}) | scopes: none
|
|
539
|
+
- Returns: { editions: [{ id, dayIndex, dateKey, headline, deck, coverageStartedAt, coverageEndedAt, sourceEventCount, generatedAt, revisionNumber, revisionCount }], cursor, hasMore }
|
|
540
|
+
- List the canonical daily newspaper archive newest-first.
|
|
541
|
+
- Works for signed-in viewers and public-build guests.
|
|
542
|
+
- Returns compact publication summaries rather than full newspaper JSON.
|
|
543
|
+
- limit defaults to 12 and is capped at 30. Pass cursor from the previous response to load older editions.
|
|
544
|
+
- Each item describes the latest canonical revision for that Twinkle day.
|
|
545
|
+
- async getEdition({ dayIndex, revisionNumber } = {}) | scopes: none
|
|
546
|
+
- Returns: { edition: { id, revisionId?, dayIndex, status, coverageStartedAt, coverageEndedAt, sourceEventCount, edition, model, provider, generatedAt, revisionNumber, revisionCount }, revisions: [{ revisionId, revisionNumber, coverageStartedAt, coverageEndedAt, sourceEventCount, model, provider, generatedAt, createdAt }], selectedRevisionNumber, canonicalRevisionNumber }
|
|
547
|
+
- Read one canonical daily edition or an exact preserved successful press run.
|
|
548
|
+
- Works for signed-in viewers and public-build guests.
|
|
549
|
+
- dayIndex is required. Omit revisionNumber to read that day's latest canonical edition.
|
|
550
|
+
- Pass a revisionNumber returned in revisions to read that exact successful press run.
|
|
551
|
+
- Historical revisions remain subject to canonical privacy and deletion redactions.
|
|
552
|
+
- async generateCurrentEdition({ refresh = false } = {}) | scopes: none
|
|
553
|
+
- Returns: { dayIndex, nextEditionAt, generationStatus, edition, pendingEdition }
|
|
554
|
+
- Atomically queue the current Twinkle day's globally shared edition.
|
|
555
|
+
- Requires a signed-in viewer.
|
|
556
|
+
- The first request for a Twinkle day creates the canonical pending edition; concurrent and later ordinary requests return that same server state.
|
|
557
|
+
- In the canonical Twinkle Newspaper app, its current owner may pass { refresh: true } to revise an already-ready same-day edition using the latest canonical events. Every successful refresh is appended as a preserved press run and becomes that day's canonical revision. Ownership is checked by the server at request time and therefore follows an app transfer.
|
|
558
|
+
- When this request creates, retries, or refreshes a model-backed edition, confirmed provider usage consumes AI Energy from this requesting viewer. Concurrent or later callers that deduplicate onto the same pending or ready edition are not charged.
|
|
559
|
+
- A quiet edition with no editorial events does not call an AI provider and does not consume AI Energy.
|
|
560
|
+
- A failed attempt may be queued again on the same day. A ready edition is immutable for ordinary viewers.
|
|
561
|
+
|
|
522
562
|
### Twinkle.leaderboards
|
|
523
563
|
- async get({ boardKey = 'default', limit, cursor } = {}) | scopes: none
|
|
524
564
|
- Returns: { entries: [{ rank, id, buildId, boardKey, viewerKind, userId, displayName, score, meta, achievedAt, createdAt, updatedAt }], scores, cursor, hasMore, personalBest: { id, buildId, boardKey, viewerKind, userId, displayName, score, meta, achievedAt, createdAt, updatedAt } | null }
|