@stage5/lumine 0.2.14 → 0.2.16
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 +7 -0
- package/lib/api.js +30 -1
- package/lib/commands.js +134 -3
- package/lib/constants.js +3 -0
- package/lib/workspace.js +17 -4
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +36 -5
package/README.md
CHANGED
|
@@ -7,7 +7,10 @@ npx @stage5/lumine@latest
|
|
|
7
7
|
npx @stage5/lumine@latest login
|
|
8
8
|
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
|
+
npx @stage5/lumine@latest rename "My New Build Title"
|
|
11
|
+
npx @stage5/lumine@latest rename "New Title" --target 123
|
|
10
12
|
npx @stage5/lumine@latest projects
|
|
13
|
+
npx @stage5/lumine@latest branches 884
|
|
11
14
|
npx @stage5/lumine@latest explore --sort forks
|
|
12
15
|
npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
|
|
13
16
|
npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
|
|
@@ -33,6 +36,10 @@ pulling the owner's main project creates or reuses your contribution branch and
|
|
|
33
36
|
checks out that branch locally. Saves go to your branch, so the project owner
|
|
34
37
|
can merge or replace main from Twinkle.
|
|
35
38
|
|
|
39
|
+
Use `lumine branches <build-url-or-id>` to list the contribution branches you
|
|
40
|
+
can review, including each contributor, branch number, status, and URL. Then use
|
|
41
|
+
`lumine diff <branch-url>` to inspect one branch.
|
|
42
|
+
|
|
36
43
|
Use `lumine explore` to list public open-source Build apps that can be used as
|
|
37
44
|
examples or starting points. It supports `--search` and `--sort forks`,
|
|
38
45
|
`--sort popular`, or `--sort recent`. Use `lumine reference <build-url-or-id>`
|
package/lib/api.js
CHANGED
|
@@ -30,6 +30,21 @@ export async function listOpenSourceBuilds({ options, auth }) {
|
|
|
30
30
|
return Array.isArray(result.builds) ? result.builds : [];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export async function listContributionBranches({
|
|
34
|
+
options,
|
|
35
|
+
auth,
|
|
36
|
+
buildId,
|
|
37
|
+
limit,
|
|
38
|
+
}) {
|
|
39
|
+
const url = new URL(`${options.apiUrl}/build/${buildId}/contributions`);
|
|
40
|
+
url.searchParams.set("limit", String(limit));
|
|
41
|
+
return await requestJson({
|
|
42
|
+
url: url.toString(),
|
|
43
|
+
authToken: auth.token,
|
|
44
|
+
timeoutMs: options.timeoutMs,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
export async function loadBuildMetadata({ options, auth, buildId }) {
|
|
34
49
|
const result = await loadBuildFiles({
|
|
35
50
|
options,
|
|
@@ -290,12 +305,26 @@ export async function updateBuildThumbnailUrl({
|
|
|
290
305
|
auth,
|
|
291
306
|
buildId,
|
|
292
307
|
thumbnailUrl,
|
|
308
|
+
}) {
|
|
309
|
+
return updateBuildMetadata({
|
|
310
|
+
options,
|
|
311
|
+
auth,
|
|
312
|
+
buildId,
|
|
313
|
+
patch: { thumbnailUrl },
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function updateBuildMetadata({
|
|
318
|
+
options,
|
|
319
|
+
auth,
|
|
320
|
+
buildId,
|
|
321
|
+
patch,
|
|
293
322
|
}) {
|
|
294
323
|
return requestJson({
|
|
295
324
|
method: "PUT",
|
|
296
325
|
url: `${options.apiUrl}/build/${buildId}`,
|
|
297
326
|
authToken: auth.token,
|
|
298
|
-
body:
|
|
327
|
+
body: patch,
|
|
299
328
|
timeoutMs: options.timeoutMs,
|
|
300
329
|
});
|
|
301
330
|
}
|
package/lib/commands.js
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
loadBuildVersionFiles,
|
|
32
32
|
loadBuildVersions,
|
|
33
33
|
loadContributionDiff,
|
|
34
|
+
listContributionBranches,
|
|
34
35
|
loadLumineCliVersionInfo,
|
|
35
36
|
loadOpenSourceBuildFiles,
|
|
36
37
|
maybeCheckForLumineCliUpdate,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
replaceMainWithContribution,
|
|
41
42
|
resolveBranchBuild,
|
|
42
43
|
saveProjectFiles,
|
|
44
|
+
updateBuildMetadata,
|
|
43
45
|
} from "./api.js";
|
|
44
46
|
import { assetsCommand, writeAssetsManifest } from "./assets.js";
|
|
45
47
|
import { thumbnailCommand } from "./thumbnail.js";
|
|
@@ -75,6 +77,7 @@ import {
|
|
|
75
77
|
collectProjectLimitFindings,
|
|
76
78
|
findLocalProjectMetadata,
|
|
77
79
|
isIndexHtmlPath,
|
|
80
|
+
isReadOnlyProjectMetadata,
|
|
78
81
|
isReadOnlyReferenceMetadata,
|
|
79
82
|
removeLocalProjectFilesNotIn,
|
|
80
83
|
stashLocalProjectFilesBeforePull,
|
|
@@ -121,10 +124,18 @@ export async function main() {
|
|
|
121
124
|
await newBuild(options);
|
|
122
125
|
return;
|
|
123
126
|
}
|
|
127
|
+
if (options.command === "rename") {
|
|
128
|
+
await renameBuild(options);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
124
131
|
if (options.command === "projects") {
|
|
125
132
|
await projects(options);
|
|
126
133
|
return;
|
|
127
134
|
}
|
|
135
|
+
if (options.command === "branches") {
|
|
136
|
+
await branches(options);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
128
139
|
if (options.command === "explore") {
|
|
129
140
|
await explore(options);
|
|
130
141
|
return;
|
|
@@ -221,6 +232,59 @@ export async function newBuild(options) {
|
|
|
221
232
|
printNewBuildResult({ createResult, pullResult: result });
|
|
222
233
|
}
|
|
223
234
|
|
|
235
|
+
export async function renameBuild(options) {
|
|
236
|
+
const title = String(options.title || "").trim();
|
|
237
|
+
if (!title) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
'Pass a title: `lumine rename "My New Build Title"` or `lumine rename --title "My New Build Title"`.',
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const auth = await ensureAuth(options);
|
|
243
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
244
|
+
const localProject = await findLocalProjectMetadata(
|
|
245
|
+
path.resolve(options.dir || process.cwd()),
|
|
246
|
+
);
|
|
247
|
+
const buildId = await resolveRequiredBuildIdOrSelected(options, auth, {
|
|
248
|
+
localProject,
|
|
249
|
+
});
|
|
250
|
+
const currentBuild = await loadBuildMetadata({ options, auth, buildId });
|
|
251
|
+
if (currentBuild.canWrite === false) {
|
|
252
|
+
throw new Error(`You cannot rename Build #${buildId}.`);
|
|
253
|
+
}
|
|
254
|
+
if (Number(currentBuild.contributionRootBuildId || 0) > 0) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
"Contribution branches use the original Build title and cannot be renamed.",
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const result = await updateBuildMetadata({
|
|
260
|
+
options,
|
|
261
|
+
auth,
|
|
262
|
+
buildId,
|
|
263
|
+
patch: { title },
|
|
264
|
+
});
|
|
265
|
+
if (result?.success !== true || !result?.build) {
|
|
266
|
+
throw new Error(result?.error || "Failed to rename the Build.");
|
|
267
|
+
}
|
|
268
|
+
const updatedBuild = { ...currentBuild, ...result.build };
|
|
269
|
+
await saveSelectedBuild({ options, auth, build: updatedBuild });
|
|
270
|
+
if (
|
|
271
|
+
localProject?.rootDir &&
|
|
272
|
+
Number(localProject.metadata?.buildId || 0) === buildId &&
|
|
273
|
+
!isReadOnlyProjectMetadata(localProject.metadata)
|
|
274
|
+
) {
|
|
275
|
+
await writeProjectMetadata({
|
|
276
|
+
dir: localProject.rootDir,
|
|
277
|
+
options,
|
|
278
|
+
build: updatedBuild,
|
|
279
|
+
manifest: localProject.metadata.manifest || null,
|
|
280
|
+
pulledAt: localProject.metadata.pulledAt || null,
|
|
281
|
+
lastSavedAt: localProject.metadata.lastSavedAt || null,
|
|
282
|
+
filesHash: localProject.metadata.filesHash || null,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
console.log(`Renamed Build #${buildId} to "${updatedBuild.title}".`);
|
|
286
|
+
}
|
|
287
|
+
|
|
224
288
|
export async function workspace(options) {
|
|
225
289
|
const auth = await ensureAuth(options);
|
|
226
290
|
const selectedBuild = options.target
|
|
@@ -246,6 +310,33 @@ export async function projects(options) {
|
|
|
246
310
|
printBuildList(builds);
|
|
247
311
|
}
|
|
248
312
|
|
|
313
|
+
export async function branches(options) {
|
|
314
|
+
const auth = await resolveAuth(options);
|
|
315
|
+
const requestedBuildId = await resolveRequiredBuildIdOrSelected(
|
|
316
|
+
options,
|
|
317
|
+
auth,
|
|
318
|
+
);
|
|
319
|
+
const requestedBuild = await loadBuildMetadata({
|
|
320
|
+
options,
|
|
321
|
+
auth,
|
|
322
|
+
buildId: requestedBuildId,
|
|
323
|
+
});
|
|
324
|
+
const rootBuildId =
|
|
325
|
+
Number(requestedBuild.contributionRootBuildId || 0) ||
|
|
326
|
+
Number(requestedBuild.id || 0);
|
|
327
|
+
const rootBuild =
|
|
328
|
+
rootBuildId === Number(requestedBuild.id || 0)
|
|
329
|
+
? requestedBuild
|
|
330
|
+
: await loadBuildMetadata({ options, auth, buildId: rootBuildId });
|
|
331
|
+
const result = await listContributionBranches({
|
|
332
|
+
options,
|
|
333
|
+
auth,
|
|
334
|
+
buildId: rootBuildId,
|
|
335
|
+
limit: options.limit,
|
|
336
|
+
});
|
|
337
|
+
printContributionBranches({ result, rootBuild, options });
|
|
338
|
+
}
|
|
339
|
+
|
|
249
340
|
export async function explore(options) {
|
|
250
341
|
const auth = await resolveAuth(options);
|
|
251
342
|
const builds = await listOpenSourceBuilds({ options, auth });
|
|
@@ -1561,6 +1652,34 @@ export function printContributionDiff({ result, build }) {
|
|
|
1561
1652
|
}
|
|
1562
1653
|
}
|
|
1563
1654
|
|
|
1655
|
+
export function printContributionBranches({ result, rootBuild, options }) {
|
|
1656
|
+
const contributions = Array.isArray(result?.contributions)
|
|
1657
|
+
? result.contributions
|
|
1658
|
+
: [];
|
|
1659
|
+
console.log(`Branches for ${formatBuildTitle(rootBuild)}:`);
|
|
1660
|
+
if (!contributions.length) {
|
|
1661
|
+
console.log("No contribution branches.");
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
for (const [index, contribution] of contributions.entries()) {
|
|
1665
|
+
const branchNumber = Number(contribution.contributionBranchNumber || 0);
|
|
1666
|
+
const branchLabel = branchNumber
|
|
1667
|
+
? `branch ${branchNumber}`
|
|
1668
|
+
: `branch #${contribution.id}`;
|
|
1669
|
+
const title = String(contribution.title || "Untitled branch").trim();
|
|
1670
|
+
const contributor = String(contribution.username || "unknown").trim();
|
|
1671
|
+
const status = String(contribution.contributionStatus || "draft").trim();
|
|
1672
|
+
console.log(
|
|
1673
|
+
`${index + 1}. ${title} - ${contributor} - ${branchLabel} (${status})`,
|
|
1674
|
+
);
|
|
1675
|
+
if (branchNumber) {
|
|
1676
|
+
console.log(
|
|
1677
|
+
` ${options.siteUrl}/build/${Number(rootBuild.id)}/${branchNumber}`,
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1564
1683
|
export function printUpdateFromMainResult({ result, build, dir, mergedFiles }) {
|
|
1565
1684
|
const branchNumber = Number(build?.contributionBranchNumber || 0) || 0;
|
|
1566
1685
|
const branchLabel = branchNumber
|
|
@@ -1693,10 +1812,16 @@ export function parseArgs(args) {
|
|
|
1693
1812
|
quality: raw.quality ? String(raw.quality) : "",
|
|
1694
1813
|
assetName: raw.name ? String(raw.name) : "",
|
|
1695
1814
|
out: raw.out ? String(raw.out) : "",
|
|
1696
|
-
target:
|
|
1815
|
+
target:
|
|
1816
|
+
raw.url ||
|
|
1817
|
+
raw.target ||
|
|
1818
|
+
(command === "rename" ? "" : positional[0] || ""),
|
|
1697
1819
|
title:
|
|
1698
1820
|
String(
|
|
1699
|
-
raw.title ||
|
|
1821
|
+
raw.title ||
|
|
1822
|
+
(command === "new" || command === "rename"
|
|
1823
|
+
? positional.join(" ")
|
|
1824
|
+
: ""),
|
|
1700
1825
|
).trim() || "",
|
|
1701
1826
|
description: Object.prototype.hasOwnProperty.call(raw, "description")
|
|
1702
1827
|
? String(raw.description || "").trim()
|
|
@@ -1916,7 +2041,9 @@ export function printHelp() {
|
|
|
1916
2041
|
lumine whoami
|
|
1917
2042
|
lumine logout
|
|
1918
2043
|
lumine new [title]
|
|
2044
|
+
lumine rename [title] [--target <twinkle-build-url-or-id>]
|
|
1919
2045
|
lumine projects
|
|
2046
|
+
lumine branches [twinkle-build-url-or-id] [--limit <n>]
|
|
1920
2047
|
lumine explore [search terms]
|
|
1921
2048
|
lumine select [twinkle-build-url]
|
|
1922
2049
|
lumine pull [twinkle-build-url]
|
|
@@ -1949,7 +2076,10 @@ Examples:
|
|
|
1949
2076
|
npx @stage5/lumine@latest
|
|
1950
2077
|
npx @stage5/lumine@latest login
|
|
1951
2078
|
npx @stage5/lumine@latest new "Daily Reflection App"
|
|
2079
|
+
npx @stage5/lumine@latest rename "My New Build Title"
|
|
2080
|
+
npx @stage5/lumine@latest rename "New Title" --target 123
|
|
1952
2081
|
npx @stage5/lumine@latest new --title "Daily Reflection App" --description "Private journal with streaks"
|
|
2082
|
+
npx @stage5/lumine@latest branches 884
|
|
1953
2083
|
npx @stage5/lumine@latest explore --sort forks
|
|
1954
2084
|
npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
|
|
1955
2085
|
npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
|
|
@@ -1983,9 +2113,10 @@ Options:
|
|
|
1983
2113
|
--auth-file <path> Saved login path
|
|
1984
2114
|
--auth-token <token> Override saved login
|
|
1985
2115
|
--dir <path> Directory for pulled project files
|
|
2116
|
+
--target <build> Explicit Build URL or ID for rename (positionals are title-only)
|
|
1986
2117
|
--main With pull/versions/restore: target the team project's main
|
|
1987
2118
|
--version <n> With pull: read-only checkout of previous save v<n>
|
|
1988
|
-
--title <text>
|
|
2119
|
+
--title <text> Build title for new/rename
|
|
1989
2120
|
--description <text> Optional New Build description
|
|
1990
2121
|
--no-description Skip the New Build description prompt
|
|
1991
2122
|
--summary <text> Save summary
|
package/lib/constants.js
CHANGED
|
@@ -308,6 +308,7 @@ export const AGENT_INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
|
308
308
|
export const MAIN_CHECKOUT_READONLY_COMMANDS = new Set([
|
|
309
309
|
"pull",
|
|
310
310
|
"check",
|
|
311
|
+
"branches",
|
|
311
312
|
"diff",
|
|
312
313
|
"sdk",
|
|
313
314
|
"select",
|
|
@@ -324,7 +325,9 @@ export const COMMANDS = new Set([
|
|
|
324
325
|
"logout",
|
|
325
326
|
"whoami",
|
|
326
327
|
"new",
|
|
328
|
+
"rename",
|
|
327
329
|
"projects",
|
|
330
|
+
"branches",
|
|
328
331
|
"explore",
|
|
329
332
|
"select",
|
|
330
333
|
"pull",
|
package/lib/workspace.js
CHANGED
|
@@ -779,14 +779,15 @@ export function resolveProjectDirForSave({ options, localProject }) {
|
|
|
779
779
|
export function assertLocalProjectCanBeSaved(localProject) {
|
|
780
780
|
const metadata = localProject?.metadata;
|
|
781
781
|
if (!metadata) return;
|
|
782
|
-
|
|
782
|
+
const readOnlyKind = readOnlyProjectMetadataKind(metadata);
|
|
783
|
+
if (readOnlyKind === "main") {
|
|
783
784
|
const rootBuildId =
|
|
784
785
|
Number(metadata.buildId || 0) || Number(metadata.build?.id || 0) || 0;
|
|
785
786
|
throw new Error(
|
|
786
787
|
`This is a read-only checkout of main${rootBuildId ? ` for Build ${rootBuildId}` : ""}. Make edits in your branch workspace (\`lumine pull${rootBuildId ? ` ${rootBuildId}` : ""}\`), and run \`lumine update-from-main\` there to bring main's changes into it.`,
|
|
787
788
|
);
|
|
788
789
|
}
|
|
789
|
-
if (
|
|
790
|
+
if (readOnlyKind === "version") {
|
|
790
791
|
const checkoutBuildId =
|
|
791
792
|
Number(metadata.buildId || 0) || Number(metadata.build?.id || 0) || 0;
|
|
792
793
|
const checkoutVersion = Number(metadata.checkoutVersion || 0) || 0;
|
|
@@ -794,7 +795,7 @@ export function assertLocalProjectCanBeSaved(localProject) {
|
|
|
794
795
|
`This is a read-only checkout of a previous save${checkoutVersion ? ` (v${checkoutVersion})` : ""}${checkoutBuildId ? ` for Build ${checkoutBuildId}` : ""}. To bring this save back, run \`lumine restore${checkoutVersion ? ` ${checkoutVersion}` : " <n>"}\` from the editable workspace, then \`lumine save\`.`,
|
|
795
796
|
);
|
|
796
797
|
}
|
|
797
|
-
if (
|
|
798
|
+
if (readOnlyKind === "reference") {
|
|
798
799
|
const sourceBuildId =
|
|
799
800
|
Number(metadata.reference?.sourceBuildId || 0) ||
|
|
800
801
|
Number(metadata.buildId || 0) ||
|
|
@@ -804,7 +805,7 @@ export function assertLocalProjectCanBeSaved(localProject) {
|
|
|
804
805
|
`This is a read-only Lumine reference${sourceBuildId ? ` for Build ${sourceBuildId}` : ""}. Run \`lumine fork${sourceBuildId ? ` ${sourceBuildId}` : ""}\` to create an editable workspace.`,
|
|
805
806
|
);
|
|
806
807
|
}
|
|
807
|
-
if (
|
|
808
|
+
if (readOnlyKind === "server") {
|
|
808
809
|
throw new Error(
|
|
809
810
|
"This Lumine checkout is read-only for the current CLI login. Pull or diff it for review; project-owner branch edits must go through merge or replace-main.",
|
|
810
811
|
);
|
|
@@ -819,6 +820,18 @@ export function isReadOnlyReferenceMetadata(metadata) {
|
|
|
819
820
|
);
|
|
820
821
|
}
|
|
821
822
|
|
|
823
|
+
export function isReadOnlyProjectMetadata(metadata) {
|
|
824
|
+
return readOnlyProjectMetadataKind(metadata) !== null;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export function readOnlyProjectMetadataKind(metadata) {
|
|
828
|
+
if (metadata?.mainCheckout === true) return "main";
|
|
829
|
+
if (metadata?.versionCheckout === true) return "version";
|
|
830
|
+
if (isReadOnlyReferenceMetadata(metadata)) return "reference";
|
|
831
|
+
if (metadata?.build?.canWrite === false) return "server";
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
|
|
822
835
|
export function resolveLocalProjectFilePath({ rootDir, projectPath }) {
|
|
823
836
|
const relativePath = projectPathToRelativePath(projectPath);
|
|
824
837
|
const root = path.resolve(rootDir);
|
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-07-
|
|
5
|
-
Generated: 2026-07-
|
|
3
|
+
Version: 1.27.0
|
|
4
|
+
Updated: 2026-07-21
|
|
5
|
+
Generated: 2026-07-23T05:57:27.486Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -25,7 +25,7 @@ Generated: 2026-07-10T05:52:15.007Z
|
|
|
25
25
|
- 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
26
|
|
|
27
27
|
## Token Scopes
|
|
28
|
-
files:read, user:read, users:read, dailyReflections:read, content:read, sharedDb:read, sharedDb:write, privateDb:read, privateDb:write, files:write, chat:read, chat:write, notifications:read, notifications:write, notifications:emit, reminders:read, reminders:write
|
|
28
|
+
files:read, user:read, users:read, dailyReflections:read, content:read, content:write, sharedDb:read, sharedDb:write, privateDb:read, privateDb:write, files:write, chat:read, chat:write, notifications:read, notifications:write, notifications:emit, reminders:read, reminders:write
|
|
29
29
|
|
|
30
30
|
## Namespaces
|
|
31
31
|
|
|
@@ -80,6 +80,14 @@ files:read, user:read, users:read, dailyReflections:read, content:read, sharedDb
|
|
|
80
80
|
- The parent validates that the target is still a Build preview URL before navigating.
|
|
81
81
|
- External URLs are rejected and do not receive the Build bridge nonce.
|
|
82
82
|
- Example: await Twinkle.app.navigate('./arena.html');
|
|
83
|
+
- async openContent(target) | scopes: none
|
|
84
|
+
- Returns: { success, url }
|
|
85
|
+
- Open a recognized Twinkle content page in the parent app from a viewer click or tap.
|
|
86
|
+
- Call this directly from a user click or tap handler; calls without an active user action are rejected.
|
|
87
|
+
- The trusted parent displays the canonical destination and requires viewer confirmation before navigating.
|
|
88
|
+
- Only recognized Twinkle content URLs are accepted. The parent preserves its current signed-in origin when opening the content.
|
|
89
|
+
- Use navigate() for routes inside the current Build and openContent() for Twinkle subjects, comments, apps, profiles, and other content pages.
|
|
90
|
+
- Example: await Twinkle.app.openContent('https://www.twin-kle.com/subjects/432');
|
|
83
91
|
|
|
84
92
|
### Twinkle.preview
|
|
85
93
|
- getLayout() | scopes: none
|
|
@@ -310,6 +318,9 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
|
|
|
310
318
|
- Generate or edit an image from a prompt and optional base64/data-URL reference image.
|
|
311
319
|
- Signed-in viewers only.
|
|
312
320
|
- Each successful image generation consumes AI Energy from the signed-in viewer.
|
|
321
|
+
- Call generateImage directly from an explicit viewer action such as a button click. Calls from page load, timers, background work, or programmatic retries are rejected.
|
|
322
|
+
- Twinkle shows a host-owned confirmation for every generation. One approval authorizes exactly one request.
|
|
323
|
+
- Only one image generation may be active at a time. Do not queue or automatically retry cancellation, ai_image_generation_in_progress, USER_ACTIVATION_REQUIRED, or 429 errors.
|
|
313
324
|
- Default engine is openai and default quality is high.
|
|
314
325
|
- The SDK timeout defaults to 390000ms for image generation because high-quality image runs can exceed normal request timing.
|
|
315
326
|
- Pass onStatus to receive real-time stages from the backend: prompt_ready, in_progress, generating, partial_image, completed, and error.
|
|
@@ -382,6 +393,19 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
382
393
|
- Returns: { comments: [{ id, content, filePath, fileName, fileSize, thumbUrl, timeStamp }], cursor? }
|
|
383
394
|
- Returns only the current viewer's own comments on the given subject.
|
|
384
395
|
- Supports cursor-based pagination. Pass cursor from previous response to load more.
|
|
396
|
+
- async getWriteStatus({ subjectId, commentId } = {}) | scopes: content:read
|
|
397
|
+
- Returns: { writeStatus: { serverNow, subjectCreate, commentCreate, subjectEdit, commentEdit } }
|
|
398
|
+
- Each operation slice is { cooldownSeconds, availableAt, retryAfterSeconds }.
|
|
399
|
+
- serverNow is unix seconds so progress bars ignore client clock skew.
|
|
400
|
+
- Pass subjectId/commentId to include per-target edit cooldowns.
|
|
401
|
+
- async create({ title, description }) | scopes: content:write
|
|
402
|
+
- Returns: { subject, writeStatus }
|
|
403
|
+
- Creates a normal site subject (title + description only in v1).
|
|
404
|
+
- Site-wide durable cooldown: 600s between creates. 429 includes writeStatus.
|
|
405
|
+
- async edit({ subjectId, title, description }) | scopes: content:write
|
|
406
|
+
- Returns: { subject, writeStatus }
|
|
407
|
+
- Own subjects only (userId === uploader). Never uses moderator edit rights.
|
|
408
|
+
- Per-subject edit cooldown: 10s.
|
|
385
409
|
|
|
386
410
|
### Twinkle.aiCards
|
|
387
411
|
- async list({ limit, cursor, level, minLevel, maxLevel, quality, userId, hasImage, hasExample } = {}) | scopes: content:read
|
|
@@ -468,6 +492,13 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
468
492
|
- For subject-poster books that include poster replies, use author: subjectPoster, includeReplies: true, and replyScope: ownThread so the poster's replies to other people do not become pages.
|
|
469
493
|
- Supports cursor-based pagination. Pass cursor from the previous response to load more.
|
|
470
494
|
- Example: const { subjects } = await Twinkle.subjects.search({ query: searchText, limit: 12 }); const subjectId = pickedSubject.id; const page = await Twinkle.subjectComments.list(subjectId, { sortBy: 'oldest', author: 'subjectPoster', includeReplies: true, replyScope: 'ownThread', limit: 50 });
|
|
495
|
+
- async create({ subjectId, content }) | scopes: content:write
|
|
496
|
+
- Returns: { comment, writeStatus }
|
|
497
|
+
- Adds a top-level subject comment (book page). Own subject only.
|
|
498
|
+
- Site-wide durable cooldown: 20s between comment creates. 429 includes writeStatus.
|
|
499
|
+
- async edit({ commentId, content }) | scopes: content:write
|
|
500
|
+
- Returns: { comment, writeStatus }
|
|
501
|
+
- Own comments only. Per-comment edit cooldown: 10s.
|
|
471
502
|
|
|
472
503
|
### Twinkle.profileComments
|
|
473
504
|
- async getProfileComments({ profileUserId, limit, offset, sortBy, includeReplies, range, since, until } = {}) | scopes: content:read
|
|
@@ -540,7 +571,7 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
540
571
|
- Update a viewer-owned shared row, optionally notifying safe recipients from the canonical write.
|
|
541
572
|
- Updates an entry. Only the entry creator or the build owner can update.
|
|
542
573
|
- data must be a JSON object, max 10 KB.
|
|
543
|
-
- notify may include eventKey, label, summary, recipients, and target. Supported recipients include { kind: 'sharedDbEntryAuthor', entryId }.
|
|
574
|
+
- notify may include eventKey, label, summary, recipients, and target. Supported recipients include { kind: 'sharedDbEntryAuthor', entryId } and { kind: 'subjectAuthor', subjectId } (subject must be referenced by this build via a subject-linked sharedDb entry).
|
|
544
575
|
- async deleteEntry(entryId) | scopes: sharedDb:write
|
|
545
576
|
- Returns: { success: true }
|
|
546
577
|
- Deletes an entry. Only the entry creator or the build owner can delete.
|