@stage5/lumine 0.3.18 → 0.3.19

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
@@ -96,8 +96,11 @@ run `lumine suggestions <build-url-or-id>` to see their open suggestion inbox.
96
96
  The inbox prints canonical follow-up commands for merging or replacing Main and
97
97
  for applying the exact frozen thumbnail shown in a suggestion. The same actions
98
98
  are also available directly as `lumine suggestions merge <id>`,
99
- `lumine suggestions replace-main <id>`, and
100
- `lumine suggestions adopt-thumbnail <id>`. Large inboxes are cursor-paginated;
99
+ `lumine suggestions replace-main <id>`,
100
+ `lumine suggestions adopt-thumbnail <id>`, and
101
+ `lumine suggestions adopt-title <id>`. To pass on a branch, thumbnail or name
102
+ suggestion, run `lumine suggestions decline <id>`; the chat card shows it was
103
+ declined for both sides and it leaves the inbox. Large inboxes are cursor-paginated;
101
104
  the CLI prints the exact `--cursor` command for the next page.
102
105
 
103
106
  Use `lumine explore` to list public open-source Build apps that can be used as
@@ -40,6 +40,33 @@ export function receiptToKeep(operation, result) {
40
40
  });
41
41
  }
42
42
  default:
43
+ // Bridge Builder writes answer with the whole crew view: members' who-
44
+ // are-you answers, parents' addresses and questions, held parent emails.
45
+ // Keep the decision only (ids, statuses, counts), never the crew.
46
+ if (String(operation?.name || "").startsWith("meetup.")) {
47
+ const crew = data.crew || {};
48
+ return keep({
49
+ crewId: crew.crewId ?? data.result?.crewId ?? null,
50
+ crewStatus: crew.status ?? null,
51
+ currentStep: crew.progress?.currentStep ?? null,
52
+ result: scalarsOf(data.result),
53
+ ...(data.slot ? { slot: scalarsOf(data.slot) } : {}),
54
+ ...(data.unlocks ? { unlocks: scalarsOf(data.unlocks) } : {}),
55
+ });
56
+ }
43
57
  return result;
44
58
  }
45
59
  }
60
+
61
+ // Numbers, booleans, short status words and arrays of ids: what proves a
62
+ // decision without copying anyone's text.
63
+ function scalarsOf(value) {
64
+ if (!value || typeof value !== "object") return null;
65
+ const out = {};
66
+ for (const [key, item] of Object.entries(value)) {
67
+ if (typeof item === "number" || typeof item === "boolean" || item === null) out[key] = item;
68
+ else if (typeof item === "string" && /^[\w:.-]{0,40}$/.test(item)) out[key] = item;
69
+ else if (Array.isArray(item) && item.every((x) => typeof x === "number")) out[key] = item;
70
+ }
71
+ return out;
72
+ }
package/lib/admin.js CHANGED
@@ -2583,7 +2583,7 @@ export function parseAdminOperation(options) {
2583
2583
  // reviews as the admin controls on the website quest page. Mikey's own
2584
2584
  // decision, any time, never in a daily run.
2585
2585
  const usage =
2586
- "Usage: lumine admin meetup list [--status review|active|completed|all] | approve-crew <crewId> | approve-grownup <crewId> | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | parent-reply <crewId> <memberUserId> --note <text> | funnel [--days <1-90>] | branches [list|show|approve|reject|merge] | show <crewId> | approve-plan <crewId> [--note <text>] | send-back <crewId> --note <text> | approve <crewId> --attended <userId,userId,...> [--note <text>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId>.";
2586
+ "Usage: lumine admin meetup list [--status review|active|completed|all] | approve-crew <crewId> | approve-grownup <crewId> | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | parent-reply <crewId> <memberUserId> --note <text> | funnel [--days <1-90>] | branches [list|show|approve|reject|merge] | show <crewId> | approve-plan <crewId> [--note <text>] | send-back <crewId> --note <text> | approve <crewId> --attended <userId,userId,...> [--note <text>] | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId>.";
2587
2587
  const note = String(options.note || "").trim();
2588
2588
  if (note.length > 1000) {
2589
2589
  throw cliValidationError("--note must be at most 1000 characters.");
@@ -2748,6 +2748,29 @@ export function parseAdminOperation(options) {
2748
2748
  { requiresRun: false },
2749
2749
  );
2750
2750
  }
2751
+ if (action === "info") {
2752
+ // Staff's "who are you" check on a member (ruled 2026-10-07): the member
2753
+ // gives their teacher's name and class name (or how they know the crew)
2754
+ // on the crew page; the crew waits until you accept. Nobody is contacted.
2755
+ const crewId = parseRequiredInteger(target, "Crew ID", 1);
2756
+ const memberId = parseRequiredInteger(extra, "Member user ID", 1);
2757
+ const decision = parseChoice(options.adminDecision || "", "--decision", [
2758
+ "ask",
2759
+ "accept",
2760
+ "ask-again",
2761
+ "withdraw",
2762
+ ]);
2763
+ if (decision === "ask-again" && !note) {
2764
+ throw cliValidationError("--decision ask-again needs --note <what to tell the member>.");
2765
+ }
2766
+ return writeOperation(
2767
+ "meetup.info",
2768
+ "POST",
2769
+ `/cli/admin/meetup-quest/crews/${crewId}/members/${memberId}/info-check`,
2770
+ { action: decision === "ask" ? "request" : decision, note },
2771
+ { requiresRun: false },
2772
+ );
2773
+ }
2751
2774
  if (action === "approve-crew" || action === "approve-grownup") {
2752
2775
  // Every step passes through the owner: the crew (members + branches) and
2753
2776
  // the grown-up step (parents' yes + the named adult).
@@ -5716,7 +5739,7 @@ function formatMeetupMembers(members) {
5716
5739
  return (members || [])
5717
5740
  .map(
5718
5741
  (member) =>
5719
- `${member.username || `user ${member.userId}`} (${member.userId}, ${member.branch || "no branch"}${member.parentOk === undefined ? "" : member.parentOk ? ", parent OK" : ", parent not yet"}${member.attended ? ", attended" : ""})`,
5742
+ `${member.username || `user ${member.userId}`} (${member.userId}, ${member.branch || "no branch"}${member.parentOk === undefined ? "" : member.parentOk ? ", parent OK" : ", parent not yet"}${member.attended ? ", attended" : ""}${member.infoCheck === "requested" ? ", details asked" : member.infoCheck === "answered" ? ", ANSWERED: review" : ""})`,
5720
5743
  )
5721
5744
  .join("; ");
5722
5745
  }
@@ -5731,6 +5754,17 @@ function printMeetupCrew(crew) {
5731
5754
  console.log(` ${step.number}. ${step.label} [${step.state}] ${step.detail}`);
5732
5755
  }
5733
5756
  console.log(` Members: ${formatMeetupMembers(crew.members)}`);
5757
+ // staff's "who are you" checks on these members (any crew), newest first
5758
+ for (const check of crew.memberChecks || []) {
5759
+ const answer = check.teacherName
5760
+ ? `teacher "${check.teacherName}", class "${check.className}"`
5761
+ : check.relationship
5762
+ ? `how they know the crew: "${check.relationship}"`
5763
+ : "no answer yet";
5764
+ console.log(
5765
+ ` Who-are-you check #${check.checkId} · ${check.username} (${check.userId}) · crew #${check.crewId} · ${check.status}${check.askNote ? ` · asked: "${check.askNote}"` : ""} · ${answer}${check.reviewedBy ? ` · ${check.status} by ${check.reviewedBy}` : ""}${check.staffNote ? ` · note: ${check.staffNote}` : ""}`,
5766
+ );
5767
+ }
5734
5768
  for (const parent of crew.parentContacts || []) {
5735
5769
  console.log(
5736
5770
  ` Parent of ${parent.childUsername}: ${parent.status}${parent.verifiedGuardian ? " (guardian on file)" : parent.staffVerified ? " (staff verified)" : " (child-typed address: NOT verified)"} · ${parent.email}${parent.question ? ` · asks: "${parent.question}"` : ""}`,
@@ -5875,11 +5909,21 @@ function printMeetupResult({ operation, data, result }) {
5875
5909
  }
5876
5910
  if (crews.some((crew) => crew.waitingFor)) {
5877
5911
  console.log(
5878
- "Review: lumine admin meetup show <crewId>, then approve-crew | approve-plan | approve-grownup | send-back --note | approve --attended <ids> (classroom plans: approve at the headmaster desk with a room and times).",
5912
+ "Review: lumine admin meetup show <crewId>, then approve-crew | approve-plan | approve-grownup | send-back --note | approve --attended <ids> | info <crewId> <userId> --decision accept|ask-again (a member's who-are-you answer) (classroom plans: approve at the headmaster desk with a room and times).",
5879
5913
  );
5880
5914
  }
5881
5915
  return;
5882
5916
  }
5917
+ if (operation.name === "meetup.info") {
5918
+ const status = data.result?.status;
5919
+ console.log(
5920
+ result?.changed === false
5921
+ ? `Member ${data.result?.userId} already has an open question (${status}).`
5922
+ : `Member ${data.result?.userId} on crew #${data.result?.crewId}: ${status === "requested" ? "asked (their crew page now asks for their teacher and class, or how they know the crew)" : status}.`,
5923
+ );
5924
+ if (data.crew) printMeetupCrew(data.crew);
5925
+ return;
5926
+ }
5883
5927
  if (operation.name === "meetup.show" || operation.name === "meetup.parent-reply") {
5884
5928
  printMeetupCrew(data.crew || {});
5885
5929
  return;
package/lib/api.js CHANGED
@@ -530,6 +530,24 @@ export async function adoptBuildTitleSuggestion({
530
530
  });
531
531
  }
532
532
 
533
+ // The owner passing on one teammate suggestion (branch updates, thumbnail or
534
+ // name): the card settles as declined for both sides and leaves the inbox.
535
+ export async function declineBuildOwnerSuggestion({
536
+ options,
537
+ auth,
538
+ rootBuildId,
539
+ contributionBuildId,
540
+ suggestionMessageId,
541
+ }) {
542
+ return await requestJson({
543
+ method: "POST",
544
+ url: `${options.apiUrl}/build/${rootBuildId}/contributions/${contributionBuildId}/decline-suggestion`,
545
+ authToken: auth.token,
546
+ body: { suggestionMessageId },
547
+ timeoutMs: options.timeoutMs,
548
+ });
549
+ }
550
+
533
551
  export async function mergeContributionIntoMain({
534
552
  options,
535
553
  auth,
package/lib/commands.js CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  import {
24
24
  adoptBuildThumbnailSuggestion,
25
25
  adoptBuildTitleSuggestion,
26
+ declineBuildOwnerSuggestion,
26
27
  createBuild,
27
28
  fetchAllRuntimeAssets,
28
29
  forkBuild,
@@ -764,11 +765,50 @@ export async function suggestions(options) {
764
765
  return;
765
766
  }
766
767
 
768
+ if (action === "decline") {
769
+ const description = describeSuggestionForDecline(suggestion);
770
+ if (!options.assumeYes) {
771
+ const confirmed = await confirmPrompt(
772
+ `Decline suggestion #${suggestionId} (${description}) on Build #${rootBuildId}? [y/N] `,
773
+ );
774
+ if (confirmed === null) {
775
+ console.log("Not a TTY — re-run with --yes to decline the suggestion.");
776
+ return;
777
+ }
778
+ if (!confirmed) {
779
+ console.log("Aborted. Suggestion left open.");
780
+ return;
781
+ }
782
+ }
783
+ await declineBuildOwnerSuggestion({
784
+ options,
785
+ auth,
786
+ rootBuildId,
787
+ contributionBuildId,
788
+ suggestionMessageId: suggestionId,
789
+ });
790
+ console.log(
791
+ `Declined suggestion #${suggestionId} (${description}) on Build #${rootBuildId}.`,
792
+ );
793
+ return;
794
+ }
795
+
767
796
  throw new Error(
768
- "Usage: lumine suggestions [build] | lumine suggestions merge|replace-main|adopt-thumbnail|adopt-title <suggestion-id> [--build <id>]",
797
+ "Usage: lumine suggestions [build] | lumine suggestions merge|replace-main|adopt-thumbnail|adopt-title|decline <suggestion-id> [--build <id>]",
769
798
  );
770
799
  }
771
800
 
801
+ function describeSuggestionForDecline(suggestion) {
802
+ if (suggestion.type === "title") {
803
+ return `name "${suggestion.suggestedTitle}"`;
804
+ }
805
+ if (suggestion.type === "thumbnail") return "thumbnail";
806
+ const branchNumber = Number(suggestion.branchNumber || 0);
807
+ return branchNumber
808
+ ? `updates from branch ${branchNumber}`
809
+ : `updates from branch #${suggestion.branchBuildId}`;
810
+ }
811
+
772
812
  async function resolveSuggestionRootBuildId(options, auth) {
773
813
  const requestedBuildId = options.buildIdFlag
774
814
  ? resolveRequiredBuildId(options.buildIdFlag)
@@ -827,6 +867,9 @@ export function printBuildSuggestions({
827
867
  console.log(
828
868
  ` replace: lumine suggestions replace-main ${suggestionId} --build ${rootBuildId}`,
829
869
  );
870
+ console.log(
871
+ ` decline: lumine suggestions decline ${suggestionId} --build ${rootBuildId}`,
872
+ );
830
873
  continue;
831
874
  }
832
875
  if (suggestion.type === "title") {
@@ -838,6 +881,9 @@ export function printBuildSuggestions({
838
881
  console.log(
839
882
  ` apply: lumine suggestions adopt-title ${suggestionId} --build ${rootBuildId}`,
840
883
  );
884
+ console.log(
885
+ ` decline: lumine suggestions decline ${suggestionId} --build ${rootBuildId}`,
886
+ );
841
887
  continue;
842
888
  }
843
889
  console.log(
@@ -850,6 +896,9 @@ export function printBuildSuggestions({
850
896
  console.log(
851
897
  ` apply: lumine suggestions adopt-thumbnail ${suggestionId} --build ${rootBuildId}`,
852
898
  );
899
+ console.log(
900
+ ` decline: lumine suggestions decline ${suggestionId} --build ${rootBuildId}`,
901
+ );
853
902
  }
854
903
  if (hasMore && nextCursor > 0) {
855
904
  console.log(
@@ -2429,6 +2478,7 @@ export function parseArgs(args) {
2429
2478
  "replace-main",
2430
2479
  "adopt-thumbnail",
2431
2480
  "adopt-title",
2481
+ "decline",
2432
2482
  ]);
2433
2483
  const suggestionAction =
2434
2484
  command === "suggest"
@@ -2970,6 +3020,7 @@ export function printHelp() {
2970
3020
  lumine suggestions replace-main <suggestion-id> [--build <id>]
2971
3021
  lumine suggestions adopt-thumbnail <suggestion-id> [--build <id>] [--yes]
2972
3022
  lumine suggestions adopt-title <suggestion-id> [--build <id>] [--yes]
3023
+ lumine suggestions decline <suggestion-id> [--build <id>] [--yes]
2973
3024
  lumine explore [search terms]
2974
3025
  lumine select [twinkle-build-url]
2975
3026
  lumine pull [twinkle-build-url]
@@ -3040,7 +3091,7 @@ export function printHelp() {
3040
3091
  lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
3041
3092
  lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
3042
3093
  lumine admin storage list [--status pending|approved|rejected|all] [--cursor <id>] | show <user-id-or-username> | approve <request-id> [--size <500MB|1GB|2GB|n MB>] [--reason <text>] | reject <request-id> [--reason <text>] | grant <user-id-or-username> --size <500MB|1GB|2GB|n MB> [--reason <text>]
3043
- lumine admin meetup list [--status review|active|completed|all] | approve-crew <crewId> | approve-grownup <crewId> | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | parent-reply <crewId> <memberUserId> --note <text> | funnel [--days <1-90>] | branches [list [--status pending|official|rejected|all] | show <id> | approve <id> | reject <id> [--note <text>] | merge <id> --into <officialId>] | show <crewId> | approve-plan <crewId> [--note <text>] | send-back <crewId> --note <text> | approve <crewId> --attended <userId,userId,...> [--note <text>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId> (Bridge Builder meetup quest)
3094
+ lumine admin meetup list [--status review|active|completed|all] | approve-crew <crewId> | approve-grownup <crewId> | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | parent-reply <crewId> <memberUserId> --note <text> | funnel [--days <1-90>] | branches [list [--status pending|official|rejected|all] | show <id> | approve <id> | reject <id> [--note <text>] | merge <id> --into <officialId>] | show <crewId> | approve-plan <crewId> [--note <text>] | send-back <crewId> --note <text> | approve <crewId> --attended <userId,userId,...> [--note <text>] | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId> (Bridge Builder meetup quest)
3044
3095
  lumine admin meetup story list | show <crewId> | approve <crewId> [--note <text>] [--no-announce] | send-back <crewId> --note <text> | unpublish <crewId> --note <text> (Bridge Builder story pages: publishes only what every tagged member's parent allowed)
3045
3096
  lumine admin reward-activity [--date YYYY-MM-DD] [--days <1..31>] [--build <id>] [--json]
3046
3097
  lumine admin chat-reports list [--status pending|open|reviewing|resolved|dismissed|all] [--cursor <id>] [--json]
package/lib/constants.js CHANGED
@@ -238,8 +238,9 @@ lumine save --summary "Describe the change"
238
238
  - On an owned canonical team project, \`lumine suggestions\` lists the owner's
239
239
  currently open branch, thumbnail and name suggestions. Act on the exact
240
240
  suggestion id it prints with \`lumine suggestions merge\`, \`replace-main\`,
241
- \`adopt-thumbnail\` or \`adopt-title\`; do not infer an action from stale
242
- local branch state.
241
+ \`adopt-thumbnail\`, \`adopt-title\` or \`decline\`; do not infer an action
242
+ from stale local branch state. Decline only when the user asks to pass on
243
+ that suggestion.
243
244
 
244
245
  ## Assets (Runtime Media)
245
246
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,7 +2,7 @@
2
2
 
3
3
  Version: 1.63.0
4
4
  Updated: 2026-09-28
5
- Generated: 2026-10-06T10:12:09.371Z
5
+ Generated: 2026-10-07T07:52:06.040Z
6
6
 
7
7
  ## Notes
8
8
  - This SDK is injected into Build iframes via the Build preview/runtime.
@@ -257,6 +257,7 @@ files:read, media:read, media:write, live:read, live:write, user:read, users:rea
257
257
  - title/body are accepted as aliases for label/summary.
258
258
  - target is accepted as a backwards-compatible alias for launchTarget.
259
259
  - Twinkle applies app API rate limits, a stricter notification emit rate limit, and existing notification mutes.
260
+ - Twinkle shows the acting member's username right before `summary` in the notification row, so write `summary` as a predicate ('posted in Lobby', 'responded to the Twinkle Newspaper'), never starting with the member's name (otherwise it reads 'Liki Liki responded…').
260
261
  - Example: await Twinkle.notifications.notifySubscribers('room.message', { targetKey: 'room:lobby', eventKey: 'room.message.created', label: 'Room messages', summary: 'posted in Lobby', launchTarget: { view: 'room', roomId: 'lobby', messageId } });
261
262
  - async getSubjectUpdateSubscription(subjectId) | scopes: notifications:read
262
263
  - Returns: { subscription }