@stage5/lumine 0.2.13 → 0.2.15
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 +10 -1
- package/lib/api.js +19 -1
- package/lib/commands.js +111 -9
- package/lib/constants.js +7 -0
- package/lib/workspace.js +2 -1
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +36 -5
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@ 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
|
|
11
13
|
npx @stage5/lumine@latest explore --sort forks
|
|
12
14
|
npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
|
|
@@ -40,7 +42,14 @@ to pull source files into a read-only reference folder, or
|
|
|
40
42
|
`lumine fork <build-url-or-id>` to create your own editable fork and pull it
|
|
41
43
|
locally.
|
|
42
44
|
|
|
43
|
-
After editing pulled files, run `lumine save` from that folder.
|
|
45
|
+
After editing pulled files, run `lumine save` from that folder. Workspaces record
|
|
46
|
+
a `filesHash` in `.twinkle/lumine-project.json` when you pull or save; the next
|
|
47
|
+
save sends that hash so the server can reject a stale checkout instead of
|
|
48
|
+
silently overwriting newer files. Saves from a folder with no `filesHash` are
|
|
49
|
+
refused unless you pass `--force` (intentional overwrite). Prefer
|
|
50
|
+
`lumine pull` to resync before saving if the folder might be out of date.
|
|
51
|
+
|
|
52
|
+
The CLI saves
|
|
44
53
|
through Twinkle's normal workspace project-file route, creates a project artifact
|
|
45
54
|
version, records the same save metadata, and marks public builds as having
|
|
46
55
|
unpublished changes. For projects you own, run `lumine launch` to publish the
|
package/lib/api.js
CHANGED
|
@@ -139,6 +139,7 @@ export async function saveProjectFiles({
|
|
|
139
139
|
files,
|
|
140
140
|
summary,
|
|
141
141
|
baseFilesHash,
|
|
142
|
+
force = false,
|
|
142
143
|
}) {
|
|
143
144
|
return await requestJson({
|
|
144
145
|
method: "PUT",
|
|
@@ -152,6 +153,9 @@ export async function saveProjectFiles({
|
|
|
152
153
|
// reject it instead of silently rewinding newer state (e.g. a branch
|
|
153
154
|
// merged into main after our pull).
|
|
154
155
|
...(baseFilesHash ? { baseFilesHash } : {}),
|
|
156
|
+
// Intentional overwrite of a non-empty project without a base claim.
|
|
157
|
+
// Required when the workspace has no filesHash (old/stale checkout).
|
|
158
|
+
...(force ? { force: true } : {}),
|
|
155
159
|
},
|
|
156
160
|
timeoutMs: options.timeoutMs,
|
|
157
161
|
});
|
|
@@ -286,12 +290,26 @@ export async function updateBuildThumbnailUrl({
|
|
|
286
290
|
auth,
|
|
287
291
|
buildId,
|
|
288
292
|
thumbnailUrl,
|
|
293
|
+
}) {
|
|
294
|
+
return updateBuildMetadata({
|
|
295
|
+
options,
|
|
296
|
+
auth,
|
|
297
|
+
buildId,
|
|
298
|
+
patch: { thumbnailUrl },
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export async function updateBuildMetadata({
|
|
303
|
+
options,
|
|
304
|
+
auth,
|
|
305
|
+
buildId,
|
|
306
|
+
patch,
|
|
289
307
|
}) {
|
|
290
308
|
return requestJson({
|
|
291
309
|
method: "PUT",
|
|
292
310
|
url: `${options.apiUrl}/build/${buildId}`,
|
|
293
311
|
authToken: auth.token,
|
|
294
|
-
body:
|
|
312
|
+
body: patch,
|
|
295
313
|
timeoutMs: options.timeoutMs,
|
|
296
314
|
});
|
|
297
315
|
}
|
package/lib/commands.js
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
replaceMainWithContribution,
|
|
41
41
|
resolveBranchBuild,
|
|
42
42
|
saveProjectFiles,
|
|
43
|
+
updateBuildMetadata,
|
|
43
44
|
} from "./api.js";
|
|
44
45
|
import { assetsCommand, writeAssetsManifest } from "./assets.js";
|
|
45
46
|
import { thumbnailCommand } from "./thumbnail.js";
|
|
@@ -121,6 +122,10 @@ export async function main() {
|
|
|
121
122
|
await newBuild(options);
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
125
|
+
if (options.command === "rename") {
|
|
126
|
+
await renameBuild(options);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
124
129
|
if (options.command === "projects") {
|
|
125
130
|
await projects(options);
|
|
126
131
|
return;
|
|
@@ -221,6 +226,58 @@ export async function newBuild(options) {
|
|
|
221
226
|
printNewBuildResult({ createResult, pullResult: result });
|
|
222
227
|
}
|
|
223
228
|
|
|
229
|
+
export async function renameBuild(options) {
|
|
230
|
+
const title = String(options.title || "").trim();
|
|
231
|
+
if (!title) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
'Pass a title: `lumine rename "My New Build Title"` or `lumine rename --title "My New Build Title"`.',
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const auth = await ensureAuth(options);
|
|
237
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
238
|
+
const localProject = await findLocalProjectMetadata(
|
|
239
|
+
path.resolve(options.dir || process.cwd()),
|
|
240
|
+
);
|
|
241
|
+
const buildId = await resolveRequiredBuildIdOrSelected(options, auth, {
|
|
242
|
+
localProject,
|
|
243
|
+
});
|
|
244
|
+
const currentBuild = await loadBuildMetadata({ options, auth, buildId });
|
|
245
|
+
if (currentBuild.canWrite === false) {
|
|
246
|
+
throw new Error(`You cannot rename Build #${buildId}.`);
|
|
247
|
+
}
|
|
248
|
+
if (Number(currentBuild.contributionRootBuildId || 0) > 0) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
"Contribution branches use the original Build title and cannot be renamed.",
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
const result = await updateBuildMetadata({
|
|
254
|
+
options,
|
|
255
|
+
auth,
|
|
256
|
+
buildId,
|
|
257
|
+
patch: { title },
|
|
258
|
+
});
|
|
259
|
+
if (result?.success !== true || !result?.build) {
|
|
260
|
+
throw new Error(result?.error || "Failed to rename the Build.");
|
|
261
|
+
}
|
|
262
|
+
const updatedBuild = { ...currentBuild, ...result.build };
|
|
263
|
+
await saveSelectedBuild({ options, auth, build: updatedBuild });
|
|
264
|
+
if (
|
|
265
|
+
localProject?.rootDir &&
|
|
266
|
+
Number(localProject.metadata?.buildId || 0) === buildId
|
|
267
|
+
) {
|
|
268
|
+
await writeProjectMetadata({
|
|
269
|
+
dir: localProject.rootDir,
|
|
270
|
+
options,
|
|
271
|
+
build: updatedBuild,
|
|
272
|
+
manifest: localProject.metadata.manifest || null,
|
|
273
|
+
pulledAt: localProject.metadata.pulledAt || null,
|
|
274
|
+
lastSavedAt: localProject.metadata.lastSavedAt || null,
|
|
275
|
+
filesHash: localProject.metadata.filesHash || null,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
console.log(`Renamed Build #${buildId} to "${updatedBuild.title}".`);
|
|
279
|
+
}
|
|
280
|
+
|
|
224
281
|
export async function workspace(options) {
|
|
225
282
|
const auth = await ensureAuth(options);
|
|
226
283
|
const selectedBuild = options.target
|
|
@@ -509,13 +566,36 @@ export async function save(options) {
|
|
|
509
566
|
const files = await collectProjectFiles(dir);
|
|
510
567
|
assertProjectFilesWithinLimits(files);
|
|
511
568
|
// Only claim a base snapshot when this workspace was pulled from the same
|
|
512
|
-
// build we are saving to
|
|
569
|
+
// build we are saving to. Missing filesHash used to mean "unguarded save",
|
|
570
|
+
// which let stale checkouts silently rewind newer server versions — refuse
|
|
571
|
+
// that path unless --force is explicit.
|
|
572
|
+
const metadataBuildId = Number(localProject?.metadata?.buildId || 0);
|
|
573
|
+
const metadataFilesHash =
|
|
574
|
+
typeof localProject?.metadata?.filesHash === "string" &&
|
|
575
|
+
localProject.metadata.filesHash.trim()
|
|
576
|
+
? localProject.metadata.filesHash.trim()
|
|
577
|
+
: null;
|
|
513
578
|
const baseFilesHash =
|
|
514
|
-
!options.force &&
|
|
515
|
-
|
|
516
|
-
typeof localProject?.metadata?.filesHash === "string"
|
|
517
|
-
? localProject.metadata.filesHash
|
|
579
|
+
!options.force && metadataBuildId === Number(buildId) && metadataFilesHash
|
|
580
|
+
? metadataFilesHash
|
|
518
581
|
: null;
|
|
582
|
+
if (!options.force && !baseFilesHash) {
|
|
583
|
+
throw new Error(
|
|
584
|
+
[
|
|
585
|
+
"Save refused: this workspace has no filesHash (server snapshot token).",
|
|
586
|
+
"That usually means an old or stale checkout that never recorded what",
|
|
587
|
+
"it was based on — saving it would overwrite the server without a guard.",
|
|
588
|
+
"Run `lumine pull` to sync with the server (local edits are preserved),",
|
|
589
|
+
"then save again. To intentionally overwrite the server with these",
|
|
590
|
+
"local files, re-run with --force.",
|
|
591
|
+
].join("\n"),
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
if (options.force && !baseFilesHash) {
|
|
595
|
+
console.error(
|
|
596
|
+
"lumine: warning — saving with --force and no filesHash; this overwrites the server project with local files and skips the stale-workspace guard.",
|
|
597
|
+
);
|
|
598
|
+
}
|
|
519
599
|
let result;
|
|
520
600
|
try {
|
|
521
601
|
result = await saveProjectFiles({
|
|
@@ -525,6 +605,7 @@ export async function save(options) {
|
|
|
525
605
|
files,
|
|
526
606
|
summary: options.summary || DEFAULT_SAVE_SUMMARY,
|
|
527
607
|
baseFilesHash,
|
|
608
|
+
force: Boolean(options.force),
|
|
528
609
|
});
|
|
529
610
|
} catch (error) {
|
|
530
611
|
if (error?.data?.code === "build_project_files_stale") {
|
|
@@ -537,6 +618,17 @@ export async function save(options) {
|
|
|
537
618
|
].join("\n"),
|
|
538
619
|
);
|
|
539
620
|
}
|
|
621
|
+
if (error?.data?.code === "build_project_files_base_required") {
|
|
622
|
+
throw new Error(
|
|
623
|
+
[
|
|
624
|
+
"Save rejected: the server requires a filesHash base for this project",
|
|
625
|
+
"(non-empty projects cannot be saved from a workspace that never",
|
|
626
|
+
"recorded its server snapshot).",
|
|
627
|
+
"Run `lumine pull` to establish a base, then save again,",
|
|
628
|
+
"or re-run with --force to overwrite deliberately.",
|
|
629
|
+
].join("\n"),
|
|
630
|
+
);
|
|
631
|
+
}
|
|
540
632
|
throw error;
|
|
541
633
|
}
|
|
542
634
|
build = result.build ||
|
|
@@ -1658,10 +1750,16 @@ export function parseArgs(args) {
|
|
|
1658
1750
|
quality: raw.quality ? String(raw.quality) : "",
|
|
1659
1751
|
assetName: raw.name ? String(raw.name) : "",
|
|
1660
1752
|
out: raw.out ? String(raw.out) : "",
|
|
1661
|
-
target:
|
|
1753
|
+
target:
|
|
1754
|
+
raw.url ||
|
|
1755
|
+
raw.target ||
|
|
1756
|
+
(command === "rename" ? "" : positional[0] || ""),
|
|
1662
1757
|
title:
|
|
1663
1758
|
String(
|
|
1664
|
-
raw.title ||
|
|
1759
|
+
raw.title ||
|
|
1760
|
+
(command === "new" || command === "rename"
|
|
1761
|
+
? positional.join(" ")
|
|
1762
|
+
: ""),
|
|
1665
1763
|
).trim() || "",
|
|
1666
1764
|
description: Object.prototype.hasOwnProperty.call(raw, "description")
|
|
1667
1765
|
? String(raw.description || "").trim()
|
|
@@ -1881,6 +1979,7 @@ export function printHelp() {
|
|
|
1881
1979
|
lumine whoami
|
|
1882
1980
|
lumine logout
|
|
1883
1981
|
lumine new [title]
|
|
1982
|
+
lumine rename [title] [--target <twinkle-build-url-or-id>]
|
|
1884
1983
|
lumine projects
|
|
1885
1984
|
lumine explore [search terms]
|
|
1886
1985
|
lumine select [twinkle-build-url]
|
|
@@ -1914,6 +2013,8 @@ Examples:
|
|
|
1914
2013
|
npx @stage5/lumine@latest
|
|
1915
2014
|
npx @stage5/lumine@latest login
|
|
1916
2015
|
npx @stage5/lumine@latest new "Daily Reflection App"
|
|
2016
|
+
npx @stage5/lumine@latest rename "My New Build Title"
|
|
2017
|
+
npx @stage5/lumine@latest rename "New Title" --target 123
|
|
1917
2018
|
npx @stage5/lumine@latest new --title "Daily Reflection App" --description "Private journal with streaks"
|
|
1918
2019
|
npx @stage5/lumine@latest explore --sort forks
|
|
1919
2020
|
npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
|
|
@@ -1948,13 +2049,14 @@ Options:
|
|
|
1948
2049
|
--auth-file <path> Saved login path
|
|
1949
2050
|
--auth-token <token> Override saved login
|
|
1950
2051
|
--dir <path> Directory for pulled project files
|
|
2052
|
+
--target <build> Explicit Build URL or ID for rename (positionals are title-only)
|
|
1951
2053
|
--main With pull/versions/restore: target the team project's main
|
|
1952
2054
|
--version <n> With pull: read-only checkout of previous save v<n>
|
|
1953
|
-
--title <text>
|
|
2055
|
+
--title <text> Build title for new/rename
|
|
1954
2056
|
--description <text> Optional New Build description
|
|
1955
2057
|
--no-description Skip the New Build description prompt
|
|
1956
2058
|
--summary <text> Save summary
|
|
1957
|
-
--force
|
|
2059
|
+
--force Overwrite server files even if this workspace is stale or missing filesHash
|
|
1958
2060
|
--search <text> Search public open-source Builds
|
|
1959
2061
|
--sort <sort> Sort open-source Builds: forks, popular, recent
|
|
1960
2062
|
--publish Publish after saving
|
package/lib/constants.js
CHANGED
|
@@ -143,6 +143,12 @@ Lumine CLI as the source of truth for saving this workspace back to Twinkle.
|
|
|
143
143
|
|
|
144
144
|
- Edit only project files in this workspace.
|
|
145
145
|
- Keep /index.html or /index.htm as the entry file.
|
|
146
|
+
- Before editing an existing project, confirm \`.twinkle/lumine-project.json\`
|
|
147
|
+
has a \`filesHash\` and matches the build you intend. If the folder may be
|
|
148
|
+
stale (old pull, missing files, fewer files than last known save), run
|
|
149
|
+
\`lumine pull\` first — never \`lumine save\` from a guessed/old checkout.
|
|
150
|
+
Saves without \`filesHash\` are refused (use \`--force\` only for deliberate
|
|
151
|
+
overwrites). A stale save can delete newer server files.
|
|
146
152
|
- Run lumine save from this folder after edits, with a short summary:
|
|
147
153
|
|
|
148
154
|
\`\`\`bash
|
|
@@ -318,6 +324,7 @@ export const COMMANDS = new Set([
|
|
|
318
324
|
"logout",
|
|
319
325
|
"whoami",
|
|
320
326
|
"new",
|
|
327
|
+
"rename",
|
|
321
328
|
"projects",
|
|
322
329
|
"explore",
|
|
323
330
|
"select",
|
package/lib/workspace.js
CHANGED
|
@@ -597,7 +597,8 @@ export async function writeProjectMetadata({
|
|
|
597
597
|
lastSavedAt,
|
|
598
598
|
// Server-issued hash of the project files this workspace is based on.
|
|
599
599
|
// `lumine save` sends it back so a stale workspace cannot silently
|
|
600
|
-
// rewind newer server state
|
|
600
|
+
// rewind newer server state. Missing/null refuses save unless --force
|
|
601
|
+
// (server also rejects unguarded saves onto non-empty projects).
|
|
601
602
|
filesHash: typeof filesHash === "string" ? filesHash : null,
|
|
602
603
|
},
|
|
603
604
|
null,
|
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.
|