@stage5/lumine 0.2.64 → 0.2.66

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/admin.js CHANGED
@@ -171,6 +171,7 @@ export async function adminCommand(options) {
171
171
  scope: operation.mutates ? "build:write" : "build:read",
172
172
  });
173
173
  let runId = 0;
174
+ let runScope = null;
174
175
  let correctionSessionId = 0;
175
176
  let correctionSession = null;
176
177
  if (adminOperationRequiresRun(operation)) {
@@ -216,6 +217,10 @@ export async function adminCommand(options) {
216
217
  }
217
218
  throw noActiveRunError();
218
219
  }
220
+ if (selectedRun) {
221
+ runScope = canonicalAdminRunScope(selectedRun);
222
+ assertAdminOperationAllowedForRunScope({ operation, runScope });
223
+ }
219
224
  if (options.adminIdentity && (selectedRun || correctionSession)) {
220
225
  const requestedIdentity = parseIdentity(options.adminIdentity);
221
226
  const canonicalIdentity = correctionSession
@@ -289,18 +294,20 @@ export async function adminCommand(options) {
289
294
  runId,
290
295
  fetchPage: fetchOperation,
291
296
  transformPage: transformResult,
292
- recordCoverage: async (coverage) =>
293
- requestJson({
294
- method: "POST",
295
- url: `${options.apiUrl}/cli/admin/daily-runs/coverage`,
296
- authToken: auth.token,
297
- body: coverage,
298
- headers: {
299
- "x-lumine-admin-run-id": String(runId),
300
- "x-lumine-idempotency-key": `cli:queue-coverage:${runId}:${adminValueFingerprint(coverage).slice(0, 32)}`,
301
- },
302
- timeoutMs: options.timeoutMs,
303
- }),
297
+ recordCoverage: shouldRecordAdminQueueCoverage(runScope)
298
+ ? async (coverage) =>
299
+ requestJson({
300
+ method: "POST",
301
+ url: `${options.apiUrl}/cli/admin/daily-runs/coverage`,
302
+ authToken: auth.token,
303
+ body: coverage,
304
+ headers: {
305
+ "x-lumine-admin-run-id": String(runId),
306
+ "x-lumine-idempotency-key": `cli:queue-coverage:${runId}:${adminValueFingerprint(coverage).slice(0, 32)}`,
307
+ },
308
+ timeoutMs: options.timeoutMs,
309
+ })
310
+ : undefined,
304
311
  });
305
312
  } else {
306
313
  if (options.adminResume) {
@@ -355,7 +362,7 @@ export async function adminCommand(options) {
355
362
  assertAiEmailPolicySetResult({ operation, result });
356
363
  }
357
364
  if (operation.name === "daily-run.start") {
358
- assertAdminTodoHandoffResult(result);
365
+ assertAdminTodoHandoffResult(result, operation.body.scope);
359
366
  }
360
367
  if (operation.name === "news.claim") {
361
368
  const artifacts = writeNewsClaimArtifacts({
@@ -449,7 +456,11 @@ async function finishAdminOutput({ options, operation, result }) {
449
456
  return result;
450
457
  }
451
458
  if (paginationStorage) {
452
- await printSpooledAdminResult({ result, storage: paginationStorage });
459
+ await printSpooledAdminResult({
460
+ operation,
461
+ result,
462
+ storage: paginationStorage,
463
+ });
453
464
  return result;
454
465
  }
455
466
  printAdminResult({ operation, result });
@@ -693,6 +704,58 @@ export function resolveOperatorViewFilter({ operation, unviewed, viewed }) {
693
704
  );
694
705
  }
695
706
 
707
+ function requestedOperatorViewFilter(options) {
708
+ return options.adminUnviewed
709
+ ? "unviewed"
710
+ : options.adminViewed
711
+ ? "viewed"
712
+ : null;
713
+ }
714
+
715
+ export function canonicalAdminRunScope(run) {
716
+ const raw = run?.runScope;
717
+ if (raw === undefined || raw === null || raw === "") return "full";
718
+ const scope = String(raw);
719
+ if (scope === "full" || scope === "featured") return scope;
720
+ throw cliValidationError("The API returned an invalid administrator run scope.");
721
+ }
722
+
723
+ export function shouldRecordAdminQueueCoverage(runScope) {
724
+ return runScope === "full";
725
+ }
726
+
727
+ const FEATURED_RUN_OPERATIONS = new Set([
728
+ "subjects.candidates",
729
+ "subject.get",
730
+ "subject.comments",
731
+ "subject.reveal",
732
+ "subject.feature",
733
+ "subject.unfeature",
734
+ "featured.list",
735
+ "featured.history",
736
+ "featured.add",
737
+ "featured.reorder",
738
+ "featured.rotate",
739
+ "daily-run.complete",
740
+ "daily-run.fail",
741
+ ]);
742
+
743
+ export function assertAdminOperationAllowedForRunScope({
744
+ operation,
745
+ runScope,
746
+ }) {
747
+ if (runScope !== "featured") return;
748
+ const subjectCommentAlias =
749
+ operation.name === "post.comments" &&
750
+ /^\/cli\/admin\/subjects\/\d+\/comments(?:\?|$)/.test(operation.path);
751
+ if (FEATURED_RUN_OPERATIONS.has(operation.name) || subjectCommentAlias) {
752
+ return;
753
+ }
754
+ throw cliValidationError(
755
+ `A Featured-only run does not authorize ${operation.name}. Complete or fail it before starting a full daily review.`,
756
+ );
757
+ }
758
+
696
759
  function adminOperationRequiresRun(operation) {
697
760
  return (
698
761
  ![
@@ -1095,16 +1158,26 @@ export function parseAdminOperation(options) {
1095
1158
 
1096
1159
  if (namespace === "daily-run") {
1097
1160
  if (action === "start") {
1161
+ const runScope = parseDailyRunScope(options.adminScope || "full");
1162
+ const commentMode = parseCommentMode(options.commentMode || "off");
1163
+ if (runScope === "featured" && commentMode !== "off") {
1164
+ throw cliValidationError(
1165
+ "A Featured-only run requires --comment-mode off.",
1166
+ );
1167
+ }
1098
1168
  return writeOperation(
1099
1169
  "daily-run.start",
1100
1170
  "POST",
1101
- "/cli/admin/daily-runs/start",
1171
+ runScope === "featured"
1172
+ ? "/cli/admin/daily-runs/start/featured"
1173
+ : "/cli/admin/daily-runs/start",
1102
1174
  {
1103
1175
  identity: options.adminIdentity
1104
1176
  ? parseIdentity(options.adminIdentity)
1105
1177
  : undefined,
1106
- commentMode: parseCommentMode(options.commentMode || "off"),
1107
- runKey: options.runKey || defaultDailyRunKey(),
1178
+ commentMode,
1179
+ scope: runScope,
1180
+ runKey: options.runKey || defaultDailyRunKey(runScope),
1108
1181
  },
1109
1182
  );
1110
1183
  }
@@ -1286,13 +1359,21 @@ export function parseAdminOperation(options) {
1286
1359
  if (action === "get") return subjectGetOperation(target, options);
1287
1360
  if (action === "comments") {
1288
1361
  const subjectId = parseSubjectId(target);
1362
+ const operatorView = requestedOperatorViewFilter(options);
1289
1363
  return readOperation(
1290
1364
  "subject.comments",
1291
1365
  withQuery(`/cli/admin/subjects/${subjectId}/comments`, {
1292
1366
  cursor: options.adminCursor,
1293
1367
  limit: options.limit,
1294
1368
  }),
1295
- { pagination: { collectionKey: "comments" } },
1369
+ {
1370
+ pagination: {
1371
+ collectionKey: "comments",
1372
+ filters: {
1373
+ ...(operatorView ? { operatorView } : {}),
1374
+ },
1375
+ },
1376
+ },
1296
1377
  );
1297
1378
  }
1298
1379
  if (action === "reveal") {
@@ -1330,6 +1411,29 @@ export function parseAdminOperation(options) {
1330
1411
  if (action === "list") {
1331
1412
  return readOperation("featured.list", "/cli/admin/subjects/featured");
1332
1413
  }
1414
+ if (action === "history") {
1415
+ const subjectIds = parseFeaturedSubjectIds(
1416
+ options.adminIds,
1417
+ "--subject-ids",
1418
+ );
1419
+ return readOperation(
1420
+ "featured.history",
1421
+ withQuery("/cli/admin/subjects/featured/history", {
1422
+ subjectIds: subjectIds.join(","),
1423
+ cursor: options.adminCursor,
1424
+ limit: options.limit,
1425
+ }),
1426
+ {
1427
+ pagination: {
1428
+ collectionKey: "events",
1429
+ filters: { subjectIds },
1430
+ },
1431
+ },
1432
+ );
1433
+ }
1434
+ if (action === "add") {
1435
+ return featuredAddOperation(options);
1436
+ }
1333
1437
  if (action === "reorder") {
1334
1438
  return featuredReorderOperation(options);
1335
1439
  }
@@ -1360,13 +1464,21 @@ export function parseAdminOperation(options) {
1360
1464
  parsedTarget.type === "subject"
1361
1465
  ? `/cli/admin/subjects/${parsedTarget.id}/comments`
1362
1466
  : `/cli/admin/posts/${parsedTarget.type}/${parsedTarget.id}/comments`;
1467
+ const operatorView = requestedOperatorViewFilter(options);
1363
1468
  return readOperation(
1364
1469
  "post.comments",
1365
1470
  withQuery(path, {
1366
1471
  cursor: options.adminCursor,
1367
1472
  limit: options.limit,
1368
1473
  }),
1369
- { pagination: { collectionKey: "comments" } },
1474
+ {
1475
+ pagination: {
1476
+ collectionKey: "comments",
1477
+ filters: {
1478
+ ...(operatorView ? { operatorView } : {}),
1479
+ },
1480
+ },
1481
+ },
1370
1482
  );
1371
1483
  }
1372
1484
  if (action === "recommend") {
@@ -1784,6 +1896,7 @@ export function parseAdminOperation(options) {
1784
1896
  }
1785
1897
 
1786
1898
  function subjectsListOperation(options) {
1899
+ const operatorView = requestedOperatorViewFilter(options);
1787
1900
  return readOperation(
1788
1901
  "subjects.candidates",
1789
1902
  withQuery("/cli/admin/subjects", {
@@ -1800,7 +1913,10 @@ function subjectsListOperation(options) {
1800
1913
  after: options.adminAfter
1801
1914
  ? parseAfterForCoverage(options.adminAfter)
1802
1915
  : null,
1803
- filters: { effort: options.adminEffort || "all" },
1916
+ filters: {
1917
+ effort: options.adminEffort || "all",
1918
+ ...(operatorView ? { operatorView } : {}),
1919
+ },
1804
1920
  },
1805
1921
  },
1806
1922
  );
@@ -1890,6 +2006,25 @@ function featuredReorderOperation(options) {
1890
2006
  );
1891
2007
  }
1892
2008
 
2009
+ function featuredAddOperation(options) {
2010
+ const addIds = parseFeaturedSubjectIds(
2011
+ options.adminIds,
2012
+ "--subject-ids",
2013
+ );
2014
+ const postedAfter = String(options.adminPostedAfter || "").trim();
2015
+ if (!postedAfter) {
2016
+ throw cliValidationError(
2017
+ "New Featured additions require --posted-after <ISO-8601-or-Unix-time>.",
2018
+ );
2019
+ }
2020
+ return writeOperation(
2021
+ "featured.add",
2022
+ "POST",
2023
+ "/cli/admin/subjects/featured/additions",
2024
+ { addIds, postedAfter },
2025
+ );
2026
+ }
2027
+
1893
2028
  function featuredRotateOperation(options) {
1894
2029
  const removeIds = parseFeaturedSubjectIds(
1895
2030
  options.adminRemoveIds,
@@ -2151,12 +2286,40 @@ export function formatAdminJsonError(error) {
2151
2286
  };
2152
2287
  }
2153
2288
 
2154
- export function assertAdminTodoHandoffResult(result) {
2289
+ export function assertAdminTodoHandoffResult(result, expectedRunScope = null) {
2155
2290
  const runId = Number(result?.data?.run?.id || 0);
2291
+ const runScope = canonicalAdminRunScope(result?.data?.run);
2292
+ if (expectedRunScope && runScope !== expectedRunScope) {
2293
+ const error = cliValidationError(
2294
+ `The API returned a ${runScope} run when the CLI requested ${expectedRunScope}.`,
2295
+ );
2296
+ error.code = "LUMINE_ADMIN_RUN_SCOPE_UNSUPPORTED";
2297
+ throw error;
2298
+ }
2156
2299
  const handoff = result?.data?.carryoverTodos;
2300
+ if (runScope === "featured") {
2301
+ if (
2302
+ !runId ||
2303
+ !handoff ||
2304
+ handoff.included !== false ||
2305
+ !Array.isArray(handoff.items) ||
2306
+ handoff.items.length !== 0 ||
2307
+ Number(handoff.count) !== 0 ||
2308
+ handoff.surfacedForRunId !== null ||
2309
+ Number(handoff.newlySurfacedCount) !== 0
2310
+ ) {
2311
+ const error = cliValidationError(
2312
+ "The API did not confirm that carry-over telemetry was suppressed for the Featured-only run.",
2313
+ );
2314
+ error.code = "LUMINE_ADMIN_SCOPED_RUN_UNSUPPORTED";
2315
+ throw error;
2316
+ }
2317
+ return handoff;
2318
+ }
2157
2319
  if (
2158
2320
  !runId ||
2159
2321
  !handoff ||
2322
+ handoff.included === false ||
2160
2323
  !Array.isArray(handoff.items) ||
2161
2324
  Number(handoff.count) !== handoff.items.length ||
2162
2325
  Number(handoff.surfacedForRunId) !== runId ||
@@ -2223,6 +2386,14 @@ function parseCommentMode(value) {
2223
2386
  return mode;
2224
2387
  }
2225
2388
 
2389
+ function parseDailyRunScope(value) {
2390
+ const scope = String(value || "full").trim().toLowerCase();
2391
+ if (!["full", "featured"].includes(scope)) {
2392
+ throw cliValidationError("--scope must be full or featured.");
2393
+ }
2394
+ return scope;
2395
+ }
2396
+
2226
2397
  function parseEscalationStatus(value) {
2227
2398
  const status = String(value || "")
2228
2399
  .trim()
@@ -2436,7 +2607,7 @@ function parseChoice(value, label, choices) {
2436
2607
  return normalized;
2437
2608
  }
2438
2609
 
2439
- function defaultDailyRunKey() {
2610
+ function defaultDailyRunKey(runScope = "full") {
2440
2611
  const parts = new Intl.DateTimeFormat("en-CA", {
2441
2612
  timeZone: "Asia/Bangkok",
2442
2613
  year: "numeric",
@@ -2446,7 +2617,10 @@ function defaultDailyRunKey() {
2446
2617
  const value = Object.fromEntries(
2447
2618
  parts.map((part) => [part.type, part.value]),
2448
2619
  );
2449
- return `daily:${value.year}-${value.month}-${value.day}`;
2620
+ const day = `${value.year}-${value.month}-${value.day}`;
2621
+ return runScope === "full"
2622
+ ? `daily:${day}`
2623
+ : `scoped:${runScope}:${day}:${randomUUID()}`;
2450
2624
  }
2451
2625
 
2452
2626
  function cliValidationError(message) {
@@ -2650,10 +2824,16 @@ function printAdminMonthlyMediaCosts(monthlyMediaCosts) {
2650
2824
  );
2651
2825
  }
2652
2826
 
2653
- async function printSpooledAdminResult({ result, storage }) {
2827
+ async function printSpooledAdminResult({ operation, result, storage }) {
2654
2828
  const data = result?.data || {};
2655
2829
  const count = Number(storage.candidateCount || 0);
2656
- if (storage.collectionKey === "subjects") {
2830
+ if (operation.name === "featured.history") {
2831
+ printFeaturedHistorySummary(data);
2832
+ console.log(`${count} Featured history event(s):`);
2833
+ await forEachPaginatedResultItem(result, async (event) => {
2834
+ printFeaturedHistoryEvent(event);
2835
+ });
2836
+ } else if (storage.collectionKey === "subjects") {
2657
2837
  console.log(`${count} subject(s):`);
2658
2838
  await forEachPaginatedResultItem(result, async (subject) => {
2659
2839
  console.log(
@@ -2930,13 +3110,13 @@ function printAdminResult({ operation, result }) {
2930
3110
  console.log("No active delegated administrator daily run.");
2931
3111
  if (data.lastRun) {
2932
3112
  console.log(
2933
- `Last run #${data.lastRun.id}: ${data.lastRun.status}; identity ${data.lastRun.identity.key}; comments ${data.lastRun.commentMode}.`,
3113
+ `Last run #${data.lastRun.id}: ${data.lastRun.status}; scope ${canonicalAdminRunScope(data.lastRun)}; identity ${data.lastRun.identity.key}; comments ${data.lastRun.commentMode}.`,
2934
3114
  );
2935
3115
  }
2936
3116
  return;
2937
3117
  }
2938
3118
  console.log(
2939
- `Run #${data.run.id}: ${data.run.status}; identity ${data.run.identity.key}; comments ${data.run.commentMode}.`,
3119
+ `Run #${data.run.id}: ${data.run.status}; scope ${canonicalAdminRunScope(data.run)}; identity ${data.run.identity.key}; comments ${data.run.commentMode}.`,
2940
3120
  );
2941
3121
  if (data.scheduledDay && data.scheduledIdentity) {
2942
3122
  console.log(
@@ -2967,11 +3147,20 @@ function printAdminResult({ operation, result }) {
2967
3147
  );
2968
3148
  console.log(
2969
3149
  data.activeRun
2970
- ? `Active run #${data.activeRun.id}: ${data.activeRun.identity.key}; comments ${data.activeRun.commentMode}.`
3150
+ ? `Active run #${data.activeRun.id}: scope ${canonicalAdminRunScope(data.activeRun)}; identity ${data.activeRun.identity.key}; comments ${data.activeRun.commentMode}.`
2971
3151
  : "No active delegated administrator daily run.",
2972
3152
  );
2973
3153
  return;
2974
3154
  }
3155
+ if (operation.name === "featured.history") {
3156
+ printFeaturedHistorySummary(data);
3157
+ console.log(`${(data.events || []).length} Featured history event(s):`);
3158
+ for (const event of data.events || []) {
3159
+ printFeaturedHistoryEvent(event);
3160
+ }
3161
+ printPagination(data.pagination);
3162
+ return;
3163
+ }
2975
3164
  if (Array.isArray(data.subjects)) {
2976
3165
  console.log(`${data.subjects.length} subject(s):`);
2977
3166
  for (const subject of data.subjects) {
@@ -3138,6 +3327,32 @@ function printAdminResult({ operation, result }) {
3138
3327
  );
3139
3328
  }
3140
3329
 
3330
+ function printFeaturedHistorySummary(data) {
3331
+ const coverage = data.coverage || {};
3332
+ console.log(
3333
+ coverage.complete
3334
+ ? `Featured history coverage begins at ${coverage.startedAt}.`
3335
+ : "Featured history coverage is not complete.",
3336
+ );
3337
+ for (const subject of data.subjects || []) {
3338
+ const lifetime = subject.knownFeatured
3339
+ ? "previously Featured"
3340
+ : subject.neverFeatured === true
3341
+ ? "never Featured"
3342
+ : "history unknown";
3343
+ console.log(
3344
+ `#${subject.id} ${subject.title || "(untitled)"} — ${lifetime}${subject.featured?.member ? ` — current position ${subject.featured.order}` : ""}`,
3345
+ );
3346
+ console.log(` ${subject.url}`);
3347
+ }
3348
+ }
3349
+
3350
+ function printFeaturedHistoryEvent(event) {
3351
+ console.log(
3352
+ `#${event.id} subject:${event.subjectId} ${event.action} ${event.fromPosition ?? "-"}->${event.toPosition ?? "-"} — ${event.operation}`,
3353
+ );
3354
+ }
3355
+
3141
3356
  function printTodoItems(items, heading) {
3142
3357
  console.log(`${heading}: ${items.length} item(s).`);
3143
3358
  for (const todo of items) {
package/lib/commands.js CHANGED
@@ -2364,6 +2364,7 @@ export function parseArgs(args) {
2364
2364
  cursor: Math.max(0, Math.floor(Number(raw.cursor) || 0)),
2365
2365
  adminCursor: raw.cursor ? String(raw.cursor) : "",
2366
2366
  adminAfter: raw.after ? String(raw.after) : "",
2367
+ adminPostedAfter: raw.postedAfter ? String(raw.postedAfter) : "",
2367
2368
  adminSinceRun: Boolean(raw.sinceRun),
2368
2369
  adminIncludeLegacy: Boolean(raw.includeLegacy),
2369
2370
  adminIncludePrivateEvidence: parseBoolean(
@@ -2457,6 +2458,7 @@ export function parseArgs(args) {
2457
2458
  adminUnviewed: Boolean(raw.unviewed),
2458
2459
  adminViewed: Boolean(raw.viewed),
2459
2460
  adminIdentity: raw.identity ? String(raw.identity) : "",
2461
+ adminScope: raw.scope ? String(raw.scope) : "",
2460
2462
  commentMode: raw.commentMode ? String(raw.commentMode) : "",
2461
2463
  runKey: raw.runKey ? String(raw.runKey) : "",
2462
2464
  idempotencyKey: raw.idempotencyKey ? String(raw.idempotencyKey) : "",
@@ -2818,7 +2820,7 @@ export function printHelp() {
2818
2820
  lumine admin ai-bucket note set --bucket-id <id> --note <text> [--json]
2819
2821
  lumine admin ai-email-policy get --email <address> [--json]
2820
2822
  lumine admin ai-email-policy set --email <address> --mode <automatic|separate_accounts> --note <text> [--json]
2821
- lumine admin daily-run start [--identity zero|ciel|auto] [--comment-mode off|draft|post] [--run-key <key>] [--json]
2823
+ lumine admin daily-run start [--scope full|featured] [--identity zero|ciel|auto] [--comment-mode off|draft|post] [--run-key <key>] [--json]
2822
2824
  lumine admin daily-run status|report|complete|fail [--reason <text>] [--json]
2823
2825
  lumine admin daily-run escalation add --target <target> --note <summary> [--severity attention|urgent] [--json]
2824
2826
  lumine admin escalation list [--status open|acknowledged|resolved|all] [--limit <number>] [--json]
@@ -2843,6 +2845,8 @@ export function printHelp() {
2843
2845
  lumine admin subject creator set-made-by-poster <subject-id> [--json]
2844
2846
  lumine admin subject feature|unfeature <subject-id> [--json]
2845
2847
  lumine admin featured list [--unviewed|--viewed] [--json]
2848
+ lumine admin featured history --subject-ids <id,id,...> [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
2849
+ lumine admin featured add --subject-ids <id,id,...> --posted-after <ISO-8601-or-Unix-time> [--json]
2846
2850
  lumine admin featured reorder --subject-ids <id,id,...> [--json]
2847
2851
  lumine admin featured rotate --remove-subject-ids <id,id,...> --add-subject-ids <id,id,...> [--json]
2848
2852
  lumine admin post get <target> [--type subject|comment|aiStory|dailyReflection] [--json]
@@ -2913,6 +2917,7 @@ Examples:
2913
2917
  npx @stage5/lumine@latest thumbnail generate --model gpt-image-2 --yes
2914
2918
  npx @stage5/lumine@latest doctor runtime-assets --build 917 --json
2915
2919
  npx @stage5/lumine@latest admin daily-run start --identity auto --comment-mode off --json
2920
+ npx @stage5/lumine@latest admin daily-run start --scope featured --identity auto --json
2916
2921
  npx @stage5/lumine@latest admin recommendations list --json
2917
2922
  npx @stage5/lumine@latest admin subjects candidates --effort unassigned --json
2918
2923
  npx @stage5/lumine@latest admin subject get 123 --include-comments --json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.64",
3
+ "version": "0.2.66",
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.38.2
4
- Updated: 2026-08-27
5
- Generated: 2026-08-27T12:11:09.595Z
3
+ Version: 1.39.0
4
+ Updated: 2026-09-03
5
+ Generated: 2026-09-03T04:17:20.106Z
6
6
 
7
7
  ## Notes
8
8
  - This SDK is injected into Build iframes via the Build preview/runtime.
@@ -256,8 +256,8 @@ files:read, media:read, media:write, live:read, live:write, user:read, users:rea
256
256
  - Unsubscribe the current viewer from Build notifications for a subject's new pages or updates.
257
257
 
258
258
  ### Twinkle.chess
259
- - async bestMove({ fen, depth?, skillLevel?, maxTimeMs?, timeoutMs? }) | scopes: none
260
- - Returns: { success, move, bestMove, from, to, promotion, evaluation, depth, mate, error, engine }
259
+ - async bestMove({ fen, depth?, skillLevel?, maxTimeMs?, timeoutMs?, multiPv? }) | scopes: none
260
+ - Returns: { success, move, bestMove, from, to, promotion, evaluation, depth, mate, lines: [{ rank, move, from, to, promotion, evaluation, mate, depth, pv }], error, engine }
261
261
  - Ask the parent-hosted Stockfish engine for the best move from a FEN position.
262
262
  - Always available in the build iframe.
263
263
  - Stockfish runs in a parent-managed worker with bounded depth, timeout, and serialized requests.
@@ -265,14 +265,18 @@ files:read, media:read, media:write, live:read, live:write, user:read, users:rea
265
265
  - skillLevel 20 defaults to the strongest bounded search budget.
266
266
  - maxTimeMs and timeoutMs are clamped between 500 and 60000 milliseconds.
267
267
  - This returns engine analysis only. Use app code or a chess rules library to validate legal moves, manage board state, detect game over, and render the board.
268
+ - multiPv (1-10, default 1) returns the top N moves as lines, ranked best first, all from the same search (a movetime stop can leave a line one iteration behind; keep the engine's rank order rather than re-sorting by score). Compare line evaluations against each other to grade a candidate move; do not compare evaluations from separate searches of different positions.
269
+ - evaluation and line evaluations are centipawns from the side to move's point of view; mate is a signed mate distance (positive = side to move mates).
268
270
  - Example: const result = await Twinkle.chess.bestMove({ fen: game.fen(), skillLevel: 8, maxTimeMs: 1000 });
269
271
  if (result.success) game.move({ from: result.from, to: result.to, promotion: result.promotion || undefined });
270
- - async evaluate({ fen, depth?, skillLevel?, maxTimeMs?, timeoutMs? }) | scopes: none
271
- - Returns: { success, move, bestMove, from, to, promotion, evaluation, depth, mate, error, engine }
272
+ - async evaluate({ fen, depth?, skillLevel?, maxTimeMs?, timeoutMs?, multiPv? }) | scopes: none
273
+ - Returns: { success, move, bestMove, from, to, promotion, evaluation, depth, mate, lines: [{ rank, move, from, to, promotion, evaluation, mate, depth, pv }], error, engine }
272
274
  - Analyze a FEN position and return Stockfish's current best move plus centipawn or mate evaluation.
273
275
  - Always available in the build iframe.
274
276
  - evaluation is the Stockfish centipawn score from the engine output when available; mate is the mate distance when Stockfish reports one.
275
277
  - Do not call this from a render loop, animation loop, or high-frequency polling path.
278
+ - multiPv (1-10, default 1) returns the top N moves as lines, ranked best first, all from the same search (a movetime stop can leave a line one iteration behind; keep the engine's rank order rather than re-sorting by score). Compare line evaluations against each other to grade a candidate move; do not compare evaluations from separate searches of different positions.
279
+ - evaluation and line evaluations are centipawns from the side to move's point of view; mate is a signed mate distance (positive = side to move mates).
276
280
  - Example: const analysis = await Twinkle.chess.evaluate({ fen: game.fen(), depth: 12 });
277
281
  console.log(analysis.bestMove, analysis.evaluation, analysis.mate);
278
282
 
@@ -463,11 +467,11 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
463
467
  - Use this instead of asking Twinkle.ai.chat to return JSON.
464
468
  - expectedStructure must be a JSON object that describes the exact returned object shape.
465
469
  - mode is accepted as an alias for thinkingMode, and mid is accepted as an alias for medium.
466
- - Omit model to use the normal Lite/Medium/High routing. model accepts gpt-5.6-sol, claude-opus-5, or claude-fable-5, and every explicit model must be paired with thinkingMode: 'high'; unknown model IDs reject instead of silently falling back.
470
+ - Omit model to use the normal Lite/Medium/High routing. model accepts gpt-5.6-sol, claude-opus-5, or claude-fable-5-1, and every explicit model must be paired with thinkingMode: 'high'; unknown model IDs reject instead of silently falling back.
467
471
  - thinkingMode low uses GPT-5.6 Luna and consumes the viewer's AI Energy from confirmed provider usage; its smaller model is usually cheaper than Medium or High.
468
472
  - thinkingMode medium uses Grok 4.6 with medium reasoning and consumes normal AI Energy.
469
473
  - thinkingMode high without model uses GPT-5.6 Sol with high reasoning and consumes high AI Energy. Explicit model: 'gpt-5.6-sol' selects Sol with xhigh reasoning at the same High AI Energy tier.
470
- - claude-opus-5 uses Anthropic adaptive High thinking. claude-fable-5 uses Anthropic xhigh thinking and normally consumes more AI Energy for comparable token use. Both debit confirmed provider usage at the High tier.
474
+ - claude-opus-5 uses Anthropic adaptive High thinking. claude-fable-5-1 uses Anthropic xhigh thinking and normally consumes more AI Energy for comparable token use. Both debit confirmed provider usage at the High tier.
471
475
  - Pass onStatus, onReasoning, and/or onText to stream progress from the same structured generation. onStatus receives high-level phases such as thinking, searching_web, responding, validating, and completed.
472
476
  - onReasoning receives accumulated provider-supplied, app-visible reasoning summaries plus { done, delta, requestId, status }. A provider retry may replace the accumulated summary; treat each callback's first argument as the current source of truth. This callback never exposes hidden/private model chain-of-thought.
473
477
  - onText receives accumulated structured-output text plus { done, delta, requestId, status }. Partial output is intentionally incomplete and may include provider formatting; parse only when done is true, when the callback receives the canonical object serialized as JSON, and use the resolved object as the source of truth.