@stage5/lumine 0.2.8 → 0.2.10
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/lib/assets.js +192 -39
- package/lib/constants.js +22 -8
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +9 -10
package/lib/assets.js
CHANGED
|
@@ -275,7 +275,7 @@ export async function assetsUpload(options) {
|
|
|
275
275
|
const filePaths = options.positional.slice(1);
|
|
276
276
|
if (!filePaths.length) {
|
|
277
277
|
throw new Error(
|
|
278
|
-
"Usage: lumine assets upload <file...> (images
|
|
278
|
+
"Usage: lumine assets upload <file...> (runtime media: images, audio, GLB/self-contained glTF, KTX2, HDR, EXR, BIN, DRC)",
|
|
279
279
|
);
|
|
280
280
|
}
|
|
281
281
|
const buildId = await resolveSdkBuildId(options);
|
|
@@ -296,39 +296,77 @@ export async function assetsUpload(options) {
|
|
|
296
296
|
// second asset with a new URL, and the old one keeps eating quota. Project-
|
|
297
297
|
// owner assets are checked too: re-uploading media the project already has
|
|
298
298
|
// defeats the reuse flow.
|
|
299
|
+
const referencedGltfAssetUrls = candidates.flatMap(
|
|
300
|
+
(candidate) => candidate.referencedGltfAssetUrls || [],
|
|
301
|
+
);
|
|
302
|
+
let existingAssets = [];
|
|
303
|
+
let existingProjectAssets = [];
|
|
299
304
|
try {
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
305
|
+
const listedAssets = await fetchAllRuntimeAssets({
|
|
306
|
+
options,
|
|
307
|
+
auth,
|
|
308
|
+
buildId,
|
|
309
|
+
buildApiToken: token,
|
|
310
|
+
});
|
|
311
|
+
existingAssets = listedAssets.assets;
|
|
312
|
+
existingProjectAssets = listedAssets.projectAssets || [];
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (referencedGltfAssetUrls.length > 0) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
"Could not verify this glTF's Twinkle asset URLs. Refresh the asset list and try again.",
|
|
317
|
+
{ cause: error },
|
|
312
318
|
);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (referencedGltfAssetUrls.length > 0) {
|
|
322
|
+
const knownAssets = [...existingAssets, ...existingProjectAssets];
|
|
323
|
+
const unverifiedUrls = referencedGltfAssetUrls.filter(
|
|
324
|
+
(referencedUrl) =>
|
|
325
|
+
!knownAssets.some((asset) =>
|
|
326
|
+
runtimeAssetUrlMatchesGltfReference(asset, referencedUrl),
|
|
327
|
+
),
|
|
328
|
+
);
|
|
329
|
+
if (unverifiedUrls.length > 0) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`The glTF references Twinkle asset URL${
|
|
332
|
+
unverifiedUrls.length === 1 ? "" : "s"
|
|
333
|
+
} that do not belong to this Build: ${unverifiedUrls
|
|
334
|
+
.slice(0, 3)
|
|
335
|
+
.map((url) => `"${url}"`)
|
|
336
|
+
.join(
|
|
337
|
+
", ",
|
|
338
|
+
)}${unverifiedUrls.length > 3 ? ", ..." : ""}. Upload or reuse assets listed by \`lumine assets list\`, then use their exact URLs.`,
|
|
322
339
|
);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (existingAssets.length > 0 || existingProjectAssets.length > 0) {
|
|
343
|
+
try {
|
|
344
|
+
const nameMatches = (asset, fileName) =>
|
|
345
|
+
asset.originalFileName === fileName || asset.fileName === fileName;
|
|
346
|
+
for (const candidate of candidates) {
|
|
347
|
+
const existing = existingAssets.find((asset) =>
|
|
348
|
+
nameMatches(asset, candidate.fileName),
|
|
349
|
+
);
|
|
350
|
+
if (existing) {
|
|
351
|
+
console.error(
|
|
352
|
+
`lumine: note — an asset named "${candidate.fileName}" already exists (#${existing.id}). ` +
|
|
353
|
+
`Uploading creates a NEW asset with a new URL; if this replaces #${existing.id}, update the code and run \`lumine assets delete ${existing.id}\` (or \`lumine assets prune\`).`,
|
|
354
|
+
);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const projectExisting = existingProjectAssets.find((asset) =>
|
|
358
|
+
nameMatches(asset, candidate.fileName),
|
|
327
359
|
);
|
|
360
|
+
if (projectExisting) {
|
|
361
|
+
console.error(
|
|
362
|
+
`lumine: note — the project owner already has an asset named "${candidate.fileName}" (#${projectExisting.id}). ` +
|
|
363
|
+
`Consider reusing its URL instead of uploading a duplicate:\n ${projectExisting.url}`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
328
366
|
}
|
|
367
|
+
} catch {
|
|
368
|
+
// Duplicate-name warnings are advisory only.
|
|
329
369
|
}
|
|
330
|
-
} catch {
|
|
331
|
-
// Advisory only — never block an upload on the pre-listing.
|
|
332
370
|
}
|
|
333
371
|
const uploaded = [];
|
|
334
372
|
for (const candidate of candidates) {
|
|
@@ -400,9 +438,10 @@ export async function assetsDelete(options) {
|
|
|
400
438
|
// matching workspace. An asset must be unreferenced by ALL THREE to be a
|
|
401
439
|
// candidate. If the server does not provide `referencedByProject` (older API
|
|
402
440
|
// deployment), prune shows the plan but refuses to delete — the published-
|
|
403
|
-
// snapshot check cannot be done client-side. Structural blind
|
|
441
|
+
// snapshot check cannot be done client-side. Structural blind spots, always
|
|
404
442
|
// disclosed: references stored in app data (privateDb/sharedDb/viewer DB by
|
|
405
|
-
// Twinkle.files-using apps)
|
|
443
|
+
// Twinkle.files-using apps), and references stored only inside uploaded asset
|
|
444
|
+
// files such as .gltf manifests, are invisible to every scan.
|
|
406
445
|
export async function assetsPrune(options) {
|
|
407
446
|
const buildId = await resolveSdkBuildId(options);
|
|
408
447
|
const auth = await ensureAuth(options);
|
|
@@ -470,7 +509,7 @@ export async function assetsPrune(options) {
|
|
|
470
509
|
);
|
|
471
510
|
}
|
|
472
511
|
console.log(
|
|
473
|
-
"Caution: asset URLs stored in app data (privateDb/sharedDb/user DBs by apps using Twinkle.files) cannot be detected.
|
|
512
|
+
"Caution: asset URLs stored in app data (privateDb/sharedDb/user DBs by apps using Twinkle.files), or referenced only inside uploaded files such as .gltf manifests, cannot be detected. Keep those URLs in project files too, or do not prune this build's assets.",
|
|
474
513
|
);
|
|
475
514
|
if (!serverVerified) {
|
|
476
515
|
console.log(
|
|
@@ -502,7 +541,9 @@ export async function assetsPrune(options) {
|
|
|
502
541
|
body: { assetId: asset.id },
|
|
503
542
|
});
|
|
504
543
|
lastUsage = result?.usage || lastUsage;
|
|
505
|
-
console.log(
|
|
544
|
+
console.log(
|
|
545
|
+
`Deleted #${asset.id} ${asset.originalFileName || asset.fileName}`,
|
|
546
|
+
);
|
|
506
547
|
}
|
|
507
548
|
printAssetUsage(lastUsage);
|
|
508
549
|
await refreshWorkspaceAssetsManifest({
|
|
@@ -528,7 +569,12 @@ export async function confirmPrompt(question) {
|
|
|
528
569
|
// 5MB on slow links) — mirrors the web workspace uploader, plus bounded
|
|
529
570
|
// retries for transient network/5xx failures. 4xx means the presigned URL
|
|
530
571
|
// itself is bad; retrying cannot help.
|
|
531
|
-
export async function putAssetPartWithRetry({
|
|
572
|
+
export async function putAssetPartWithRetry({
|
|
573
|
+
url,
|
|
574
|
+
chunk,
|
|
575
|
+
mimeType,
|
|
576
|
+
partLabel,
|
|
577
|
+
}) {
|
|
532
578
|
let lastError = null;
|
|
533
579
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
534
580
|
try {
|
|
@@ -558,7 +604,7 @@ export async function readAssetUploadCandidate(filePath) {
|
|
|
558
604
|
const mimeType = ASSET_MIME_BY_EXTENSION[extension];
|
|
559
605
|
if (!mimeType) {
|
|
560
606
|
throw new Error(
|
|
561
|
-
`Unsupported asset type for ${fileName}. Build assets support
|
|
607
|
+
`Unsupported asset type for ${fileName}. Build assets support runtime media files (${Object.keys(ASSET_MIME_BY_EXTENSION).join(", ")}).`,
|
|
562
608
|
);
|
|
563
609
|
}
|
|
564
610
|
let buffer;
|
|
@@ -573,7 +619,105 @@ export async function readAssetUploadCandidate(filePath) {
|
|
|
573
619
|
if (!buffer.length) {
|
|
574
620
|
throw new Error(`Asset file is empty: ${filePath}`);
|
|
575
621
|
}
|
|
576
|
-
|
|
622
|
+
const referencedGltfAssetUrls =
|
|
623
|
+
extension === ".gltf"
|
|
624
|
+
? assertGltfUploadCandidateHasOnlySupportedUris({ fileName, buffer })
|
|
625
|
+
: [];
|
|
626
|
+
return {
|
|
627
|
+
absolutePath,
|
|
628
|
+
fileName,
|
|
629
|
+
mimeType,
|
|
630
|
+
buffer,
|
|
631
|
+
referencedGltfAssetUrls,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const TWINKLE_GLTF_ASSET_HOSTS = new Set([
|
|
636
|
+
"d3jvoamd2k4p0s.cloudfront.net",
|
|
637
|
+
"www.twin-kle.com",
|
|
638
|
+
"twin-kle.com",
|
|
639
|
+
]);
|
|
640
|
+
const TWINKLE_GLTF_ASSET_PATH_PREFIXES = [
|
|
641
|
+
"/attachments/build-runtime/",
|
|
642
|
+
"/attachments/optimized/",
|
|
643
|
+
];
|
|
644
|
+
|
|
645
|
+
function isSupportedGltfUri(uri) {
|
|
646
|
+
const normalizedUri = String(uri || "").trim();
|
|
647
|
+
if (!normalizedUri) return true;
|
|
648
|
+
const schemeMatch = normalizedUri.match(/^([A-Za-z][A-Za-z0-9+.-]*):/);
|
|
649
|
+
if (!schemeMatch) return false;
|
|
650
|
+
if (schemeMatch[1].toLowerCase() === "data") return true;
|
|
651
|
+
let parsedUri;
|
|
652
|
+
try {
|
|
653
|
+
parsedUri = new URL(normalizedUri);
|
|
654
|
+
} catch {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
return (
|
|
658
|
+
parsedUri.protocol === "https:" &&
|
|
659
|
+
!parsedUri.port &&
|
|
660
|
+
!parsedUri.username &&
|
|
661
|
+
!parsedUri.password &&
|
|
662
|
+
!parsedUri.search &&
|
|
663
|
+
!parsedUri.hash &&
|
|
664
|
+
TWINKLE_GLTF_ASSET_HOSTS.has(parsedUri.hostname.toLowerCase()) &&
|
|
665
|
+
TWINKLE_GLTF_ASSET_PATH_PREFIXES.some((prefix) =>
|
|
666
|
+
parsedUri.pathname.startsWith(prefix),
|
|
667
|
+
)
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function collectUnsupportedGltfUris(parsedGltf) {
|
|
672
|
+
const unsupportedUris = [];
|
|
673
|
+
if (!parsedGltf || typeof parsedGltf !== "object") {
|
|
674
|
+
return unsupportedUris;
|
|
675
|
+
}
|
|
676
|
+
for (const entry of [
|
|
677
|
+
...(Array.isArray(parsedGltf.buffers) ? parsedGltf.buffers : []),
|
|
678
|
+
...(Array.isArray(parsedGltf.images) ? parsedGltf.images : []),
|
|
679
|
+
]) {
|
|
680
|
+
const uri = typeof entry?.uri === "string" ? entry.uri.trim() : "";
|
|
681
|
+
if (uri && !isSupportedGltfUri(uri)) {
|
|
682
|
+
unsupportedUris.push(uri);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return unsupportedUris;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function assertGltfUploadCandidateHasOnlySupportedUris({ fileName, buffer }) {
|
|
689
|
+
let parsedGltf;
|
|
690
|
+
try {
|
|
691
|
+
parsedGltf = JSON.parse(buffer.toString("utf8"));
|
|
692
|
+
} catch {
|
|
693
|
+
throw new Error(`Invalid .gltf JSON in ${fileName}.`);
|
|
694
|
+
}
|
|
695
|
+
const unsupportedUris = collectUnsupportedGltfUris(parsedGltf);
|
|
696
|
+
if (unsupportedUris.length === 0) {
|
|
697
|
+
return [
|
|
698
|
+
...(Array.isArray(parsedGltf.buffers) ? parsedGltf.buffers : []),
|
|
699
|
+
...(Array.isArray(parsedGltf.images) ? parsedGltf.images : []),
|
|
700
|
+
]
|
|
701
|
+
.map((entry) => (typeof entry?.uri === "string" ? entry.uri.trim() : ""))
|
|
702
|
+
.filter((uri) => uri && !uri.toLowerCase().startsWith("data:"));
|
|
703
|
+
}
|
|
704
|
+
const sampleUris = unsupportedUris
|
|
705
|
+
.slice(0, 3)
|
|
706
|
+
.map((uri) => `"${uri}"`)
|
|
707
|
+
.join(", ");
|
|
708
|
+
throw new Error(
|
|
709
|
+
`${fileName} contains unsupported glTF URI${
|
|
710
|
+
unsupportedUris.length === 1 ? "" : "s"
|
|
711
|
+
} (${sampleUris}${unsupportedUris.length > 3 ? ", ..." : ""}). Raw .gltf companion files are stored under separate asset URLs, and Build apps cannot load arbitrary external URLs. Use .glb, embed data URIs, or rewrite .gltf URIs to Twinkle runtime attachment URLs before uploading.`,
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function runtimeAssetUrlMatchesGltfReference(asset, referencedUrl) {
|
|
716
|
+
const normalizedReference = String(referencedUrl || "").trim();
|
|
717
|
+
if (!normalizedReference) return false;
|
|
718
|
+
return [asset?.url, asset?.thumbUrl].some(
|
|
719
|
+
(assetUrl) => String(assetUrl || "").trim() === normalizedReference,
|
|
720
|
+
);
|
|
577
721
|
}
|
|
578
722
|
|
|
579
723
|
export async function uploadRuntimeAsset({
|
|
@@ -607,7 +751,10 @@ export async function uploadRuntimeAsset({
|
|
|
607
751
|
const parts = [];
|
|
608
752
|
for (let partNumber = 0; partNumber < urls.length; partNumber += 1) {
|
|
609
753
|
const start = partNumber * ASSET_UPLOAD_CHUNK_BYTES;
|
|
610
|
-
const end = Math.min(
|
|
754
|
+
const end = Math.min(
|
|
755
|
+
start + ASSET_UPLOAD_CHUNK_BYTES,
|
|
756
|
+
candidate.buffer.length,
|
|
757
|
+
);
|
|
611
758
|
const response = await putAssetPartWithRetry({
|
|
612
759
|
url: urls[partNumber],
|
|
613
760
|
chunk: candidate.buffer.subarray(start, end),
|
|
@@ -636,7 +783,9 @@ export async function uploadRuntimeAsset({
|
|
|
636
783
|
});
|
|
637
784
|
const asset = completed?.asset || prepared?.asset;
|
|
638
785
|
if (!asset?.url) {
|
|
639
|
-
throw new Error(
|
|
786
|
+
throw new Error(
|
|
787
|
+
`Upload of ${candidate.fileName} completed without an asset URL.`,
|
|
788
|
+
);
|
|
640
789
|
}
|
|
641
790
|
return asset;
|
|
642
791
|
} catch (error) {
|
|
@@ -745,7 +894,9 @@ export async function writeAssetsManifest({
|
|
|
745
894
|
const unmatchedReferences = refs
|
|
746
895
|
? refs.filter(
|
|
747
896
|
(ref) =>
|
|
748
|
-
!knownAssets.some((asset) =>
|
|
897
|
+
!knownAssets.some((asset) =>
|
|
898
|
+
runtimeAssetMatchesReference(asset, ref),
|
|
899
|
+
),
|
|
749
900
|
)
|
|
750
901
|
: null;
|
|
751
902
|
const metadataDir = path.join(dir, PROJECT_METADATA_DIR);
|
|
@@ -760,7 +911,9 @@ export async function writeAssetsManifest({
|
|
|
760
911
|
fetchedAt: new Date().toISOString(),
|
|
761
912
|
usage: usage || null,
|
|
762
913
|
assets: annotatedAssets,
|
|
763
|
-
...(annotatedProjectAssets
|
|
914
|
+
...(annotatedProjectAssets
|
|
915
|
+
? { projectAssets: annotatedProjectAssets }
|
|
916
|
+
: {}),
|
|
764
917
|
...(unmatchedReferences ? { unmatchedReferences } : {}),
|
|
765
918
|
},
|
|
766
919
|
null,
|
package/lib/constants.js
CHANGED
|
@@ -37,8 +37,9 @@ export const ASSETS_METADATA_FILE = "assets.json";
|
|
|
37
37
|
// Must match the web workspace's 5MB part size: the server presigns one S3
|
|
38
38
|
// part URL per 5MB of the declared fileSize.
|
|
39
39
|
export const ASSET_UPLOAD_CHUNK_BYTES = 5 * 1024 * 1024;
|
|
40
|
-
// Mirrors the web workspace's asset accept list
|
|
41
|
-
//
|
|
40
|
+
// Mirrors the web workspace's runtime asset accept list. The server rejects
|
|
41
|
+
// video; client allowlists keep project asset uploads focused on files loaders
|
|
42
|
+
// can consume from the sandbox.
|
|
42
43
|
export const ASSET_MIME_BY_EXTENSION = {
|
|
43
44
|
".png": "image/png",
|
|
44
45
|
".jpg": "image/jpeg",
|
|
@@ -60,6 +61,13 @@ export const ASSET_MIME_BY_EXTENSION = {
|
|
|
60
61
|
".flac": "audio/flac",
|
|
61
62
|
".aif": "audio/aiff",
|
|
62
63
|
".aiff": "audio/aiff",
|
|
64
|
+
".glb": "model/gltf-binary",
|
|
65
|
+
".gltf": "model/gltf+json",
|
|
66
|
+
".ktx2": "image/ktx2",
|
|
67
|
+
".hdr": "image/vnd.radiance",
|
|
68
|
+
".exr": "image/x-exr",
|
|
69
|
+
".bin": "application/octet-stream",
|
|
70
|
+
".drc": "application/octet-stream",
|
|
63
71
|
};
|
|
64
72
|
// Mirrors the platform's project-file limits. File count and effective-line
|
|
65
73
|
// limits are hardcoded server constants; the total-byte limit is
|
|
@@ -155,22 +163,28 @@ lumine save --summary "Describe the change"
|
|
|
155
163
|
need --allow-write and mutate real app data.
|
|
156
164
|
- Owned canonical builds may be published only when the user explicitly asks.
|
|
157
165
|
|
|
158
|
-
## Assets (
|
|
166
|
+
## Assets (Runtime Media)
|
|
159
167
|
|
|
160
|
-
- Binary files are NOT project files. Never place
|
|
168
|
+
- Binary files are NOT project files. Never place bundled media in this
|
|
161
169
|
workspace — lumine save rejects binaries. Assets live in Twinkle's asset
|
|
162
170
|
storage and are referenced from code by absolute URL.
|
|
163
171
|
- .twinkle/${ASSETS_METADATA_FILE} lists this build's uploaded assets (the current
|
|
164
172
|
CLI user's uploads) with their URLs. Reference an asset by its \`url\` value.
|
|
165
|
-
- \`lumine assets upload <file...>\` uploads
|
|
166
|
-
|
|
173
|
+
- \`lumine assets upload <file...>\` uploads supported runtime assets (images,
|
|
174
|
+
audio, GLB/self-contained glTF models, texture maps, HDR/EXR files, and glTF
|
|
175
|
+
companion files) from disk and prints the URL to use in code. Raw .gltf files
|
|
176
|
+
with relative companion URIs must be converted to .glb or rewritten to
|
|
177
|
+
absolute uploaded asset URLs first. Reference every uploaded asset that must
|
|
178
|
+
survive pruning from project files. \`lumine assets list\`
|
|
179
|
+
prints assets and refreshes
|
|
167
180
|
.twinkle/${ASSETS_METADATA_FILE}; \`lumine assets delete <assetId>\` removes one;
|
|
168
181
|
\`lumine assets prune\` deletes your uploads that nothing references (server
|
|
169
182
|
checks draft files, published/version snapshots, and derived builds; local
|
|
170
183
|
unsaved edits are scanned too). The server enforces storage quotas.
|
|
171
184
|
- CAUTION: asset URLs an app stores in privateDb/sharedDb/user DBs at runtime
|
|
172
|
-
(Twinkle.files uploads)
|
|
173
|
-
|
|
185
|
+
(Twinkle.files uploads), or URLs referenced only inside uploaded files such as
|
|
186
|
+
.gltf manifests, are invisible to prune. Keep those asset URLs in project
|
|
187
|
+
files too, or do not run \`assets prune\` for that build.
|
|
174
188
|
- On team projects the manifest also lists \`projectAssets\` (the project
|
|
175
189
|
owner's uploads) and \`unmatchedReferences\` (asset URLs in code matching no
|
|
176
190
|
known asset). Reuse existing asset URLs — do NOT re-upload media the
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build SDK Index
|
|
2
2
|
|
|
3
3
|
Version: 1.26.3
|
|
4
|
-
Updated: 2026-07-
|
|
5
|
-
Generated: 2026-07-
|
|
4
|
+
Updated: 2026-07-10
|
|
5
|
+
Generated: 2026-07-10T05:52:15.007Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -277,7 +277,7 @@ console.log(analysis.bestMove, analysis.evaluation, analysis.mate);
|
|
|
277
277
|
- Returns: { text, response, model, aiUsagePolicy }
|
|
278
278
|
- Generate text with the default Lumine text model, optionally streaming text updates through onText.
|
|
279
279
|
- Signed-in viewers only.
|
|
280
|
-
- Uses
|
|
280
|
+
- Uses Grok 4.5 by default.
|
|
281
281
|
- Each successful text generation consumes AI Energy from the signed-in viewer.
|
|
282
282
|
- history must be an array of { role: 'user' | 'assistant', content: string }. Twinkle.ai.chat does not read a text field.
|
|
283
283
|
- The server keeps the latest 12 valid history entries.
|
|
@@ -294,9 +294,9 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
|
|
|
294
294
|
- Use this instead of asking Twinkle.ai.chat to return JSON.
|
|
295
295
|
- expectedStructure must be a JSON object that describes the exact returned object shape.
|
|
296
296
|
- mode is accepted as an alias for thinkingMode, and mid is accepted as an alias for medium.
|
|
297
|
-
- thinkingMode low uses GPT
|
|
298
|
-
- thinkingMode medium uses GPT
|
|
299
|
-
- thinkingMode high uses
|
|
297
|
+
- thinkingMode low uses GPT-5.6 Luna and records free low-energy usage.
|
|
298
|
+
- thinkingMode medium uses GPT-5.6 Luna and normal AI Energy while AI Energy remains.
|
|
299
|
+
- thinkingMode high uses Grok 4.5 with high reasoning and high AI Energy while AI Energy remains.
|
|
300
300
|
- If medium or high is requested after AI Energy is empty, the server falls back to low and returns thinkingMode: low.
|
|
301
301
|
- The SDK validates shape and retries malformed JSON, but app code should still validate business-specific enum values.
|
|
302
302
|
- 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 } });
|
|
@@ -335,10 +335,9 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
|
|
|
335
335
|
- The character route also accepts text or message fields for compatibility, but generated apps should use content.
|
|
336
336
|
- The server keeps the latest 16 valid character history entries.
|
|
337
337
|
- Pass onText/onStatus for streaming dialogue. Omit callbacks for non-streaming dialogue where the promise resolves with the final response.
|
|
338
|
-
- thinkingMode low uses Lite Mode and
|
|
339
|
-
- thinkingMode medium uses normal AI Energy: Zero uses
|
|
340
|
-
- thinkingMode high uses high AI Energy: Zero uses
|
|
341
|
-
- Zero maps low/medium/high to GPT nano/GPT mini/full GPT. Ciel maps low/medium/high to Claude Haiku/Claude Sonnet/Claude Opus 4.8.
|
|
338
|
+
- thinkingMode low uses Lite Mode: Zero uses Grok 4.5 with low reasoning and Ciel uses Claude Haiku 4.5; usage is recorded as free low-energy usage.
|
|
339
|
+
- thinkingMode medium uses normal AI Energy: Zero uses Grok 4.5 with medium reasoning and Ciel uses Claude Sonnet 5 while AI Energy remains.
|
|
340
|
+
- thinkingMode high uses high AI Energy: Zero uses Grok 4.5 with high reasoning and Ciel uses Claude Opus 4.8 with extended thinking while AI Energy remains.
|
|
342
341
|
- If medium or high is requested after AI Energy is empty, the server falls back to low and returns thinkingMode: low.
|
|
343
342
|
- Pass roomContext as a short shared scene transcript so Zero and Ciel can know what happened in the same room.
|
|
344
343
|
- 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.
|