@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 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: { thumbnailUrl },
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: raw.url || raw.target || positional[0] || "",
1815
+ target:
1816
+ raw.url ||
1817
+ raw.target ||
1818
+ (command === "rename" ? "" : positional[0] || ""),
1697
1819
  title:
1698
1820
  String(
1699
- raw.title || (command === "new" ? positional.join(" ") : ""),
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> New Build title
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
- if (metadata.mainCheckout === true) {
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 (metadata.versionCheckout === true) {
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 (isReadOnlyReferenceMetadata(metadata)) {
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 (metadata.build?.canWrite === false) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,8 +1,8 @@
1
1
  # Build SDK Index
2
2
 
3
- Version: 1.26.3
4
- Updated: 2026-07-10
5
- Generated: 2026-07-10T05:52:15.007Z
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.